package ru.yandex.webmaster3.storage.user.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.data.WebmasterUser;
import ru.yandex.webmaster3.core.host.verification.UserHostVerificationInfo;
import ru.yandex.webmaster3.core.host.verification.VerificationType;
import ru.yandex.webmaster3.core.user.UserVerifiedHost;
import ru.yandex.webmaster3.core.worker.client.WorkerClient;
import ru.yandex.webmaster3.core.worker.task.UserVerifiesHostTaskData;
import ru.yandex.webmaster3.storage.events.data.events.UserVerifiesHostEvent;
import ru.yandex.webmaster3.storage.events.service.WMCEventsService;
import ru.yandex.webmaster3.storage.host.service.HostService;
import ru.yandex.webmaster3.storage.host.service.SyncVerificationStateService;
import ru.yandex.webmaster3.storage.spam.ISpamHostFilter;
import ru.yandex.webmaster3.storage.spam.SpamHostsYDao;
import ru.yandex.webmaster3.storage.user.UserTakeoutDataProvider;
import ru.yandex.webmaster3.storage.user.UserTakeoutTableData;
import ru.yandex.webmaster3.storage.user.UserUnverifiedHost;
import ru.yandex.webmaster3.storage.user.dao.UserHostVerificationYDao;
import ru.yandex.webmaster3.storage.user.dao.UserPinnedHostsYDao;
import ru.yandex.webmaster3.storage.user.dao.VerifiedHostsYDao;
import ru.yandex.webmaster3.storage.verification.auto.AutoVerificationYDao;

/**
 * @author aherman
 */
@Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
public class UserHostsService implements UserTakeoutDataProvider {
    private static final Logger log = LoggerFactory.getLogger(UserHostsService.class);

    private static final String USER_TAKEOUT_VERIFIED_HOSTS_LABEL = "verifiedHosts";
    private static final String USER_TAKEOUT_UNVERIFIED_HOSTS_LABEL = "unverifiedHosts";
    private static final String USER_TAKEOUT_PINNED_HOSTS_LABEL = "pinnedHosts";

    private final HostService hostService;
    private final ISpamHostFilter fastSpamHostFilter;
    private final SpamHostsYDao spamHostsYDao;
    private final SyncVerificationStateService syncVerificationStateService;
    private final UserHostVerificationYDao userHostVerificationYDao;
    private final UserPinnedHostsYDao userPinnedHostsYDao;
    private final VerifiedHostsYDao verifiedHostsYDao;
    private final AutoVerificationYDao autoVerificationYDao;
    private final WMCEventsService wmcEventsService;
    private final WorkerClient workerClient;

    private void checkForSpam(WebmasterUser user, WebmasterHostId hostId) {
        List<UserVerifiedHost> hosts = getVerifiedHosts(user)
                .stream()
                .filter(h -> h.getVerificationType().isDisplayable())
                .collect(Collectors.toList());
        if (fastSpamHostFilter.checkHost(hostId)) {
            // Если новый хост спамный - то нужно пометить старые
            markAsSpam(hosts, user.getUserId());
        } else {
            boolean haveSpamHosts = hosts.stream().anyMatch(h -> fastSpamHostFilter.checkHost(h.getWebmasterHostId()));
            if (haveSpamHosts) {
                // Если среди старых есть спам - то нужно пометить вновь добавленный
                log.info("Marking host {} just verified by user {} as spam", hostId, user.getUserId());
                spamHostsYDao.insertHost(hostId);
            }
        }
    }

    private void markAsSpam(List<UserVerifiedHost> hosts, long userId) {
        for (UserVerifiedHost host : hosts) {
            log.info("Marking host {} owner by user {} as spam", host.getWebmasterHostId(), userId);
            spamHostsYDao.insertHost(host.getWebmasterHostId());
        }
    }

    public void addVerifiedHost(WebmasterUser user, UserVerifiedHost userVerifiedHost) {
        boolean isNewHost = !hostService.isHostAdded(userVerifiedHost.getWebmasterHostId());
        if (isNewHost) {
            hostService.addHost(userVerifiedHost.getWebmasterHostId());
        }
        verifiedHostsYDao.addHost(user, userVerifiedHost);
        syncVerificationStateService.syncVerificationState(user.getUserId(), userVerifiedHost.getWebmasterHostId());
        if (!workerClient.checkedEnqueueTask(new UserVerifiesHostTaskData(user.getUserId(), userVerifiedHost.getWebmasterHostId(), isNewHost))) {
            throw new WebmasterException("Failed to add user verified host", null);
        }
        wmcEventsService.addEvent(new UserVerifiesHostEvent(user.getUserId(), userVerifiedHost.getWebmasterHostId(), isNewHost));

        if (userVerifiedHost.getVerificationType().isDisplayable()) {
            checkForSpam(user, userVerifiedHost.getWebmasterHostId());
        }
    }

    public void deleteVerifiedHost(WebmasterUser user, WebmasterHostId hostId) {
        verifiedHostsYDao.deleteHost(user, hostId);
        syncVerificationStateService.syncVerificationState(user.getUserId(), hostId);
    }

    public UserVerifiedHost getVerifiedHost(WebmasterUser user, WebmasterHostId webmasterHostId) {
        UserVerifiedHost result = verifiedHostsYDao.getUserVerifiedHost(user, webmasterHostId);
        if (result != null && result.getVerificationType() == VerificationType.UNKNOWN && result.getVerificationUin() == 0L) {
            result = new UserVerifiedHost(
                    result.getWebmasterHostId(),
                    result.getVerificationDate(),
                    result.getVerifiedUntilDate(),
                    ThreadLocalRandom.current().nextLong(),
                    result.getVerificationType()
            );
            verifiedHostsYDao.addHost(user, result);
        }
        return result;
    }

    public List<WebmasterHostId> getPinnedHosts(WebmasterUser user) {
        return userPinnedHostsYDao.listPinnedHosts(user);
    }

    public boolean isHostPinned(WebmasterUser user, WebmasterHostId hostId) {
        return userPinnedHostsYDao.isHostPinned(user, hostId);
    }

    public void changePinState(WebmasterUser user, WebmasterHostId hostId, boolean pinState) {
        if (pinState) {
            userPinnedHostsYDao.savePinnedHost(user, hostId);
        } else {
            userPinnedHostsYDao.deletePinnedHost(user, hostId);
        }
    }

    public List<UserVerifiedHost> getVerifiedHosts(WebmasterUser user, Collection<WebmasterHostId> hostIds) {
        return verifiedHostsYDao.getUserVerifiedHosts(user, hostIds);
    }

    public List<UserVerifiedHost> getVerifiedHosts(WebmasterUser user) {
        return verifiedHostsYDao.listHosts(user);
    }

    public List<UserVerifiedHost> getVerifiedHostsQuorumOne(WebmasterUser user) {
        return verifiedHostsYDao.listHosts(user);
    }

    public List<UserHostVerificationInfo> getAllVerificationRecords(long userId, WebmasterHostId hostId) {
        return userHostVerificationYDao.listUserHostRecords(userId, hostId);
    }

    public UserHostVerificationInfo getVerificationInfo(long userId, WebmasterHostId hostId) {
        return userHostVerificationYDao.getLatestRecord(userId, hostId);
    }

    /**
     * Возвращает список потенциально не верифицированных хостов. Для точного списка
     * его нужно проредить через список верифицированных хостов.
     */
    public List<UserUnverifiedHost> getMayBeUnverifiedHosts(WebmasterUser user) {
        Map<WebmasterHostId, UserUnverifiedHost> latestRecordsMap = new HashMap<>();
        MutableObject<UserHostVerificationInfo> lastRecord = new MutableObject<>();
        userHostVerificationYDao.listRecordsForUser(user.getUserId(), record -> {
            if (lastRecord.getValue() != null && !record.getHostId().equals(lastRecord.getValue().getHostId())) {
                if (lastRecord.getValue().isAddedToList()) {
                    latestRecordsMap.put(lastRecord.getValue().getHostId(), makeUserUnverifiedHost(lastRecord.getValue()));
                }
            }
            lastRecord.setValue(record);
        });
        if (lastRecord.getValue() != null) {
            if (lastRecord.getValue().isAddedToList()) {
                latestRecordsMap.put(lastRecord.getValue().getHostId(), makeUserUnverifiedHost(lastRecord.getValue()));
            }
        }
        return new ArrayList<>(latestRecordsMap.values());
    }

    public void forEachMayBeUnverifiedHosts(Consumer<UserHostVerificationInfo> consumer) {
        MutableObject<UserHostVerificationInfo> lastVerificationInfo = new MutableObject<>();

        userHostVerificationYDao.forEach(record -> {
            UserHostVerificationInfo prevRecord = lastVerificationInfo.getValue();
            if (prevRecord == null || record.getUserId() != prevRecord.getUserId() || !record.getHostId().equals(prevRecord.getHostId())) {
                if (prevRecord != null && prevRecord.isAddedToList()) {
                    consumer.accept(prevRecord);
                }
            }
            lastVerificationInfo.setValue(record);
        });
        // do not miss last value
        if (lastVerificationInfo.getValue() != null && lastVerificationInfo.getValue().isAddedToList()) {
            consumer.accept(lastVerificationInfo.getValue());
        }
    }

    public void forEachUserUnverifiedHosts(Consumer<UserHostVerificationInfo> consumer) {
        Map<Pair<Long, WebmasterHostId>, UserHostVerificationInfo> batch = new HashMap<>();
        forEachMayBeUnverifiedHosts(info -> {
            batch.put(Pair.of(info.getUserId(), info.getHostId()), info);
            if (batch.size() >= 1000) {
                List<Pair<Long, WebmasterHostId>> verifiedHosts = verifiedHostsYDao.filterUserVerifiedHosts(batch.keySet());
                verifiedHosts.forEach(batch.keySet()::remove);
                batch.values().forEach(consumer);
                batch.clear();
            }
        });
        if (!batch.isEmpty()) {
            List<Pair<Long, WebmasterHostId>> verifiedHosts = verifiedHostsYDao.filterUserVerifiedHosts(batch.keySet());
            verifiedHosts.forEach(batch.keySet()::remove);
            batch.values().forEach(consumer);
        }
    }

    public UserUnverifiedHost getUnverifiedHost(WebmasterUser user, WebmasterHostId hostId) {
        UserHostVerificationInfo verificationInfo = userHostVerificationYDao.getLatestRecord(user.getUserId(), hostId);
        if (verificationInfo == null || !verificationInfo.isAddedToList()) {
            return null;
        } else {
            return makeUserUnverifiedHost(verificationInfo);
        }
    }

    public Map<Long, UserVerifiedHost> listUsersVerifiedHost(WebmasterHostId hostId) {
        return verifiedHostsYDao.listUsersVerifiedHostWithInfos(hostId);
    }

    public boolean isAnyHostVerified(long userId, List<WebmasterHostId> hostIds) {
        return verifiedHostsYDao.contains(userId, hostIds);
    }

    public boolean isHostVerified(WebmasterHostId hostId) {
        return verifiedHostsYDao.contains(hostId);
    }

    public List<WebmasterHostId> filterUserVerifiedHosts(long userId, Collection<WebmasterHostId> hostIds) {
        return verifiedHostsYDao.filterUserVerifiedHosts(userId, hostIds);
    }

    public void forEach(Consumer<Pair<Long, UserVerifiedHost>> consumer) {
        verifiedHostsYDao.forEach(consumer);
    }

    public void forEachUserHostPair(Consumer<Pair<Long, WebmasterHostId>> consumer) {
        verifiedHostsYDao.forEach(pair -> consumer.accept(Pair.of(pair.getLeft(), pair.getRight().getWebmasterHostId())));
    }


    public int getAllHostsCount(WebmasterUser user) {
        Set<WebmasterHostId> allHosts = new HashSet<>();
        getMayBeUnverifiedHosts(user)
                .forEach(host -> allHosts.add(host.getWebmasterHostId()));
        getVerifiedHosts(user)
                .forEach(host -> allHosts.add(host.getWebmasterHostId()));

        return allHosts.size();
    }

    @Override
    @NotNull
    public List<UserTakeoutTableData> getUserTakeoutData(WebmasterUser user) {
        List<UserTakeoutTableData> takeoutData = new ArrayList<>();

        List<UserVerifiedHost> verifiedHosts = getVerifiedHosts(user);
        Set<WebmasterHostId> verifiedHostIds = verifiedHosts.stream().map(UserVerifiedHost::getWebmasterHostId).collect(Collectors.toSet());
        takeoutData.add(new UserTakeoutTableData(USER_TAKEOUT_VERIFIED_HOSTS_LABEL, verifiedHosts));

        List<UserUnverifiedHost> unverifiedHosts = getMayBeUnverifiedHosts(user).stream()
                .filter(h -> !verifiedHostIds.contains(h.getWebmasterHostId()))
                .collect(Collectors.toList());
        takeoutData.add(new UserTakeoutTableData(USER_TAKEOUT_UNVERIFIED_HOSTS_LABEL, unverifiedHosts));

        List<WebmasterHostId> pinnedHosts = userPinnedHostsYDao.listPinnedHosts(user);
        takeoutData.add(new UserTakeoutTableData(USER_TAKEOUT_PINNED_HOSTS_LABEL, pinnedHosts));

        return takeoutData;
    }

    @Override
    public void deleteUserData(WebmasterUser user) {
        verifiedHostsYDao.deleteForUser(user);
        userPinnedHostsYDao.deleteForUser(user);
        userHostVerificationYDao.deleteForUser(user);
        autoVerificationYDao.deleteForUser(user);
    }

    @Override
    public @NotNull List<String> getTakeoutTables() {
        return List.of(
                verifiedHostsYDao.getTablePath(),
                userPinnedHostsYDao.getTablePath(),
                userHostVerificationYDao.getTablePath(),
                autoVerificationYDao.getTablePath()
        );
    }

    private UserUnverifiedHost makeUserUnverifiedHost(UserHostVerificationInfo verificationInfo) {
        return new UserUnverifiedHost(
                verificationInfo.getHostId(),
                verificationInfo.getVerificationUin(),
                verificationInfo.getRecordId()
        );
    }
}
