package ru.yandex.webmaster3.worker.iks;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.Range;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.format.ISODateTimeFormat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskState;
import ru.yandex.webmaster3.core.worker.task.PeriodicTaskType;
import ru.yandex.webmaster3.storage.abt.AbtService;
import ru.yandex.webmaster3.storage.abt.model.Experiment;
import ru.yandex.webmaster3.storage.abt.model.ExperimentInfo;
import ru.yandex.webmaster3.storage.events.data.WMCEvent;
import ru.yandex.webmaster3.storage.events.data.events.UserHostMessageEvent;
import ru.yandex.webmaster3.storage.events.service.WMCEventsService;
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.user.message.content.MessageContent;
import ru.yandex.webmaster3.storage.user.message.iks.IksMessageContent;
import ru.yandex.webmaster3.storage.user.message.iks.IksMessageType;
import ru.yandex.webmaster3.storage.user.notification.NotificationType;
import ru.yandex.webmaster3.storage.util.yt.AsyncTableReader;
import ru.yandex.webmaster3.storage.util.yt.YtCypressService;
import ru.yandex.webmaster3.storage.util.yt.YtPath;
import ru.yandex.webmaster3.storage.util.yt.YtService;
import ru.yandex.webmaster3.storage.util.yt.YtTableReadDriver;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;

/**
 * ishalaru
 * 17.08.2020
 **/
@Slf4j
@Component
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class UpdateIksStatusPeriodicTask extends PeriodicTask<UpdateIksStatusPeriodicTask.State> {
    public static final int BATCH_SIZE = 50;
    public static final int ADDITIONAL_HOST_INFO_COUNT = 5;
    @Value("${external.yt.service.arnold.root.default}/iks/notification/")
    private YtPath path;
    private final YtService ytService;
    private final CommonDataStateYDao commonDataStateYDao;
    private final WMCEventsService wmcEventsService;
    private final AbtService abtService;


    @Override
    public Result run(UUID runId) throws Exception {
        setState(new State());

        final LocalDate lastImportedDate = Optional.ofNullable(commonDataStateYDao.getValue(CommonDataType.LAST_IKS_VALUE_UPDATE)).
                map(state -> LocalDate.parse(state.getValue()))
                .orElse(LocalDate.parse("2020-09-28"));


        ytService.inTransaction(path).execute(cypressService -> {
            List<YtPath> tableForProcess = findTableToProcess(lastImportedDate, cypressService);
            if (tableForProcess.isEmpty()) {
                log.info("Did not find table to process. Last processed table: {}. Finishing task",
                        lastImportedDate);
                return false;
            }
            tableForProcess.sort(Comparator.comparing(YtPath::toString));
            final YtPath tablePath = tableForProcess.get(tableForProcess.size() - 1);
            processTable(cypressService, tablePath);
            commonDataStateYDao.update(new CommonDataState(CommonDataType.LAST_IKS_VALUE_UPDATE, tablePath.getName().substring(tablePath.getName().length() - 10), DateTime.now()));
            return true;
        });
        return Result.SUCCESS;
    }


    private List<YtPath> findTableToProcess(LocalDate tableImportedLastTime,
                                            YtCypressService cypressService) {
        return cypressService.list(path)
                .stream()
                .map(YtPath::getName)
                .filter(e -> e.indexOf("iks_update_notification_") >= 0)
                .filter(n -> LocalDate.parse(n.substring("iks_update_notification_".length()), ISODateTimeFormat.yearMonthDay()).isAfter(tableImportedLastTime))
                .sorted(Comparator.naturalOrder())
                .map(name -> YtPath.path(path, name))
                .collect(Collectors.toList());
    }

    private void processTable(YtCypressService cypressService, YtPath tablePath) {
        AsyncTableReader<YtRow> tableReader =
                new AsyncTableReader<>(cypressService, tablePath, Range.all(),
                        YtTableReadDriver
                                .createYSONDriver(YtRow.class))
                        .splitInParts(100000)
                        .withThreadName("iks-diff-reader");

        try (AsyncTableReader.TableIterator<YtRow> iterator = tableReader.read()) {
            List<NotificationInfo> hosts = new ArrayList<>();
            iterator.hasNext();
            YtRow row = iterator.next();
            NotificationInfo notificationInfo =
                    new NotificationInfo(row.uuid);
            notificationInfo.add(IdUtils.stringToHostId(row.hostId), row.iks);
            while (iterator.hasNext()) {
                row = iterator.next();
                if (!notificationInfo.uuid.equals(row.uuid)) {
                    hosts.add(notificationInfo);
                    if (hosts.size() > BATCH_SIZE) {
                        send(hosts);
                        hosts.clear();
                    }
                    notificationInfo = new NotificationInfo(row.uuid);

                }
                notificationInfo.add(IdUtils.stringToHostId(row.hostId), row.iks);
                state.countUpdate++;
            }
            hosts.add(notificationInfo);
            if (!hosts.isEmpty()) {
                send(hosts);
            }
        } catch (IOException | InterruptedException e) {
            throw new WebmasterException("YT error",
                    new WebmasterErrorResponse.YTServiceErrorResponse(getClass(), e), e);
        }

    }

    public void send(List<NotificationInfo> hosts) {
        final List<WMCEvent> collect = hosts.stream().map(this::create).filter(e -> e != null).collect(Collectors.toList());
        //log.info("Ready to send: {}", collect);
        wmcEventsService.addEvents(collect);
    }

    public WMCEvent create(NotificationInfo notificationInfo) {
        try {
            String groupName = "TYPE_3";
            if (notificationInfo.hosts.size() == 1) {
                groupName = abtService.getHostExperiments(notificationInfo.hosts.get(0)).get(Experiment.IKS_UPDATE_EMAIL.getName());

            }
            if (groupName.equals("TYPE_3")) {
                state.countType3++;
            } else if (groupName.equals("TYPE_2")) {
                state.countType2++;
            } else if (groupName.equals("TYPE_1")) {
                state.countType1++;
            } else {
                state.countWithoutGroup++;
            }
            return WMCEvent.create(
                    new UserHostMessageEvent<>(
                            notificationInfo.hosts.get(0),
                            Long.valueOf(notificationInfo.uuid),
                            new MessageContent.IksNewInfo(notificationInfo.hosts,notificationInfo.iksValues,notificationInfo.hosts.get(0),
                                    new IksMessageContent.IksUpdate(notificationInfo.hosts, notificationInfo.iksValues),
                                    IksMessageType.UPDATE,
                                    groupName),
                            NotificationType.IKS_UPDATE,
                            false, new ExperimentInfo(Experiment.IKS_UPDATE_EMAIL.getName(), groupName))
            );
        } catch (Exception exp) {
            log.error(exp.getMessage(), exp);
            state.countInvalid++;
            return null;
        }
    }


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

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

    public static final class YtRow {
        private final Long uuid;
        private final Long iks;
        private final String hostId;

        @JsonCreator
        public YtRow(@JsonProperty("user_id") Long uuid,
                     @JsonProperty("iks") Long iks,
                     @JsonProperty("host_id") String hostId) {
            this.uuid = uuid;
            this.iks = iks;
            this.hostId = hostId;
        }
    }


    public static final class NotificationInfo {
        private final Long uuid;
        private final List<WebmasterHostId> hosts;
        private final List<Long> iksValues;

        public NotificationInfo(Long uuid) {
            this.uuid = uuid;
            hosts = new ArrayList<>(ADDITIONAL_HOST_INFO_COUNT);
            iksValues = new ArrayList<>(ADDITIONAL_HOST_INFO_COUNT);
        }

        public void add(WebmasterHostId hostId, Long iks) {
            if (hosts.size() < ADDITIONAL_HOST_INFO_COUNT) {
                this.hosts.add(hostId);
                this.iksValues.add(iks);
            }
        }
    }

    public static class State implements PeriodicTaskState {
        @Getter
        int countUpdate;
        @Getter
        int countType3;
        @Getter
        int countType2;
        @Getter
        int countType1;
        @Getter
        int countWithoutGroup;
        @Getter
        int countInvalid;
    }

}
