package ru.yandex.webmaster3.worker.checklist;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import com.google.common.collect.Range;
import lombok.Getter;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.commons.lang3.tuple.Pair;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import ru.yandex.webmaster3.core.util.enums.IntEnum;
import ru.yandex.webmaster3.core.util.enums.IntEnumResolver;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemContent;
import ru.yandex.webmaster3.core.checklist.data.SiteProblemTypeEnum;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.metrika.counters.MetrikaCountersUtil;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.TimeUtils;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskState;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskType;
import ru.yandex.webmaster3.core.worker.task.TaskResult;
import ru.yandex.webmaster3.storage.checklist.data.ProblemSignal;
import ru.yandex.webmaster3.storage.checklist.service.SiteProblemsService;
import ru.yandex.webmaster3.storage.host.CommonDataState;
import ru.yandex.webmaster3.storage.host.CommonDataType;
import ru.yandex.webmaster3.storage.settings.dao.CommonDataStateYDao;
import ru.yandex.webmaster3.storage.util.yt.AsyncTableReader;
import ru.yandex.webmaster3.storage.util.yt.YtMissingValueMode;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtRowMapper;
import ru.yandex.webmaster3.storage.util.yt.YtService;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;

/**
 * @author avhaliullin
 */
@Service("importMetrikaStatusTask")
public class ImportMetrikaStatusTask extends PeriodicTask<ImportMetrikaStatusTask.State> {
    private static final Logger log = LoggerFactory.getLogger(ImportMetrikaStatusTask.class);
    private static final int BATCH_SIZE = 2000;
    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^has_metrika.(\\d{4}-\\d{2}-\\d{2})$");

    private final YtService ytService;
    private final YtPath metrikaDirPath;
    private final SiteProblemsService siteProblemsService;
    private final CommonDataStateYDao commonDataStateYDao;


    @Autowired
    public ImportMetrikaStatusTask(
            YtService ytService,
            @Value("${webmaster3.storage.metrika.status.dir}") YtPath metrikaDirPath,
            SiteProblemsService siteProblemsService,
            CommonDataStateYDao commonDataStateYDao) {
        this.ytService = ytService;
        this.metrikaDirPath = metrikaDirPath;
        this.siteProblemsService = siteProblemsService;
        this.commonDataStateYDao = commonDataStateYDao;
    }

    @Override
    public Result run(UUID runId) throws Exception {
        setState(new State());
        LocalDate prevUpdate = Optional.ofNullable(commonDataStateYDao.getValue(CommonDataType.LAST_METRIKA_STATUS_UPDATE))
                .map(state -> LocalDate.parse(state.getValue()))
                .orElse(new LocalDate(0));
        MutableObject<DateTime> dataUpdateDate = new MutableObject<>(null);
        ytService.withoutTransaction(cypressService -> {
            Optional<Pair<YtPath, LocalDate>> tableOpt = cypressService.list(metrikaDirPath).stream()
                    .flatMap(node -> {
                        Matcher m = TABLE_NAME_PATTERN.matcher(node.getName());
                        if (m.find()) {
                            LocalDate date = LocalDate.parse(m.group(1));
                            return Stream.of(Pair.of(node, date));
                        } else {
                            return Stream.empty();
                        }
                    })
                    .max(Comparator.comparing(Pair::getRight));
            YtPath tablePath;
            LocalDate tableDate;
            if (tableOpt.isEmpty()) {
                throw new RuntimeException("No metrika source tables found");
            } else {
                tablePath = tableOpt.get().getLeft();
                tableDate = tableOpt.get().getRight();
                log.info("Latest table is " + tablePath);
            }
            if (!tableDate.isAfter(prevUpdate)) {
                log.info("Table already loaded");
                return true;
            }
            DateTime metrikaDataUpdate = tableDate.toDateTimeAtStartOfDay(TimeUtils.EUROPE_MOSCOW_ZONE);

            dataUpdateDate.setValue(metrikaDataUpdate);

            var tableReader = new AsyncTableReader<>(cypressService, tablePath,
                    Range.all(), new Mapper(), YtMissingValueMode.SKIP_ROW)
                    .splitInParts(100_000L)
                    .withRetry(5);
            Map<WebmasterHostId, ProblemSignal> map = new HashMap<>(BATCH_SIZE);
            try (var it = tableReader.read()) {
                while (it.hasNext()) {
                    Optional<MetrikaSignal> signalOpt = it.next();
                    if (signalOpt.isEmpty()) {
                        continue;
                    }

                    MetrikaSignal signal = signalOpt.get();
                    if (!signal.toShow) {
                        continue;
                    }

                    WebmasterHostId hostId;
                    try {
                        hostId = IdUtils.urlToHostId(signal.host);
                    } catch (IllegalArgumentException | WebmasterException e) {
                        log.error("Failed to parse host " + signal.host, e);
                        continue;
                    }

                    ProblemSignal noMetrikaProblem;
                    switch (signal.status) {
                        case COUNTER_OK:
                            noMetrikaProblem = ProblemSignal.createAbsent(SiteProblemTypeEnum.NO_METRIKA_COUNTER, metrikaDataUpdate);
                            break;
                        case NO_COUNTER:
                            noMetrikaProblem = ProblemSignal.createPresent(new SiteProblemContent.NoMetrikaCounter(), metrikaDataUpdate);
                            break;
                        default:
                            String message = "Unknown metrika status " + signal.status + " for host " + signal.host;
                            log.error(message);
                            throw new RuntimeException(message);
                    }

                    WebmasterHostId domainHostId = IdUtils.urlToHostId(MetrikaCountersUtil.hostToPunycodeDomain(hostId));
                    map.put(domainHostId, noMetrikaProblem);
                    if (map.size() >= BATCH_SIZE) {
                        state.countUpdated+= map.size();
                        siteProblemsService.updateCleanableProblems(map, SiteProblemTypeEnum.NO_METRIKA_COUNTER);
                        map.clear();
                    }
                }
                siteProblemsService.updateCleanableProblems(map, SiteProblemTypeEnum.NO_METRIKA_COUNTER);
                state.countUpdated+= map.size();
            } catch (IOException e) {
                log.error("Failed to read metrika statuses table", e);
                throw new RuntimeException("Failed to read metrika statuses table", e);
            }

            return true;
        });

        if (dataUpdateDate.getValue() != null) {
            commonDataStateYDao.update(new CommonDataState(
                    CommonDataType.LAST_METRIKA_STATUS_UPDATE,
                    dataUpdateDate.getValue().toLocalDate().toString(),
                    DateTime.now()
            ));
            siteProblemsService.notifyCleanableProblemUpdateFinished(SiteProblemTypeEnum.NO_METRIKA_COUNTER, dataUpdateDate.getValue());
        }

        return new Result(TaskResult.SUCCESS);
    }

    static class MetrikaSignal {
        final String host;
        final MetrikaStatus status;
        final boolean toShow;

        MetrikaSignal(String host, MetrikaStatus status, boolean toShow) {
            this.host = host;
            this.status = status;
            this.toShow = toShow;
        }
    }

    enum MetrikaStatus implements IntEnum {
        COUNTER_OK(1),
        NO_COUNTER(0),
        ;

        private final int value;

        MetrikaStatus(int value) {
            this.value = value;
        }


        @Override
        public int value() {
            return value;
        }

        static final IntEnumResolver<MetrikaStatus> R = IntEnumResolver.r(MetrikaStatus.class);
    }

    static class Mapper implements YtRowMapper<Optional<MetrikaSignal>> {
        static final String FIELD_HOST = "domain";
        static final String FIELD_STATUS = "has_metrika_aggr";
        static final String FIELD_TO_SHOW = "to_show";

        String host;
        MetrikaStatus status;
        boolean toShow = false;

        @Override
        public void nextField(String name, InputStream data) {
            try {
                switch (name) {
                    case FIELD_HOST:
                        host = IOUtils.toString(data, StandardCharsets.UTF_8);
                        break;
                    case FIELD_STATUS:
                        // float, потому что у метрики есть какая-то тактика, и они ее придерживаются
                        status = MetrikaStatus.R.fromValueOrNull((int) Float.parseFloat(IOUtils.toString(data, StandardCharsets.UTF_8)));
                        break;
                    case FIELD_TO_SHOW:
                        toShow = Integer.parseInt(IOUtils.toString(data, StandardCharsets.UTF_8)) > 0;
                        break;
                    default:
                }
            } catch (Exception e) {
                log.error("Failed to parse yt field ", e);
            }
        }

        @Override
        public Optional<MetrikaSignal> rowEnd() {
            if (host == null || status == null) {
                return Optional.empty();
            }
            return Optional.of(new MetrikaSignal(host, status, toShow));
        }

        @Override
        public List<String> getColumns() {
            return Arrays.asList(FIELD_HOST, FIELD_STATUS, FIELD_TO_SHOW);
        }
    }

    @Override
    public PeriodicTaskType getType() {
        return PeriodicTaskType.IMPORT_METRIKA_STATUS;
    }

    @Override
    public TaskSchedule getSchedule() {
        return TaskSchedule.startByCron("0 30 3 * * *");
    }


    public class State implements PeriodicTaskState {
        @Getter
        int countUpdated;
    }
}
