package ru.yandex.direct.web.entity.deal.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.collect.ImmutableMap;
import one.util.streamex.StreamEx;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.yandex.direct.core.entity.campaign.model.CampaignSimple;
import ru.yandex.direct.core.entity.campaign.repository.CampaignRepository;
import ru.yandex.direct.core.entity.deal.container.UpdateDealContainer;
import ru.yandex.direct.core.entity.deal.model.CompleteReason;
import ru.yandex.direct.core.entity.deal.model.Deal;
import ru.yandex.direct.core.entity.deal.repository.DealRepository;
import ru.yandex.direct.core.entity.deal.service.DealService;
import ru.yandex.direct.core.entity.placements.model.Placement;
import ru.yandex.direct.core.entity.user.model.User;
import ru.yandex.direct.core.entity.user.service.UserService;
import ru.yandex.direct.core.security.DirectAuthentication;
import ru.yandex.direct.dbutil.SqlUtils;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.dbutil.sharding.ShardHelper;
import ru.yandex.direct.operation.Applicability;
import ru.yandex.direct.result.MassResult;
import ru.yandex.direct.result.Result;
import ru.yandex.direct.validation.result.Defect;
import ru.yandex.direct.validation.result.ValidationResult;
import ru.yandex.direct.web.core.model.WebResponse;
import ru.yandex.direct.web.core.security.DirectWebAuthenticationSource;
import ru.yandex.direct.web.entity.campaign.model.CampaignForWebDealDetails;
import ru.yandex.direct.web.entity.deal.model.DealsChangeStatusResponse;
import ru.yandex.direct.web.entity.deal.model.GetDealsDetailsResponse;
import ru.yandex.direct.web.entity.deal.model.GetDealsListResponse;
import ru.yandex.direct.web.entity.deal.model.UpdateDeal;
import ru.yandex.direct.web.entity.deal.model.UpdateDealResponse;
import ru.yandex.direct.web.entity.deal.model.WebDeal;
import ru.yandex.direct.web.entity.deal.model.WebDealConverter;
import ru.yandex.direct.web.entity.deal.model.WebDealDetails;
import ru.yandex.direct.web.validation.kernel.ValidationResultConversionService;

import static java.util.Collections.emptyList;
import static ru.yandex.direct.utils.FunctionalUtils.mapList;
import static ru.yandex.direct.web.entity.deal.model.WebDealConverter.addStatsToWebDeals;
import static ru.yandex.direct.web.entity.deal.model.WebDealConverter.toUpdateDealContainer;
import static ru.yandex.direct.web.entity.deal.model.WebDealConverter.toWebDeal;
import static ru.yandex.direct.web.entity.deal.model.WebDealDetailsCampaign.toWebDealDetailsCampaign;

@Service
@ParametersAreNonnullByDefault
public class DealWebService {

    private final DealService dealService;
    private final DirectWebAuthenticationSource directWebAuthenticationSource;
    private final ValidationResultConversionService validationResultConversionService;
    private final DealWebValidationService dealWebValidationService;
    private final DealRepository dealRepository;
    private final ShardHelper shardHelper;
    private final CampaignRepository campaignRepository;
    private final UserService userService;

    private static final Map<String, Comparator<WebDeal>> FIELDS_COMPARATORS =
            ImmutableMap.copyOf(getFieldsComparators());

    /**
     * Соответствие полям {@link WebDeal} и {@link Comparator<WebDeal>}. Поля те же, что отдаются фронту в сделке
     */
    private static Map<String, Comparator<WebDeal>> getFieldsComparators() {
        Map<String, Comparator<WebDeal>> result = new HashMap<>();
        result.put(WebDeal.DEAL_TYPE, defaultComparator(WebDeal::getDealType));
        result.put(WebDeal.NAME, defaultComparator(t -> StringUtils.lowerCase(t.getName())));
        result.put(WebDeal.ID, defaultComparator(WebDeal::getId));
        result.put(WebDeal.STATUS, statusComparator());
        result.put(WebDeal.SPENT, defaultComparator(WebDeal::getSpent));
        result.put(WebDeal.SHOWS, defaultComparator(WebDeal::getShows));
        result.put(WebDeal.MIN_PRICE, defaultComparator(WebDeal::getMinPrice));
        result.put(WebDeal.CPM, defaultComparator(WebDeal::getCpm));
        result.put(WebDeal.CLICKS, defaultComparator(WebDeal::getClicks));
        result.put(WebDeal.CPC, defaultComparator(WebDeal::getCpc));
        result.put(WebDeal.CTR, defaultComparator(WebDeal::getCtr));
        return result;
    }

    @Autowired
    public DealWebService(DealService dealService,
                          DirectWebAuthenticationSource directWebAuthenticationSource,
                          ValidationResultConversionService validationResultConversionService,
                          DealWebValidationService dealWebValidationService,
                          DealRepository dealRepository, ShardHelper shardHelper,
                          CampaignRepository campaignRepository, UserService userService) {
        this.dealService = dealService;
        this.directWebAuthenticationSource = directWebAuthenticationSource;
        this.validationResultConversionService = validationResultConversionService;
        this.dealWebValidationService = dealWebValidationService;
        this.dealRepository = dealRepository;
        this.shardHelper = shardHelper;
        this.campaignRepository = campaignRepository;
        this.userService = userService;
    }

    public GetDealsDetailsResponse getDealsDetails(List<Long> dealIds) {
        ClientId clientId = directWebAuthenticationSource.getAuthentication().getSubjectUser().getClientId();
        List<Deal> deals = dealService.getDeals(clientId, dealIds);
        Map<Long, Placement> placementsMap = dealService.getPlacementsMapByDeals(deals);

        Map<Long, List<CampaignForWebDealDetails>> linkedCampaigns = getCampaignsForWebDealDetails(dealIds);

        return new GetDealsDetailsResponse()
                .withResult(mapList(deals, deal -> new WebDealDetails().withDeal(toWebDeal(deal, placementsMap))
                        .withCampaigns(
                                toWebDealDetailsCampaign(linkedCampaigns.getOrDefault(deal.getId(), emptyList())))));
    }

    public GetDealsListResponse getDealsList(String sortBy, String sortOrder, boolean noStat) {

        ClientId clientId = directWebAuthenticationSource.getAuthentication().getSubjectUser().getClientId();
        List<Deal> deals = dealService.getDealsBrief(clientId);
        List<Long> dealsIds = mapList(deals, Deal::getId);

        List<WebDeal> webDeals = StreamEx.of(deals)
                .map(WebDealConverter::toWebDeal)
                .toList();

        if (!noStat && !dealsIds.isEmpty()) {
            addStatsToWebDeals(webDeals, dealService.getDealStatistics(dealsIds));
        }

        Map<Long, List<Long>> linkedCampaignsByDealIds = dealService.getLinkedCampaignsByDealIds(dealsIds);
        webDeals.forEach(deal -> deal.withNumberOfLinkedCampaigns(
                linkedCampaignsByDealIds.getOrDefault(deal.getId(), Collections.emptyList()).size()));

        sortWebDeals(webDeals, sortBy, sortOrder);

        return new GetDealsListResponse().withResult(webDeals);
    }

    /**
     * Сортировка сделок по заданным параметрам
     */
    private void sortWebDeals(List<WebDeal> webDeals, String sortBy, String sortOrder) {
        if (!FIELDS_COMPARATORS.containsKey(sortBy)) {
            //Дефолтная сортировка. Фронтенд не может передавать дефолтную сортировку, поэтому задаем ее сами.
            sortBy = WebDeal.ID;
            sortOrder = SqlUtils.SortOrder.DESC.name().toLowerCase();
        }
        Comparator<WebDeal> comparator = FIELDS_COMPARATORS.get(sortBy);
        Comparator<WebDeal> idComparator = defaultComparator(WebDeal::getId);

        if (StringUtils.equals(SqlUtils.SortOrder.DESC.name().toLowerCase(), sortOrder)) {
            comparator = comparator.reversed();
            idComparator = idComparator.reversed();
        }
        comparator = comparator.thenComparing(idComparator);
        webDeals.sort(comparator);
    }

    private static Comparator<WebDeal> statusComparator() {
        Comparator<WebDeal> comparator = defaultComparator(WebDeal::getStatus);
        return comparator.thenComparing(defaultComparator(WebDeal::getCompleteReason));
    }

    private static <T, U extends Comparable<? super U>> Comparator<T> defaultComparator(
            Function<? super T, ? extends U> keyExtractor) {
        return (o1, o2) -> ObjectUtils.compare(keyExtractor.apply(o1), keyExtractor.apply(o2));
    }

    /**
     * Может вызываться только для агентства в качестве subjectUser
     */
    public WebResponse updateDeal(UpdateDeal request) {
        User agency = directWebAuthenticationSource.getAuthentication().getSubjectUser();
        long agencyUid = agency.getUid();
        ClientId agencyId = agency.getClientId();
        ValidationResult<UpdateDeal, Defect> validation =
                dealWebValidationService.validateLinkCampaigns(request);
        if (validation.hasAnyErrors()) {
            return validationResultConversionService.buildValidationResponse(validation);
        }
        Result<UpdateDealContainer> result = dealService.updateDeal(agencyUid, agencyId,
                toUpdateDealContainer(request));
        if (!result.isSuccessful()) {
            return validationResultConversionService.buildValidationResponse(result);
        }
        return validationResultConversionService
                .buildMassResponse(result.getValidationResult(), WebDealConverter.toUpdateDeal(result.getResult()),
                        UpdateDealResponse::new);
    }

    public WebResponse activateDeals(List<Long> dealIds) {
        DirectAuthentication authentication = directWebAuthenticationSource.getAuthentication();
        MassResult<Long> result = dealService.activateDeals(authentication.getSubjectUser().getClientId(),
                dealIds, Applicability.FULL);

        if (result.getErrorCount() > 0) {
            return validationResultConversionService.buildValidationResponse(result);
        }
        return new DealsChangeStatusResponse().withResult(mapList(result.getResult(), Result::getResult));
    }

    public WebResponse archiveDeals(List<Long> dealIds) {
        ClientId clientId = directWebAuthenticationSource.getAuthentication().getSubjectUser().getClientId();
        MassResult<Long> result = dealService.archiveDeals(clientId, dealIds, Applicability.FULL);
        if (result.getErrorCount() > 0) {
            return validationResultConversionService.buildValidationResponse(result);
        }
        return new DealsChangeStatusResponse().withResult(mapList(result.getResult(), Result::getResult));
    }

    public WebResponse completeDeals(List<Long> dealIds) {
        ClientId clientId = directWebAuthenticationSource.getAuthentication().getSubjectUser().getClientId();
        MassResult<Long> result =
                dealService.completeDeals(clientId, dealIds, CompleteReason.BY_CLIENT, Applicability.FULL);
        if (result.getErrorCount() > 0) {
            return validationResultConversionService.buildValidationResponse(result);
        }
        return new DealsChangeStatusResponse().withResult(mapList(result.getResult(), Result::getResult));
    }

    /**
     * по списку сделок возвращает информацию о кампаниях, привязанных к сделкам
     */
    public Map<Long, List<CampaignForWebDealDetails>> getCampaignsForWebDealDetails(List<Long> dealIds) {
        Map<Long, List<CampaignForWebDealDetails>> result = new HashMap<>();

        shardHelper.forEachShard(shard -> {
            Map<Long, List<Long>> linkedCampaignsInShard = dealRepository.getLinkedCampaigns(shard, dealIds);
            Set<Long> campaignIds = StreamEx.ofValues(linkedCampaignsInShard).flatMap(Collection::stream).toSet();

            Map<Long, CampaignSimple> campaignsById = campaignRepository.getCampaignsSimple(shard, campaignIds);
            Set<ClientId> clientIds = StreamEx.ofValues(campaignsById).map(CampaignSimple::getClientId).map(
                    ClientId::fromLong).toSet();
            Map<ClientId, String> loginsByClientIds = userService.getChiefsLoginsByClientIds(clientIds);

            for (Long dealId : linkedCampaignsInShard.keySet()) {
                Set<CampaignForWebDealDetails> campaignsByDealId = linkedCampaignsInShard.get(dealId).stream()
                        .distinct()
                        .map(campaignsById::get)
                        .filter(Objects::nonNull)
                        .map(campaign -> new CampaignForWebDealDetails()
                                .withCampaignId(campaign.getId())
                                .withCampaignName(campaign.getName())
                                .withClientId(campaign.getClientId())
                                .withUserName(loginsByClientIds.get(ClientId.fromLong(campaign.getClientId()))))
                        .collect(Collectors.toSet());
                result.computeIfAbsent(dealId, id -> new ArrayList<>()).addAll(campaignsByDealId);
            }
        });
        return result;
    }
}
