package ru.yandex.webmaster3.worker.user;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.mutable.MutableLong;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import ru.yandex.webmaster3.core.blackbox.service.BlackboxUsersService;
import ru.yandex.webmaster3.core.util.RetryUtils;
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.user.UserPersonalInfo;
import ru.yandex.webmaster3.storage.user.dao.UserNotificationEmailYDao;
import ru.yandex.webmaster3.storage.user.notification.UserSetEmailInfo;
import ru.yandex.webmaster3.storage.user.service.UserPersonalInfoService;
import ru.yandex.webmaster3.worker.PeriodicTask;
import ru.yandex.webmaster3.worker.TaskSchedule;

/**
 * @author avhaliullin
 */
@Component
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class EmailsVerificationStateUpdateTask extends PeriodicTask<PeriodicTaskState> {
    private static final Logger log = LoggerFactory.getLogger(EmailsVerificationStateUpdateTask.class);

    private static final int TOO_MANY_CHAGES_THRESHOLD = 60_000;
    private static final int BATCH_SIZE = 450;

    private static final Pattern YANDEX_EMAIL_REGEXP = Pattern.compile("^(?:.*\\+)?(.*)@(yandex|ya)\\." +
            "(ru|com|by|kz)$");

    private final UserNotificationEmailYDao userNotificationEmailYDao;
    private final UserPersonalInfoService userPersonalInfoService;
    private final BlackboxUsersService blackboxExternalYandexUsersService;

    @Override
    public Result run(UUID runId) throws Exception {
        List<EmailState> toUpdateState = new ArrayList<>();
        MutableLong goingToVerifyCount = new MutableLong();
        MutableLong goingToUnverifyCount = new MutableLong();
        List<Pair<Long, UserSetEmailInfo>> batch = new ArrayList<>();

        userNotificationEmailYDao.forEach(
                p -> {
                    if (p.getRight().getEmail() != null) {
                        batch.add(Pair.of(p.getLeft(), p.getRight()));
                    }
                    if (batch.size() >= BATCH_SIZE) {
                        processBatch(toUpdateState, goingToVerifyCount, goingToUnverifyCount, batch);
                    }
                });
        if (!batch.isEmpty()) {
            processBatch(toUpdateState, goingToVerifyCount, goingToUnverifyCount, batch);
        }
        log.info("Found {} emails to verify and {} to unverify", goingToVerifyCount.longValue(),
                goingToUnverifyCount.longValue());
        if (toUpdateState.size() > TOO_MANY_CHAGES_THRESHOLD) {
            throw new RuntimeException("Will not update emails verifications states - too many changes found " +
                    "(" + toUpdateState.size() + " > " + TOO_MANY_CHAGES_THRESHOLD + ")");
        }
        for (EmailState state : toUpdateState) {
            log.info("Updating state {} {} {}", state.userId, state.email, state.verified);
            userNotificationEmailYDao.updateVerifiedState(state.userId, state.email, state.verified);
        }
        return new Result(TaskResult.SUCCESS);
    }

    private void processBatch(List<EmailState> toUpdateState, MutableLong goingToVerifyCount,
                              MutableLong goingToUnverifyCount, List<Pair<Long, UserSetEmailInfo>> batch) {
        List<Triple<Long, UserSetEmailInfo, Boolean>> batchWithNewVerification = isVerified(batch);
        batchWithNewVerification.forEach(triple-> {
            long userId = triple.getLeft();
            UserSetEmailInfo emailInfo = triple.getMiddle();
            boolean newVerified = triple.getRight();
            if (emailInfo.isVerified() != newVerified) {
                log.info("State changed for user {} email {} from {} to {}", userId, emailInfo.getEmail(), emailInfo.isVerified(), newVerified);
                toUpdateState.add(new EmailState(userId, emailInfo.getEmail(), newVerified));
                if (newVerified) {
                    goingToVerifyCount.increment();
                } else {
                    goingToUnverifyCount.increment();
                }
            }
        });
        batch.clear();
    }

    private List<Triple<Long, UserSetEmailInfo, Boolean>> isVerified(List<Pair<Long, UserSetEmailInfo>> userIdEmailBatch) {
        try {
            return RetryUtils.query(RetryUtils.linearBackoff(5, Duration.standardSeconds(30)), () -> {
                Map<Long, UserPersonalInfo> personalInfos = userPersonalInfoService.getUsersPersonalInfos(
                        userIdEmailBatch.stream().map(Pair::getLeft).collect(Collectors.toList())
                );
                return userIdEmailBatch.stream().map(p -> {
                    Matcher m = YANDEX_EMAIL_REGEXP.matcher(p.getRight().getEmail());
                    if (m.find()) {
                        if (!personalInfos.containsKey(p.getLeft()) || personalInfos.get(p.getLeft()) == null) {
                            log.info("User {} not found in passport", p.getLeft());
                            return Triple.of(p.getLeft(), p.getRight(), false);
                        }
                        if (personalInfos.get(p.getLeft()).getLogin().equals(m.group(1))) {
                            return Triple.of(p.getLeft(), p.getRight(), true);
                        }
                    }
                    return Triple.of(p.getLeft(), p.getRight(),
                            blackboxExternalYandexUsersService.isEmailValid(p.getLeft(), p.getRight().getEmail()));
                }).collect(Collectors.toList());
            });
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }

    private static class EmailState {
        private final long userId;
        private final String email;
        private final boolean verified;

        public EmailState(long userId, String email, boolean verified) {
            this.userId = userId;
            this.email = email;
            this.verified = verified;
        }
    }

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

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