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

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;

import com.univocity.parsers.csv.CsvWriter;
import com.univocity.parsers.csv.CsvWriterSettings;
import one.util.streamex.EntryStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.yandex.direct.common.TranslationService;
import ru.yandex.direct.core.entity.campaign.service.CampaignService;
import ru.yandex.direct.core.entity.metrika.service.MetrikaGoalsService;
import ru.yandex.direct.core.entity.retargeting.model.Goal;
import ru.yandex.direct.core.entity.retargeting.model.GoalBase;
import ru.yandex.direct.core.entity.retargeting.service.common.GoalUtilsService;
import ru.yandex.direct.dbutil.model.ClientId;
import ru.yandex.direct.metrika.client.MetrikaClientException;
import ru.yandex.direct.utils.InterruptedRuntimeException;
import ru.yandex.direct.ytcomponents.statistics.model.ConversionsStatisticsResponse;
import ru.yandex.direct.ytcore.entity.statistics.service.ConversionStatisticsService;

import static java.nio.charset.StandardCharsets.UTF_8;
import static ru.yandex.direct.utils.CommonUtils.nvl;
import static ru.yandex.direct.utils.FunctionalUtils.filterList;
import static ru.yandex.direct.utils.FunctionalUtils.listToMap;
import static ru.yandex.direct.utils.FunctionalUtils.mapList;

@Service
@ParametersAreNonnullByDefault
public class ConversionStatService {
    private static final char BOM_HEADER = '\uFEFF';

    private final CampaignService campaignService;
    private final ConversionStatisticsService conversionStatisticsService;
    private final MetrikaGoalsService metrikaGoalsService;
    private final TranslationService translationService;

    @Autowired
    public ConversionStatService(
            CampaignService campaignService,
            ConversionStatisticsService conversionStatisticsService,
            MetrikaGoalsService metrikaGoalsService, TranslationService translationService) {
        this.campaignService = campaignService;
        this.conversionStatisticsService = conversionStatisticsService;
        this.metrikaGoalsService = metrikaGoalsService;
        this.translationService = translationService;
    }

    /**
     * Метод пишет CSV отчёт в указанный {@link OutputStream} и возвращает
     * булевское значение, означающее "удалось ли записать результат".
     * <p>
     * Пока что существует только одна не исключительная причина,
     * чтобы не удалось записать результат — кампания не найдена.
     */
    public boolean writeReport(OutputStream outputStream,
                               Long operatorUid, ClientId clientId,
                               @Nullable Long campaignId, @Nullable Locale locale) throws IOException {

        if (campaignId != null && isCampaignIdUnavailable(clientId, campaignId)) {
            return false;
        }

        if (campaignId != null) {
            writeCampaignsConversionStatisticsAsCsv(operatorUid, clientId, List.of(campaignId), locale, outputStream);
        } else {
            writeClientCampaignsConversionStatisticsAsCsv(outputStream, operatorUid, clientId, locale);
        }
        return true;
    }

    private boolean isCampaignIdUnavailable(ClientId clientId, Long campaignId) {
        return campaignService.getExistingNonSubCampaignIds(clientId, List.of(campaignId))
                .isEmpty();
    }

    private void writeClientCampaignsConversionStatisticsAsCsv(
            OutputStream outputStream, Long operatorUid,
            ClientId clientId, @Nullable Locale locale) throws IOException {
        var clientCampaignIds = campaignService.getClientNonSubCampaignIds(clientId);
        writeCampaignsConversionStatisticsAsCsv(operatorUid, clientId, clientCampaignIds, locale, outputStream);
    }

    private void writeCampaignsConversionStatisticsAsCsv(
            Long operatorUid,
            ClientId clientId,
            Collection<Long> campaignIds,
            @Nullable Locale locale,
            OutputStream outputStream) throws IOException {
        var availableGoals = getAvailableGoalsByIds(operatorUid, clientId);

        // Отфильтровываем конверсии по недоступным целям
        var stats = filterList(getCampaignConversionStatistics(campaignIds),
                response -> availableGoals.containsKey(response.getGoalId()));

        var goalNamesByIds = EntryStream.of(availableGoals)
                .mapValues(GoalUtilsService::changeEcommerceGoalName)
                .mapValues(GoalBase::getName)
                .toMap();
        var rows = statResponsesToCsvRows(stats, goalNamesByIds);

        var outputWriter = new OutputStreamWriter(outputStream, UTF_8);
        outputWriter.write(BOM_HEADER); // Без BOM-заголовка будут проблемы при открытии файла в excel
        var writer = new CsvWriter(outputWriter, new CsvWriterSettings());
        writer.writeHeaders(new ConversionStatCsvHeaders(translationService, locale).getAllHeaders());
        writer.writeRowsAndClose(rows.toArray(new String[0][]));
    }

    private List<ConversionsStatisticsResponse> getCampaignConversionStatistics(Collection<Long> campaignIds) {
        var rawStats = conversionStatisticsService.getConversionStatistics(campaignIds);
        return EntryStream.of(rawStats)
                .flatMapValues(Collection::stream)
                .sortedBy(Map.Entry::getKey)
                .values()
                .toList();
    }

    private Map<Long, Goal> getAvailableGoalsByIds(Long operatorUid, ClientId clientId) {
        List<Goal> availableGoals;
        try {
            availableGoals = metrikaGoalsService.getGoalsWithCounters(operatorUid, clientId, null);
        } catch (MetrikaClientException | InterruptedRuntimeException e) {
            throw new IllegalStateException("Metrika is unavailable");
        }
        return listToMap(availableGoals, GoalBase::getId);
    }

    private List<String[]> statResponsesToCsvRows(
            List<ConversionsStatisticsResponse> responses,
            Map<Long, String> goalNamesByIds) {
        return mapList(responses, response -> {
            var goalName = nvl(goalNamesByIds.get(response.getGoalId()), "");
            return new String[]{
                    response.getCampaignId().toString(),
                    response.getUpdateTime(),
                    response.getGoalId().toString(),
                    goalName,
                    response.getAttributionType().toString(),
                    response.getCostTaxFree().toString(),
                    response.getGoalsNum().toString()
            };
        });
    }
}
