package ru.yandex.wmconsole.service;

import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import ru.yandex.common.util.collections.Cf;
import ru.yandex.common.util.collections.CollectionUtils;
import ru.yandex.common.util.collections.Cu;
import ru.yandex.common.util.functional.Function;
import ru.yandex.webmaster.common.host.dao.TblHostsMainDao;
import ru.yandex.wmconsole.data.MainMirrorStateEnum;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.mirror.MainMirrorHistoryInfo;
import ru.yandex.wmconsole.data.mirror.MirrorGroupActionEnum;
import ru.yandex.wmconsole.data.mirror.MirrorGroupChangeRequest;
import ru.yandex.wmconsole.data.mirror.MirrorGroupChangeStateEnum;
import ru.yandex.wmconsole.data.partition.WMCPartition;
import ru.yandex.wmconsole.error.WMCExtraTagNameEnum;
import ru.yandex.wmconsole.service.dao.TblMainMirrorHistoryDao;
import ru.yandex.wmconsole.service.dao.TblMainMirrorRequestsDao;
import ru.yandex.wmconsole.service.error.WMCUserProblem;
import ru.yandex.wmconsole.util.WwwUtil;
import ru.yandex.wmtools.common.SupportedProtocols;
import ru.yandex.wmtools.common.error.*;
import ru.yandex.wmtools.common.service.AbstractLockableDbService;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Service for processing main mirror requests.
 *
 * User: azakharov
 * Date: 23.10.13
 * Time: 16:52
 */
public class MirrorGroupsChangeService extends AbstractLockableDbService {

    private static final Logger log = LoggerFactory.getLogger(MirrorGroupsChangeService.class);

    private static final String MIRROR_REQUEST_LOCK_NAME = "mirror_requests_lock";

    private TblMainMirrorRequestsDao tblMainMirrorRequestsDao;
    private TblMainMirrorHistoryDao tblMainMirrorHistoryDao;
    private TblHostsMainDao tblHostsMainDao;
    private UsersHostsService usersHostsService;

    /**
     * Validate request
     *
     * @param       hostInfo
     * @param       action
     * @param       desiredHostName
     * @param       userId
     * @return      current main mirror information
     * @throws InternalException
     * @throws UserException
     */
    public BriefHostInfo validateMirrorRequest(
                final BriefHostInfo hostInfo,
                final MirrorGroupActionEnum action,
                final String desiredHostName,
                final long userId) throws InternalException, UserException {

        final BriefHostInfo mainMirror;
        if (hostInfo.getMainMirrorId() == null) {
            mainMirror = hostInfo;
        } else {
            mainMirror = tblHostsMainDao.getBriefHostInfoByHostId(hostInfo.getMainMirrorId());
        }

        //
        // check that desiredHostName is www!www or is http/https twin or is verified not main mirror
        //

        if (action == MirrorGroupActionEnum.RERANGE) {

            final String simplifiedHostName = getSimplifiedHostName(mainMirror);
            final Set<String> allowedHostNames = getSimilarHostNames(simplifiedHostName);

            final List<BriefHostInfo> verifiedNotMainMirrors = usersHostsService.getVerifiedMirrorsForHostByUser(mainMirror, userId);
            allowedHostNames.addAll(Cu.map(briefHostInfoToNameMapper, verifiedNotMainMirrors));
            allowedHostNames.add(mainMirror.getName().toLowerCase());

            if (!allowedHostNames.contains(desiredHostName.toLowerCase())) {
                throw new UserException(WMCUserProblem.DESIRED_MAIN_MIRROR_IS_NOT_ALLOWED,
                        "desired main is not verified for user and is not www/https clone of the main mirror");
            }

        }

        //
        // check that there are no contradictory request for main mirror or other not main mirrors
        //

        // get all hosts of group
        List<BriefHostInfo> allNotMainMirrors = tblHostsMainDao.getHostMirrors(mainMirror.getId());
        List<BriefHostInfo> allRequests = new LinkedList<BriefHostInfo>(allNotMainMirrors);
        allRequests.remove(hostInfo);

        // get all active requests from other hosts of group (not DECLINED nor RECHECK_DECLINED)
        List<MirrorGroupChangeRequest> otherRequests = tblMainMirrorRequestsDao.getActualRerankRequestsForHosts(
                Cu.map(briefHostInfoToIdMapper, allRequests));

        // check if there are rerank requests with another desiredMainMirror
        if (action == MirrorGroupActionEnum.RERANGE) {
            for (MirrorGroupChangeRequest r : otherRequests) {
                if (r.getAction() == MirrorGroupActionEnum.RERANGE &&
                        !desiredHostName.equalsIgnoreCase(r.getDesiredMain())) {
                    String wwwMirrorHostName = tblHostsMainDao.getBriefHostInfoByHostId(r.getHostId()).getName();
                    throw new UserException(WMCUserProblem.MAIN_MIRROR_WWW_STATUS_IS_INCONSISTENT,
                            "main mirror request for main mirror " + desiredHostName +
                                    " conflicts with request " + r.getDesiredMain() + " created in context of " + r.getHostId(),
                            new ExtraTagInfo[]{
                                    new ExtraTagInfo(WMCExtraTagNameEnum.WWW_MIRROR_HOST_NAME, wwwMirrorHostName),
                                    new ExtraTagInfo(WMCExtraTagNameEnum.WWW_MIRROR_DESIRED_MAIN, r.getDesiredMain())
                            });
                } else if (r.getAction() == MirrorGroupActionEnum.UNSTICK) {
                    String wwwMirrorHostName = tblHostsMainDao.getBriefHostInfoByHostId(r.getHostId()).getName();
                    if (wwwMirrorHostName.equalsIgnoreCase(desiredHostName)) {
                        throw new UserException(WMCUserProblem.MAIN_MIRROR_WWW_STATUS_IS_INCONSISTENT,
                                "main mirror request for main mirror " + desiredHostName +
                                        " conflicts with unstick request " + wwwMirrorHostName + " created in context of " + r.getHostId(),
                                new ExtraTagInfo[]{
                                        new ExtraTagInfo(WMCExtraTagNameEnum.WWW_MIRROR_HOST_NAME, wwwMirrorHostName),
                                        new ExtraTagInfo(WMCExtraTagNameEnum.WWW_MIRROR_DESIRED_MAIN, wwwMirrorHostName)
                                });
                    }
                }
            }
        } else if (action == MirrorGroupActionEnum.UNSTICK) {
            for (MirrorGroupChangeRequest r : otherRequests) {
                if (r.getAction() == MirrorGroupActionEnum.RERANGE) {
                    String wwwMirrorHostName = tblHostsMainDao.getBriefHostInfoByHostId(r.getHostId()).getName();
                    if (hostInfo.getName().equalsIgnoreCase(r.getDesiredMain())) {
                        throw new UserException(WMCUserProblem.MAIN_MIRROR_WWW_STATUS_IS_INCONSISTENT,
                                "unstick request for " + hostInfo.getName() +
                                        " conflicts with rerank request " + r.getDesiredMain() + " created in context of " + r.getHostId(),
                                new ExtraTagInfo[]{
                                        new ExtraTagInfo(WMCExtraTagNameEnum.WWW_MIRROR_HOST_NAME, wwwMirrorHostName),
                                        new ExtraTagInfo(WMCExtraTagNameEnum.WWW_MIRROR_DESIRED_MAIN, r.getDesiredMain())
                                });
                    }
                }
            }
        }

        return mainMirror;
    }

    /**
     * Calculates simplified host name which is host name without schema and without www.
     *
     * @param hostInfo
     * @return
     */
    public static String getSimplifiedHostName(final BriefHostInfo hostInfo) {
        try {
            URL url = SupportedProtocols.getURL(hostInfo.getName());
            StringBuilder builder = new StringBuilder();
            builder.append(url.getHost());
            if (url.getPort() != -1) {
                builder.append(":").append(url.getPort());
            }
            return WwwUtil.getHostName(builder.toString(), MainMirrorStateEnum.WITHOUT_WWW);
        } catch (MalformedURLException e) {
            log.error("Malformed url " + hostInfo.getName(), e);
            return hostInfo.getName();
        } catch (SupportedProtocols.UnsupportedProtocolException e) {
            log.error("Unsupporter protocol in url " + hostInfo.getName(), e);
            return hostInfo.getName();
        } catch (URISyntaxException e) {
            log.error("URI Syntax error in url " + hostInfo.getName(), e);
            return hostInfo.getName();
        }
    }

    /**
     * Generates a set of similar hosts in lowercase given simple host name
     *
     * @param simplifiedHostName    hostName without schema and without www
     * @return  a set of similar hosts
     */
    public static Set<String> getSimilarHostNames(final String simplifiedHostName) {

        Set<String> result = new HashSet<>();

        String lcSimplifiedHostName = simplifiedHostName.toLowerCase();

        result.add(lcSimplifiedHostName);
        result.add(WwwUtil.switchWWW(lcSimplifiedHostName));
        result.add("https://" + lcSimplifiedHostName);
        result.add("https://www." + lcSimplifiedHostName);
        return result;
    }

    /**
     * Stores request to unstick the host or rerank in the next mirror database
     *
     * @param briefHostInfo     host
     * @param action            requested action
     * @param desiredHostName   desired main mirror
     * @param userId            user who requested the action
     * @param oldMainMirror     main mirror of a host at the moment of request
     * @throws InternalException
     */
    public void saveMirrorRequest(
            final BriefHostInfo briefHostInfo,
            final MirrorGroupActionEnum action,
            final String desiredHostName,
            final long userId,
            final String oldMainMirror) throws InternalException {

        List<MirrorGroupChangeRequest> rs = tblMainMirrorRequestsDao.getRequests(briefHostInfo);
        if (!rs.isEmpty()) {
            // TODO: is it always necessary?
            tblMainMirrorHistoryDao.saveRequest(CollectionUtils.first(rs));
        }

        Date now = new Date();
        MirrorGroupChangeRequest request = new MirrorGroupChangeRequest(
                briefHostInfo.getId(), userId, now, now, desiredHostName, action, MirrorGroupChangeStateEnum.NEW, oldMainMirror, null, null, null, null, null, MirrorGroupChangeRequest.DEFAULT_ATTEMPTS, false
        );

        // save request in history table
        tblMainMirrorHistoryDao.saveRequest(request);

        if (action == MirrorGroupActionEnum.DEFAULT) {
            tblMainMirrorRequestsDao.deleteRequest(briefHostInfo);
        } else {
            tblMainMirrorRequestsDao.saveRequestEx(briefHostInfo, request);
        }
    }

    /**
     * Returns main mirror requests for host
     *
     * @param briefHostInfo         host
     * @return                      list of main mirror requests
     * @throws InternalException    if db request fails
     */
    public List<MirrorGroupChangeRequest> getMainMirrorChangeRequests(final BriefHostInfo briefHostInfo) throws InternalException {
        List<MirrorGroupChangeRequest> res = tblMainMirrorRequestsDao.getRequests(briefHostInfo);

        for (MirrorGroupChangeRequest r : res) {
            if (r.getAction() == MirrorGroupActionEnum.RERANGE &&
                    Cf.set(MirrorGroupChangeStateEnum.DECLINED,
                            MirrorGroupChangeStateEnum.RECHECK_DECLINED).contains(r.getState())) {

                final MainMirrorHistoryInfo lastSuccessful = tblMainMirrorHistoryDao.getLastSuccessfulRequest(r.getHostId());
                if (lastSuccessful != null && !tblMainMirrorHistoryDao.hasFailedRequestsAfter(lastSuccessful)) {
                    r.setLastSuccessfulMainMirrorName(lastSuccessful.getDesiredMain());
                }
            }
        }

        return res;
    }

    /**
     * Copies requests in the case of www!www reranking
     *
     * @throws InternalException
     */
    public void copyRequestsOnMainMirrorChange(BriefHostInfo oldHostInfo, BriefHostInfo newHostInfo) throws InternalException {
        List<MirrorGroupChangeRequest> oldRequests = tblMainMirrorRequestsDao.getRequests(oldHostInfo);
        List<MirrorGroupChangeRequest> newRequests = tblMainMirrorRequestsDao.getRequests(newHostInfo);
        if (oldRequests.isEmpty()) {
            // nothing to copy
            return;
        } else if (newRequests.isEmpty() && WwwUtil.equalsIgnoreWww(oldHostInfo.getName(), newHostInfo.getName())) {
            MirrorGroupChangeRequest r = Cu.first(oldRequests);
            tblMainMirrorRequestsDao.saveRequestEx(newHostInfo, r);
        }
    }

    /**
     * Changing state of main mirror request
     *
     * @param hostInfo          host which requests have to be updated
     * @param newMainMirror     new main mirror of host (possibly null in case of unsticking)
     * @throws InternalException
     */
    public void applyLogicOnMainMirrorChange(BriefHostInfo hostInfo, @Nullable BriefHostInfo newMainMirror) throws InternalException {
        List<MirrorGroupChangeRequest> requests = tblMainMirrorRequestsDao.getRequests(hostInfo);
        if (requests.isEmpty()) {
            return;
        }

        MirrorGroupChangeRequest r = Cu.first(requests);
        if (MirrorGroupActionEnum.UNSTICK.equals(r.getAction())) {
            switch (r.getState()) {
                case CHECKED:
                    if (newMainMirror == null) {
                        tblMainMirrorHistoryDao.saveRequest(r);
                        tblMainMirrorRequestsDao.updateState(new MirrorGroupChangeRequest(r, MirrorGroupChangeStateEnum.ACCEPTED, r.getAttempts()));
                    } else {
                        tblMainMirrorHistoryDao.saveRequest(r);
                        tblMainMirrorRequestsDao.updateState(new MirrorGroupChangeRequest(r, MirrorGroupChangeStateEnum.NEED_RECHECK, MirrorGroupChangeRequest.DEFAULT_ATTEMPTS));
                    }
                    break;
                case ACCEPTED:
                    if (newMainMirror != null) {
                        tblMainMirrorHistoryDao.saveRequest(r);
                        tblMainMirrorRequestsDao.updateState(new MirrorGroupChangeRequest(r, MirrorGroupChangeStateEnum.NEED_RECHECK, MirrorGroupChangeRequest.DEFAULT_ATTEMPTS));
                    }
                    break;
            }
        } else if (MirrorGroupActionEnum.RERANGE.equals(r.getAction())) {
            final String desiredMainMirror = r.getDesiredMain();
            switch (r.getState()) {
                case CHECKED:
                    if (newMainMirror == null && desiredMainMirror.equalsIgnoreCase(hostInfo.getName()) ||
                           newMainMirror != null && desiredMainMirror.equalsIgnoreCase(newMainMirror.getName())) {
                        tblMainMirrorHistoryDao.saveRequest(r);
                        tblMainMirrorRequestsDao.updateState(new MirrorGroupChangeRequest(r, MirrorGroupChangeStateEnum.ACCEPTED, r.getAttempts()));
                    } else {
                        tblMainMirrorHistoryDao.saveRequest(r);
                        tblMainMirrorRequestsDao.updateState(new MirrorGroupChangeRequest(r, MirrorGroupChangeStateEnum.NEED_RECHECK, MirrorGroupChangeRequest.DEFAULT_ATTEMPTS));
                    }
                    break;
                case ACCEPTED:
                    if (newMainMirror != null) {
                        tblMainMirrorHistoryDao.saveRequest(r);
                        tblMainMirrorRequestsDao.updateState(new MirrorGroupChangeRequest(r, MirrorGroupChangeStateEnum.NEED_RECHECK, MirrorGroupChangeRequest.DEFAULT_ATTEMPTS));
                    }
                    break;
            }
        } else {
            log.error("unsupported main mirror request action {}", r.getAction());
        }
    }

    public boolean getLockForMirrorRequestProcessing() {
        return getLock(WMCPartition.nullPartition(), MIRROR_REQUEST_LOCK_NAME, (int)TimeUnit.MINUTES.toMinutes(10));
    }

    public boolean releaseLockForMirrorRequestProcessing() {
        return releaseLock(WMCPartition.nullPartition(), MIRROR_REQUEST_LOCK_NAME);
    }

    public MirrorGroupChangeRequest listFirstWaiting() throws InternalException {
        return tblMainMirrorRequestsDao.getFirstWaitingRequest();
    }

    public void updateStateToInProgress(MirrorGroupChangeRequest request) throws InternalException {
        final MirrorGroupChangeStateEnum stateInProgress =
                MirrorGroupChangeStateEnum.NEW.equals(request.getState()) ? MirrorGroupChangeStateEnum.IN_PROGRESS : MirrorGroupChangeStateEnum.RECHECK_IN_PROGRESS;
        tblMainMirrorRequestsDao.updateState(new MirrorGroupChangeRequest(request, stateInProgress, request.getAttempts()));
    }

    public void updateState(MirrorGroupChangeRequest request) throws InternalException {
        tblMainMirrorRequestsDao.updateState2(request);
    }

    public void markNotified(BriefHostInfo hostInfo) throws InternalException {
        tblMainMirrorRequestsDao.markNotified(hostInfo);
    }
    
    private final Function<BriefHostInfo, String> briefHostInfoToNameMapper = new Function<BriefHostInfo, String>() {
        @Override
        public String apply(BriefHostInfo info) {
            return info.getName().toLowerCase();
        }
    };

    private final Function<BriefHostInfo, Long> briefHostInfoToIdMapper = new Function<BriefHostInfo, Long>() {
        @Override
        public Long apply(BriefHostInfo info) {
            return info.getId();
        }
    };

    @Required
    public void setTblMainMirrorRequestsDao(TblMainMirrorRequestsDao tblMainMirrorRequestsDao) {
        this.tblMainMirrorRequestsDao = tblMainMirrorRequestsDao;
    }

    @Required
    public void setTblMainMirrorHistoryDao(TblMainMirrorHistoryDao tblMainMirrorHistoryDao) {
        this.tblMainMirrorHistoryDao = tblMainMirrorHistoryDao;
    }

    @Required
    public void setTblHostsMainDao(TblHostsMainDao tblHostsMainDao) {
        this.tblHostsMainDao = tblHostsMainDao;
    }

    @Required
    public void setUsersHostsService(UsersHostsService usersHostsService) {
        this.usersHostsService = usersHostsService;
    }
}
