package io.delphiplatform.api.v3.rdb.service.ads;

import com.google.common.collect.Lists;
import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.ComboCondition;
import com.healthmarketscience.sqlbuilder.Condition;
import com.healthmarketscience.sqlbuilder.SelectQuery;
import com.healthmarketscience.sqlbuilder.UnaryCondition;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbTable;

import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.delphiplatform.api.util.JpaUtils;
import io.delphiplatform.api.util.SqlBuilderUtils;
import io.delphiplatform.api.v3.constant.ColumnNameConstants;
import io.delphiplatform.api.v3.decibelrdb.entity.DecibelCampaignEntity;
import io.delphiplatform.api.v3.decibelrdb.repository.DecibelCampaignEntityRepository;
import io.delphiplatform.api.v3.model.ads.CampaignSimple;
import io.delphiplatform.api.v3.rdb.repository.LinkfireDimReferrerRepository;
import io.delphiplatform.api.v3.rdb.service.EntityService;
import io.delphiplatform.api.v3.rdb.service.ads.query.sqlbuilder.query.CampaignSelectQuery;
import io.delphiplatform.api.v3.rdb.service.specification.SpecificationProvider;
import io.delphiplatform.api.v3.view.util.Params;

import static io.delphiplatform.api.util.CollectionUtils.isEmpty;
import static io.delphiplatform.api.util.CollectionUtils.isNotEmpty;

@Service
public class DecibelCampaignService extends EntityService<DecibelCampaignEntity> {

    private final DecibelCampaignEntityRepository repository;
    private final SpecificationProvider<DecibelCampaignEntity> specificationProvider;
    private final LinkfireDimReferrerRepository dimReferrerRepository;

    public DecibelCampaignService(DecibelCampaignEntityRepository repository,
        SpecificationProvider<DecibelCampaignEntity> specificationProvider,
        LinkfireDimReferrerRepository dimReferrerRepository) {
        super(repository);
        this.repository = repository;
        this.specificationProvider = specificationProvider;
        this.dimReferrerRepository = dimReferrerRepository;
    }

    private Set<String> getCampaignIds(Params params) {
        if (isEmpty(params.getReferrerDomains())) {
            return params.getCampaignIds();
        }

        Set<String> campaignIdsFromParams = params.getCampaignIds();
        Set<String> campaignsForReferrerName = dimReferrerRepository
            .findCampaignsForReferrerName(params.getReferrerDomains());

        if (isEmpty(campaignIdsFromParams)) {
            return campaignsForReferrerName;
        }

        Set<String> campaignIds = new HashSet<>(campaignIdsFromParams);
        campaignIds.retainAll(campaignsForReferrerName);
        return campaignIds;
    }

    public List<CampaignSimple> findAllById(List<String> ids) {
        return repository.findAllByIdIn(ids).stream()
            .map(CampaignSimple::from)
            .collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public SelectQuery findCampaignIdsExpression(Params params) {
        Boolean projectsShouldBeEmpty = params.getIsAssigned() == null ? null : !params.getIsAssigned();
        Set<String> campaignIds = getCampaignIds(params);

        CampaignSelectQuery campaignSelectQuery = new CampaignSelectQuery();
        campaignSelectQuery.getOrCreateUserProjectLinking();

        Condition[] conditions = Stream.of(
                considerConfidentialCondition(params.isConsiderConfidential(),
                    campaignSelectQuery::getOrCreateProjectLinking),
                SqlBuilderUtils.itemCollectionCondition(campaignIds,
                    campaignSelectQuery::getRootTable, ColumnNameConstants.ID),
                SqlBuilderUtils.itemCollectionCondition(params.getDecibelLabelIds(),
                    campaignSelectQuery::getOrCreateDecibelLabelLinking, ColumnNameConstants.ID),
                SqlBuilderUtils.itemCollectionCondition(params.getPlatforms(),
                    campaignSelectQuery::getOrCreateCampaignPlatformLinking, ColumnNameConstants.NAME),
                SqlBuilderUtils.itemCollectionCondition(params.getProjectIds(),
                    campaignSelectQuery::getOrCreateProjectLinking, ColumnNameConstants.ID),
                SqlBuilderUtils.itemCollectionCondition(params.getProviderIds(),
                    campaignSelectQuery::getOrCreateCampaignProviderLinking, ColumnNameConstants.ID),
                SqlBuilderUtils.itemCollectionCondition(params.getSubCategoryIds(),
                    campaignSelectQuery::getOrCreateCampaignSubcategoryLinking, ColumnNameConstants.ID),
                SqlBuilderUtils.booleanItemCondition(params.getIsPending(),
                    campaignSelectQuery::getRootTable, ColumnNameConstants.IS_PENDING),
                userCondition(params.getUserId(), campaignSelectQuery::getOrCreateUserProjectLinking,
                    campaignSelectQuery::getOrCreateUserLabelLinking),
                projectsShouldBeEmptyCondition(projectsShouldBeEmpty,
                    campaignSelectQuery::getOrCreateProjectLinking)
            )
            .filter(Objects::nonNull)
            .toArray(Condition[]::new);

        return campaignSelectQuery.getQuery()
            .addCondition(ComboCondition.and(conditions));
    }

    private Condition considerConfidentialCondition(boolean considerConfidential, Supplier<DbTable> tableSupplier) {
        if (!considerConfidential) {
            return null;
        }
        DbTable table = tableSupplier.get();
        DbColumn isConfidentialColumn = new DbColumn(table, ColumnNameConstants.IS_CONFIDENTIAL, null);
        DbColumn idColumn = new DbColumn(table, ColumnNameConstants.ID, null);
        return ComboCondition.or(
            List.of(
                UnaryCondition.isNull(isConfidentialColumn),
                BinaryCondition.equalTo(isConfidentialColumn, Boolean.FALSE.toString()),
                UnaryCondition.isNull(idColumn)
            ).toArray(Condition[]::new)
        );
    }

    private Condition projectsShouldBeEmptyCondition(Boolean projectsShouldBeEmpty,
        Supplier<DbTable> tableSupplier) {
        if (projectsShouldBeEmpty == null) {
            return null;
        }

        DbColumn projectIdColumn = new DbColumn(tableSupplier.get(), ColumnNameConstants.ID, null);
        return (projectsShouldBeEmpty) ?
            UnaryCondition.isNull(projectIdColumn) :
            UnaryCondition.isNotNull(projectIdColumn);
    }

    private Condition userCondition(Integer userId, Supplier<DbTable> userProjectTableSupplier,
        Supplier<DbTable> userLabelTableSupplier) {
        if (userId == null) {
            return null;
        }
        return ComboCondition.or(
            List.of(
                BinaryCondition.equalTo(new DbColumn(userProjectTableSupplier.get(),
                    ColumnNameConstants.USER_ID, null), userId),
                BinaryCondition.equalTo(new DbColumn(userLabelTableSupplier.get(),
                    ColumnNameConstants.USER_ID, null), userId)
            ).toArray(Condition[]::new));
    }
}
