package ru.yandex.webmaster3.worker.iks;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.collect.Range;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.logbroker.writer.LogbrokerWriter;
import ru.yandex.webmaster3.core.sup.SupIntegrationService;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.RetryUtils;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
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.host.CommonDataState;
import ru.yandex.webmaster3.storage.host.CommonDataType;
import ru.yandex.webmaster3.storage.settings.dao.CommonDataStateYDao;
import ru.yandex.webmaster3.storage.user.UserPersonalInfo;
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.service.UserPersonalInfoService;
import ru.yandex.webmaster3.storage.util.yt.*;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;
import ru.yandex.webmaster3.worker.notifications.auto.SupNotificationTemplateUtil;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;

/**
 * ishalaru
 * 28.09.2020
 **/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Component
public class SendIksPushNotificationPeriodicTask extends PeriodicTask<SendIksPushNotificationPeriodicTask.State> {
    private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^.*/iks_update_notification_(\\d\\d\\d\\d-\\d\\d-\\d\\d)$");
    private static final int BATCH_SIZE = 30;
    private static final int BATCH_SIZE_OLD = 5000;
    private static final String USER_ID = "robot-webmaster";

    @Value("${external.yt.service.arnold.root.default}/iks/notification/")
    private YtPath path;
    private final YtService ytService;
    private final CommonDataStateYDao commonDataStateYDao;
    private final SupIntegrationService supIntegrationService;
    private final UserPersonalInfoService userPersonalInfoService;
    @Value("${webmaster3.core.sup.projectId}")
    private final String projectId;
    @Value("${webmaster3.core.sup.ttl}")
    private Long ttl;

    private final LogbrokerWriter supLbWriter;

    private interface F {
        YtSchema SCHEMA = new YtSchema();
        YtColumn<String> push = SCHEMA.addColumn("push", YtColumn.Type.STRING);
    }

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

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

        YtPath tablePath = findTableToProcess(lastImportedDate);
        if (tablePath == null) {
            log.info("Did not find table to process. Last processed table: {}. Finishing task", lastImportedDate);
            return Result.SUCCESS;
        }

        log.info("Found table to process: {}", tablePath);
        ytService.inTransaction(path).execute(cypressService -> {
            try {
                processTable(tablePath, cypressService);
            } catch (Exception e) {
                log.error("Error sending pushes, WILL NOT RETRY", e);
            }

            var newState = new CommonDataState(
                    CommonDataType.LAST_IKS_PUSH_NOTIFICATION_SEND,
                    getTableDate(tablePath),
                    DateTime.now());
            commonDataStateYDao.update(newState);
            return true;
        });

        return Result.SUCCESS;
    }

    private void processTable(YtPath tablePath, YtCypressService cypressService) {
        var tableReader = new AsyncTableReader<>(cypressService, tablePath, Range.all(), YtTableReadDriver.createYSONDriver(YtRow.class))
                .splitInParts(100000)
                .withThreadName("iks-notification-reader");
        try (var iterator = tableReader.read()) {
            List<YtRow> notificationList = new ArrayList<>();
            iterator.hasNext();

            YtRow row = iterator.next();
            Long lastUid = row.uid;
            YtRow firstRow = row;
            while (iterator.hasNext()) {
                row = iterator.next();
                if (!lastUid.equals(row.uid)) {
                    notificationList.add(firstRow);
                    if (notificationList.size() > BATCH_SIZE_OLD) {
                        send(notificationList);
                        notificationList.clear();
                    }

                    lastUid = row.uid;
                    firstRow = row;

                }
            }

            notificationList.add(firstRow);
            send(notificationList);
        } catch (IOException | InterruptedException e) {
            throw new WebmasterException("YT error",
                    new WebmasterErrorResponse.YTServiceErrorResponse(getClass(), e), e);
        }
    }

    private void send(List<YtRow> notificationList) {
        Map<Long, UserPersonalInfo> personalInfos = getUserPersonalInfoMap(notificationList);
        List<byte[]> pushMessageDataList = new ArrayList<>();
        for (var row : notificationList) {
            var userPersonalInfo = personalInfos.get(row.uid);
            if (userPersonalInfo != null) {
                var pushMessage = getPushMessage(row, userPersonalInfo);
                String msg = JsonMapping.writeValueAsString(pushMessage);
                pushMessageDataList.add(msg.getBytes(StandardCharsets.UTF_8));
            }
        }

        state.totalNotifications += pushMessageDataList.size();
        try {
            RetryUtils.execute(RetryUtils.instantRetry(3), () -> {
                try {
                    supLbWriter.write(pushMessageDataList);
                } catch (Exception e) {
                    log.error("Failed to write push message to LB", e);
                }
            });

            state.sentNotifications += pushMessageDataList.size();
        } catch (Exception exp){
            log.error(exp.getMessage(),exp);
        }
    }

    @NotNull
    private Map<Long, UserPersonalInfo> getUserPersonalInfoMap(List<YtRow> notificationList) {
        List<Long> workList = new ArrayList<>();
        Map<Long, UserPersonalInfo> personalInfos = new HashMap<>();
        for (var row : notificationList) {
            workList.add(row.uid);
            if (workList.size() > BATCH_SIZE) {
                personalInfos.putAll(userPersonalInfoService.getUsersPersonalInfos(workList));
                workList.clear();
            }
        }

        if (!workList.isEmpty()) {
            personalInfos.putAll(userPersonalInfoService.getUsersPersonalInfos(workList));
            workList.clear();
        }

        return personalInfos;
    }

    @NotNull
    private PushMessage getPushMessage(YtRow row, UserPersonalInfo userPersonalInfo) {
        var hostId = IdUtils.stringToHostId(row.hostId);
        var iksMessageContent = new MessageContent.IksNewInfo(List.of(hostId), List.of(0L), hostId,
                new IksMessageContent.IksUpdate(List.of(hostId), List.of(0L)), IksMessageType.UPDATE, null);
        var nC = SupNotificationTemplateUtil.createNotificationContent(hostId, userPersonalInfo, iksMessageContent);
        var pushData = new SupIntegrationService.PushData(nC.getPushId(), nC.getTitle(), nC.getProjectTitle(),
                nC.getShortTitle(), nC.getBody(), nC.getLink(), List.of(row.uid));
        var query = SupIntegrationService.getQuery(pushData, ttl, projectId);

        return new PushMessage(JsonMapping.writeValueAsString(query), row.rowUUID, USER_ID);
    }

    @Nullable
    private YtPath findTableToProcess(LocalDate tableImportedLastTime) {
        var tablePath = YtUtils.getLatestTablePath(ytService, path, TABLE_NAME_PATTERN);
        if (tablePath != null) {
            var tableDate = LocalDate.parse(getTableDate(tablePath), ISODateTimeFormat.yearMonthDay());
            if (tableDate == null) {
                log.info("Failed to parse table {} date", tablePath.getName());
                return null;
            }

            if (!tableDate.isAfter(tableImportedLastTime)) {
                log.info("Table {} is already processed", tablePath.getName());
                tablePath = null;
            }
        }

        return tablePath;
    }

    @Nullable
    private String getTableDate(YtPath tablePath) {
        var matcher = TABLE_NAME_PATTERN.matcher(tablePath.toString());
        if (matcher.matches()) {
            return matcher.group(1);
        };

        return null;
    }

    private static final class YtRow {
        private final Long uid;
        private final Long iks;
        private final String hostId;
        private final String rowUUID;

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

    @Data
    private static class PushMessage {
        public final String push;
        public final String reqid;
        public final String user;
    }

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

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

    public static class State implements PeriodicTaskState {
        public int totalNotifications;
        public int sentNotifications;
    }
}
