package ru.yandex.webmaster3.storage.verification;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;

import com.datastax.driver.core.utils.UUIDs;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Instant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.stereotype.Service;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.blackbox.service.BlackboxUsersService;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.data.WebmasterUser;
import ru.yandex.webmaster3.core.events2.HostEvent;
import ru.yandex.webmaster3.core.events2.HostEventId;
import ru.yandex.webmaster3.core.events2.client.HostEventLogClient;
import ru.yandex.webmaster3.core.events2.events.userhost.UserHostAddedEvent;
import ru.yandex.webmaster3.core.host.service.HostOwnerService;
import ru.yandex.webmaster3.core.host.verification.IUserHostVerifier;
import ru.yandex.webmaster3.core.host.verification.UserHostVerificationInfo;
import ru.yandex.webmaster3.core.host.verification.VerificationCausedBy;
import ru.yandex.webmaster3.core.host.verification.VerificationFailEnum;
import ru.yandex.webmaster3.core.host.verification.VerificationFailInfo;
import ru.yandex.webmaster3.core.host.verification.VerificationStatus;
import ru.yandex.webmaster3.core.host.verification.VerificationType;
import ru.yandex.webmaster3.core.host.verification.VerificationTypeScope;
import ru.yandex.webmaster3.core.host.verification.fail.NotApplicableFailInfo;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.solomon.metric.SolomonCounter;
import ru.yandex.webmaster3.core.solomon.metric.SolomonKey;
import ru.yandex.webmaster3.core.solomon.metric.SolomonMetricConfiguration;
import ru.yandex.webmaster3.core.solomon.metric.SolomonMetricRegistry;
import ru.yandex.webmaster3.core.user.UserVerifiedHost;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
import ru.yandex.webmaster3.core.worker.client.WorkerClient;
import ru.yandex.webmaster3.core.worker.task.VerifyHostTaskData;
import ru.yandex.webmaster3.core.worker.task.WorkerTaskData;
import ru.yandex.webmaster3.core.worker.task.WorkerTaskPriority;
import ru.yandex.webmaster3.storage.delegation.UserDelegationsForSendYDao;
import ru.yandex.webmaster3.storage.events.data.events.UserHostMessageEvent;
import ru.yandex.webmaster3.storage.events.service.WMCEventsService;
import ru.yandex.webmaster3.storage.host.service.SyncVerificationStateService;
import ru.yandex.webmaster3.storage.postpone.PostponeActionYDao;
import ru.yandex.webmaster3.storage.postpone.PostponeOperationType;
import ru.yandex.webmaster3.storage.spam.VerificationsForOwnerYDao;
import ru.yandex.webmaster3.storage.user.dao.UserHostVerificationYDao;
import ru.yandex.webmaster3.storage.user.message.content.MessageContent;
import ru.yandex.webmaster3.storage.user.notification.NotificationType;
import ru.yandex.webmaster3.storage.user.service.UserHostsService;
import ru.yandex.webmaster3.storage.util.ydb.exception.WebmasterYdbException;

import static ru.yandex.webmaster3.core.host.verification.VerificationStatus.VERIFICATION_FAILED;

/**
 * @author avhaliullin
 */
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class HostVerifierService {
    private static final Logger log = LoggerFactory.getLogger(HostVerifierService.class);

    public static final Duration VERIFICATION_TIMEOUT = Duration.standardMinutes(2);

    private static final String SOLOMON_LABEL_VERIFICATION_TYPE = "verification_type";
    private static final String SOLOMON_LABEL_VERIFICATION_STATUS = "verification_status";

    private final UserHostsService userHostsService;
    private final UserHostVerificationYDao userHostVerificationYDao;

    private final WMCEventsService wmcEventsService;
    private final SyncVerificationStateService syncVerificationStateService;
    private final SolomonMetricRegistry solomonMetricRegistry;
    private final VerificationsForOwnerYDao verificationsForOwnerYDao;
    private final HostOwnerService hostOwnerService;
    private final UserDelegationsForSendYDao userDelegationsForSendYDao;
    private final WorkerClient workerClient;
    private final HostEventLogClient hostEventLogClient;
    private final PostponeActionYDao postponeActionYDao;
    @Qualifier("blackboxExternalYandexUsersService")
    private final BlackboxUsersService blackboxUsersService;

    private Map<VerificationType, IUserHostVerifier> hostVerifiersMap;
    private Map<Pair<VerificationType, VerificationStatus>, SolomonCounter> metrics;

    @PostConstruct
    public void init() {
        var solomonMetricConfiguration = new SolomonMetricConfiguration(
                Set.of(
                        Set.of("verification_type", "verification_status"),
                        Set.of("verification_type"),
                        Set.of("verification_status")
                ),
                "host_verification",
                1.0,
                true
        );

        metrics = new HashMap<>();
        for (VerificationType type : VerificationType.values()) {
            for (VerificationStatus status : VerificationStatus.values()) {
                metrics.put(
                        Pair.of(type, status),
                        solomonMetricRegistry.createCounter(
                                solomonMetricConfiguration,
                                SolomonKey.create(SOLOMON_LABEL_VERIFICATION_TYPE, type.name())
                                        .withLabel(SOLOMON_LABEL_VERIFICATION_STATUS, status.name())
                        )
                );
            }
        }
    }

    public boolean initiateVerification(
            @NotNull UserHostVerificationInfo verificationInfo,
            VerificationCausedBy cause,
            WebmasterHostId hostId,
            long userId,
            VerificationType verificationType)  {

        if (!hostVerifiersMap.get(verificationType).beforeVerification(hostId, userId)) {
            return false;
        }

        UUID newRecordId = UUIDs.timeBased();
        UserHostVerificationInfo newInfo = verificationInfo.copyAsVerificationStartRecord(newRecordId, verificationType, cause);
        userHostVerificationYDao.addVerificationRecord(newInfo);
        boolean enqueued = workerClient.checkedEnqueueTask(VerifyHostTaskData.createTaskData(
                hostId,
                userId,
                newRecordId,
                verificationType,
                WorkerTaskPriority.HIGHEST
        ));

        if (!enqueued) {
            userHostVerificationYDao.addVerificationRecord(newInfo.copyWithInternalError(UUIDs.timeBased()));
            throw new WebmasterException("Failed to enqueue verification task",
                    new WebmasterErrorResponse.WorkerErrorResponse(getClass(), null));
        }

        //noinspection ConstantConditions
        return enqueued;
    }

    public void verifyHost(long userId, WebmasterHostId hostId, UUID verificationRecordId) {
        VerificationType finallyVerificationType = VerificationType.UNKNOWN;
        VerificationStatus finallyVerificationStatus = VerificationStatus.INTERNAL_ERROR;
        try {

            WebmasterUser user = new WebmasterUser(userId);
            List<UserHostVerificationInfo> records = userHostVerificationYDao.getRecordsAfter(userId, hostId, verificationRecordId);
            UserHostVerificationInfo verificationInfo = null;
            for (UserHostVerificationInfo info : records) {
                if (info.getRecordId().equals(verificationRecordId)) {
                    verificationInfo = info;
                    break;
                }
            }
            if (verificationInfo == null) {
                finallyVerificationStatus = VerificationStatus.INTERNAL_ERROR;
                finallyVerificationType = VerificationType.UNKNOWN;
                var message = "Verification record " + verificationRecordId + " not found for user " + userId + " host " + hostId;
                log.error(message);
                throw new RuntimeException(message);
            }
            finallyVerificationType = verificationInfo.getVerificationType();
            if (records.size() > 1) {
                finallyVerificationStatus = VerificationStatus.INTERNAL_ERROR;
                log.warn("Some records were written atop of " + verificationRecordId + " for user " + userId + " host " + hostId + ", verification is cancelled");
                return;
            }
            if (verificationInfo.getVerificationStatus() == null ||
                    verificationInfo.getVerificationStatus() == VerificationStatus.INTERNAL_ERROR ||
                    verificationInfo.getVerificationStatus() == VerificationStatus.VERIFIED) {
                finallyVerificationStatus = VerificationStatus.INTERNAL_ERROR;
                var message = "Verification record " + verificationRecordId + " for user " + userId + " host " + hostId + " is not in processable state";
                log.error(message);
                throw new RuntimeException(message);
            }

            VerificationType verificationType = verificationInfo.getVerificationType();
            IUserHostVerifier verifier = hostVerifiersMap.get(verificationType);

            UUID resultRecordId = UUIDs.timeBased();
            UserHostVerificationInfo resultRecord;
            try {
                log.info("Started verification of host {} for user {} with type {}",
                        hostId, userId, verificationType);

                Optional<VerificationFailInfo> failOpt = verifier.verify(userId, hostId,
                        verificationInfo.getRecordId(), verificationInfo.getVerificationUin(), verificationInfo.getVerificationCausedBy());
                if (failOpt.isPresent()) {
                    VerificationFailInfo failInfo = failOpt.get();
                    Optional<Duration> retryAfterOpt = verificationType.shouldRetry(verificationInfo.getFailedAttempts() + 1);
                    if (retryAfterOpt.isPresent()) {
                        Duration retryAfter = retryAfterOpt.get();
                        resultRecord = verificationInfo.copyWithIntermediateFail(resultRecordId, failInfo, retryAfter);
                    } else {
                        resultRecord = verificationInfo.copyWithTotallyFail(resultRecordId, failInfo);
                        if (resultRecord.getVerificationType() == VerificationType.SELF) {
                            resultRecord = resultRecord.withAddedToList(false);
                        }
                    }
                } else {
                    resultRecord = verificationInfo.copyAsVerified(resultRecordId);
                }
            } catch (Exception e) {
                finallyVerificationStatus = VerificationStatus.INTERNAL_ERROR;
                log.error("Verification failed with unknown error for host " + hostId + " user " + userId, e);
                resultRecord = verificationInfo.copyWithInternalError(resultRecordId);
            }

            if (blackboxUsersService.getUserById(userId) == null) {
                finallyVerificationStatus = VERIFICATION_FAILED;
                resultRecord = verificationInfo.copyWithTotallyFail(resultRecordId, new NotApplicableFailInfo());

                log.info("deleted userId - {}", userId);
            }

            // вызов addVerificationRecord унесен внутрь switch'а, потому что требуется разный порядок записи в БД для разных кейсов
            switch (resultRecord.getVerificationStatus()) {
                case VERIFIED:
                    insertIntoVerifiedHosts(resultRecord);
                    // если мы сломаемся тут, то хост останется подтвержденным, хоть и без подтверждающей записи -
                    // это лучше, чем неподтвержденный хост, с надписью "права подтверждены" на странице работы с правами
                    userHostVerificationYDao.addVerificationRecord(resultRecord);
                    WebmasterHostId owner = hostOwnerService.getHostOwner(hostId);
                    verificationsForOwnerYDao.addVerification(owner, resultRecordId);
                    finallyVerificationStatus = VerificationStatus.VERIFIED;

                    break;

                case IN_PROGRESS:
                    // подтвердить не удалось, но подтверждение "долгое" - нужно повторить через определенное время
                    userHostVerificationYDao.addVerificationRecord(resultRecord);
                    //Если ждать меньше минуты, может помучать очередь, а если ждать больше то лучше положить в очередь отложенных событий.
                    final WorkerTaskData taskData = VerifyHostTaskData.createTaskData(
                            hostId,
                            userId,
                            resultRecordId,
                            verificationType,
                            WorkerTaskPriority.HIGH,
                            verificationInfo.getNextAttempt().toDateTime()
                    );
                    if (verificationInfo.getNextAttempt().toDateTime().isBefore(DateTime.now().plusMinutes(1))) {
                        workerClient.enqueueTask(taskData);
                    } else {
                        postponeActionYDao.insert(verificationInfo.getNextAttempt().toDateTime(), PostponeOperationType.VERIFY_HOST, JsonMapping.writeValueAsString(taskData));
                    }
                    finallyVerificationStatus = VerificationStatus.IN_PROGRESS;
                    break;

                case VERIFICATION_FAILED:
                    UserVerifiedHost userVerifiedHost = userHostsService.getVerifiedHost(user, hostId);
                    userHostVerificationYDao.addVerificationRecord(resultRecord);
                    // если сломаемся тут, то хост тоже останется подтвержденным с записью о провале проверки
                    // это снова лучше, чем "расподтверждение" хоста без записи о причинах
                    if (userVerifiedHost != null && userVerifiedHost.getVerificationType() == verificationType) {
                        log.info("Unverifying host {} for user {}", hostId, userId);
                        userHostsService.deleteVerifiedHost(user, hostId);

                        // не отправляем, если пользователь сам нажал кнопку проверить
                        if (verificationInfo.getVerificationCausedBy() == VerificationCausedBy.INITIAL_VERIFICATION) {
                            break;
                        }

                        // отправляем только если права "слетели". Если и до этого был fail, то не отправляем
                        if (verificationInfo.getVerificationFailInfo() != null) {
                            break;
                        }

                        if (resultRecord.getVerificationFailInfo().getType() == VerificationFailEnum.DELEGATION_CANCELLED) {
                            //если мы еще не отослали уведомление о том, что пользователю делегировали права
                            //то не будем уведомлять ни об одном из событий
                            if (userDelegationsForSendYDao.contains(userId, hostId)) {
                                userDelegationsForSendYDao.delete(userId, hostId);
                                break;
                            }
                        }

                        // отправляем сообщение только в случае "расподтверждения" хоста
                        wmcEventsService.addEvent(
                                new UserHostMessageEvent<>(
                                        hostId,
                                        userId,
                                        new MessageContent.HostAccessLost(hostId),
                                        NotificationType.ACCESS_LOST,
                                        false)
                        );
                    } else {
                        syncVerificationStateService.syncVerificationState(user.getUserId(), hostId);
                    }
                    finallyVerificationStatus = VERIFICATION_FAILED;
                    break;
                default:
                    userHostVerificationYDao.addVerificationRecord(resultRecord);
                    syncVerificationStateService.syncVerificationState(user.getUserId(), hostId);
                    finallyVerificationStatus = resultRecord.getVerificationStatus();
            }
        } catch (WebmasterYdbException e) {
            finallyVerificationStatus = VERIFICATION_FAILED;
            throw new WebmasterException("Host verification failed",
                    new WebmasterErrorResponse.YDBErrorResponse(getClass(), e), e);
        } finally {
            metrics.get(Pair.of(finallyVerificationType, finallyVerificationStatus)).update();
        }
    }

    public Optional<InheritedVerificationInfo> inheritVerificationInfo(long userId, WebmasterHostId hostId) {
        var inheritableHostnamePredicate = VerificationInheritanceUtil.inheritableHostnamePredicate(hostId);
        List<UserVerifiedHost> verifiedHosts = userHostsService.getVerifiedHosts(new WebmasterUser(userId));
        return verifiedHosts
                .stream()
                .filter(info -> info.getVerificationType().isCanBeInherited())
                .filter(inheritableHostnamePredicate)
                .min(VerificationInheritanceUtil.inheritableHostsComparator(hostId))
                .map(info -> new InheritedVerificationInfo(
                        info.getWebmasterHostId(),
                        info.getVerificationUin(),
                        info.getVerificationType()
                ));
    }

    public boolean isApplicable(WebmasterHostId hostId, long userId, VerificationType verificationType) {
        return hostVerifiersMap.get(verificationType).isApplicable(hostId, userId);
    }

    public Set<VerificationType> listApplicable(WebmasterHostId hostId, long userId, VerificationTypeScope scope) {
        return Arrays.stream(VerificationType.values())
                .filter(vt -> {
                    if (!vt.isExplicit() || !vt.visibleIn(scope)) {
                        return false;
                    }
                    IUserHostVerifier verifier = hostVerifiersMap.get(vt);
                    try {
                        return verifier.isApplicable(hostId, userId);
                    } catch (Exception e) {
                        log.error("Failed to check applicability of " + vt + " for " + hostId + " " + userId, e);
                        return false;
                    }
                })
                .collect(Collectors.toCollection(() -> EnumSet.noneOf(VerificationType.class)));
    }

    private void insertIntoVerifiedHosts(UserHostVerificationInfo verificationInfo) {
        Duration reverifyPeriod = verificationInfo.getVerificationType().getReverifyPeriod();
        userHostsService.addVerifiedHost(
                new WebmasterUser(verificationInfo.getUserId()),
                new UserVerifiedHost(
                        verificationInfo.getHostId(),
                        verificationInfo.getLastAttempt().toDateTime(),
                        verificationInfo.getLastAttempt().plus(reverifyPeriod).toDateTime(),
                        verificationInfo.getVerificationUin(),
                        verificationInfo.getVerificationType()
                )
        );
    }

    public static class InheritedVerificationInfo {
        public final WebmasterHostId fromHostId;
        public final long verificationUin;
        public final VerificationType verificationType;

        public InheritedVerificationInfo(WebmasterHostId fromHostId, long verificationUin, VerificationType verificationType) {
            this.fromHostId = fromHostId;
            this.verificationUin = verificationUin;
            this.verificationType = verificationType;
        }
    }

    public boolean tryAutoVerify(UserHostVerificationInfo verificationInfo) {
        try {
            for (VerificationType verificationType : VerificationType.AUTO_VERIFIERS) {
                IUserHostVerifier verifier = hostVerifiersMap.get(verificationType);
                Optional<VerificationFailInfo> failOpt;
                try {
                    failOpt = verifier.verify(
                            verificationInfo.getUserId(),
                            verificationInfo.getHostId(),
                            verificationInfo.getRecordId(),
                            verificationInfo.getVerificationUin(),
                            verificationInfo.getVerificationCausedBy()
                    );
                } catch (Exception e) {
                    log.error("Auto verification with type " + verificationType + " failed", e);
                    continue;
                }
                if (!failOpt.isPresent()) {
                    UUID recordId = UUIDs.timeBased();

                    UserHostVerificationInfo newVerificationInfo = verificationInfo
                            .copyAsVerificationStartRecord(recordId, verificationType, VerificationCausedBy.INITIAL_VERIFICATION)
                            .copyAsVerified(recordId);
                    insertIntoVerifiedHosts(newVerificationInfo);
                    userHostVerificationYDao.addVerificationRecord(newVerificationInfo);
                    return true;
                }
            }
            return false;
        } catch (WebmasterYdbException e) {
            throw new WebmasterException("Auto verification failed",
                    new WebmasterErrorResponse.YDBErrorResponse(getClass(), e), e);
        }
    }

    public boolean addHostUinInitRecord(WebmasterUser user, WebmasterHostId hostId, UserHostVerificationInfo verificationInfo, HostEventId eventId) {
        UserHostVerificationInfo newVerificationInfo = getUinInitRecord(user, hostId, verificationInfo);
        log.info("Host uin init record: {}", newVerificationInfo);
        boolean autoVerified = false;
        try {
            autoVerified = tryAutoVerify(newVerificationInfo);
        } catch (Exception e) {
            log.error("Auto verification failed", e);
        }

        if (!autoVerified) {
            userHostVerificationYDao.addVerificationRecord(newVerificationInfo);
        }

        HostEvent event = HostEvent.create(eventId, hostId, new UserHostAddedEvent(eventId.getUserId()));
        hostEventLogClient.log(event);

        return autoVerified;
    }

    private UserHostVerificationInfo getUinInitRecord(WebmasterUser user, WebmasterHostId hostId, @Nullable Long verificationUinOpt) {
        UserHostVerificationInfo newVerificationInfo;
        log.info("getUinInitRecord: {}, {}", user.getUserId(), hostId);
        if (verificationUinOpt != null) {
            log.info("Already got uin: {}", verificationUinOpt);
            // uin сайта уже зафиксирован, нужно просто "вывести его из тени" флагом isAddedToList
            newVerificationInfo = UserHostVerificationInfo.createUinInitRecord(
                    UUIDs.timeBased(),
                    user.getUserId(),
                    hostId,
                    verificationUinOpt,
                    null,
                    true,
                    VerificationCausedBy.INITIAL_VERIFICATION
            );
        } else {
            long verificationUin;

            Optional<InheritedVerificationInfo> inheritedInfoOpt = inheritVerificationInfo(user.getUserId(), hostId);
            VerificationType verificationType = null;
            if (inheritedInfoOpt.isPresent()) {
                InheritedVerificationInfo inheritedInfo = inheritedInfoOpt.get();
                verificationUin = inheritedInfo.verificationUin;
                log.info("Inherited verification for user {} host {} from host {}, uin {}", user.getUserId(), hostId, inheritedInfo.fromHostId, verificationUin);
                verificationType = inheritedInfo.verificationType;
            } else {
                verificationUin = ThreadLocalRandom.current().nextLong();
                log.info("No verifications to inherit, generated new uin {}", verificationUin);
            }

            newVerificationInfo = UserHostVerificationInfo.createUinInitRecord(
                    UUIDs.timeBased(),
                    user.getUserId(),
                    hostId,
                    verificationUin,
                    verificationType,
                    true,
                    VerificationCausedBy.INITIAL_VERIFICATION
            );
        }

        return newVerificationInfo;
    }

    private UserHostVerificationInfo getUinInitRecord(WebmasterUser user, WebmasterHostId hostId,
                                                      @Nullable UserHostVerificationInfo verificationInfo) {
        return getUinInitRecord(user, hostId, verificationInfo == null ? null : verificationInfo.getVerificationUin());
    }


    public static boolean isVerificationInProgress(UserHostVerificationInfo verificationInfo) {
        if (verificationInfo != null && verificationInfo.getVerificationStatus() == VerificationStatus.IN_PROGRESS) {
            Instant shouldFinishBefore = verificationInfo.getNextAttempt()
                    .plus(HostVerifierService.VERIFICATION_TIMEOUT);
            return shouldFinishBefore.isAfterNow();
        }

        return false;
    }

    public static boolean isVerificationExpired(UserHostVerificationInfo verificationInfo) {
        if (verificationInfo != null && verificationInfo.getVerificationStatus() == VerificationStatus.IN_PROGRESS) {
            Instant shouldFinishBefore = verificationInfo.getNextAttempt()
                    .plus(HostVerifierService.VERIFICATION_TIMEOUT);
            return shouldFinishBefore.isBeforeNow();
        }

        return false;
    }

    // TODO: Почему то не цепляется как надо через Autowired, приезжает пустая мапа (нe null), надо разбираться
    @Required
    public void setHostVerifiersMap(Map<VerificationType, IUserHostVerifier> hostVerifiersMap) {
        this.hostVerifiersMap = hostVerifiersMap;
    }
}
