package ru.yandex.wmconsole.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import ru.yandex.common.util.collections.Cu;
import ru.yandex.common.util.concurrent.CommonThreadFactory;
import ru.yandex.webmaster.common.host.dao.TblHostsMainDao;
import ru.yandex.wmconsole.common.service.AbstractUrlVisitorService;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.mirror.MirrorGroupChangeRequest;
import ru.yandex.wmconsole.data.mirror.MirrorGroupChangeStateEnum;
import ru.yandex.wmtools.common.SupportedProtocols;
import ru.yandex.wmtools.common.error.*;
import ru.yandex.wmtools.common.sita.*;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * User: azakharov
 * Date: 21.03.14
 * Time: 16:28
 */
public class MainMirrorOnlineCheckerService extends AbstractUrlVisitorService<MirrorGroupChangeRequest> {

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

    private ExecutorService pool;
    private final AtomicInteger numActive = new AtomicInteger(0);

    private MirrorGroupsChangeService mirrorGroupsChangeService;
    private SitaService newSitaService;
    private SitaService prodSitaService;
    private TblHostsMainDao tblHostsMainDao;
    private int sleepIntervalMs = 1000;

    public void init() {
        CommonThreadFactory threadFactory = new CommonThreadFactory(true, MainMirrorOnlineCheckerService.class.getSimpleName() + "-");
        pool = Executors.newFixedThreadPool(1, threadFactory);
    }

    @Override
    protected MirrorGroupChangeRequest listFirstWaiting(int databaseIndex) throws InternalException {
        return mirrorGroupsChangeService.listFirstWaiting();
    }

    @Override
    protected MirrorGroupChangeRequest check(MirrorGroupChangeRequest request) {
        try {
            switch (request.getAction()) {
                case UNSTICK:
                    return checkUnstickAllowed(request);
                case RERANGE:
                    return checkRerankAllowed(request);
                case DEFAULT:
                    return new MirrorGroupChangeRequest(request, MirrorGroupChangeStateEnum.ACCEPTED, request.getAttempts());
                default:
                    return new MirrorGroupChangeRequest(request, MirrorGroupChangeStateEnum.DECLINED, request.getAttempts());
            }
        } catch (SitaException e) {
            log.error("Sita exception", e);
            int attempts = request.getAttempts() != null ? request.getAttempts() - 1 : 0;
            return new MirrorGroupChangeRequest(request, MirrorGroupChangeStateEnum.NEED_RECHECK, attempts);
        } catch (InternalException e) {
            log.error("Internal exception", e);
            int attempts = request.getAttempts() != null ? request.getAttempts() - 1 : 0;
            return new MirrorGroupChangeRequest(request, MirrorGroupChangeStateEnum.NEED_RECHECK, attempts);
        }
    }

    @Override
    protected void updateStateToInProgress(MirrorGroupChangeRequest request) throws InternalException {
        mirrorGroupsChangeService.updateStateToInProgress(request);
    }

    @Override
    protected void updateStateToCurrentStateValue(MirrorGroupChangeRequest request) throws InternalException {
        mirrorGroupsChangeService.updateState(request);
    }

    @Override
    protected void onStateChanged(MirrorGroupChangeRequest mirrorGroupChangeRequest, MirrorGroupChangeRequest newT) throws InternalException {
        // not used
    }

    @Override
    public void fireCheck() {
        if (numActive.compareAndSet(0, 1)) {
            try {

                if (!mirrorGroupsChangeService.getLockForMirrorRequestProcessing()) {
                    log.error("Can not get lock for MainMirrorOnlineChecker");
                    numActive.set(0);
                }

                pool.submit(new Runnable() {
                    @Override
                    public void run() {
                        logUserDbConnections();
                        try {
                            checkDatabase(getDatabaseCount() - 1);
                        } finally {
                            mirrorGroupsChangeService.releaseLockForMirrorRequestProcessing();
                            logUserDbConnections();
                            numActive.decrementAndGet();
                        }
                    }
                });
            } catch (Exception e) {
                log.error("Error while submitting new task to MainMirrorOnlineChecker", e);
                numActive.set(0);
            }
        } else {
            // Предыдущие потоки еще не завершились
            log.info("Previous threads are still running. Suspend checks");
            // Восстанавливаем работу системы, если пришли в недопустимое состояние
            int num = numActive.get();
            if (num < 0 || num > 1) {
                log.error("Changing numActive from " + num + " to 0");
                numActive.set(0);
            }
        }
    }

    private MirrorGroupChangeRequest checkUnstickAllowed(MirrorGroupChangeRequest request) throws InternalException {
        BriefHostInfo briefHostInfo = tblHostsMainDao.getBriefHostInfoByHostId(request.getHostId());

        final URI uri;
        try {
            uri = SupportedProtocols.getURL(briefHostInfo.getName()).toURI();
        } catch (URISyntaxException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "URI syntax error", e);
        } catch (MalformedURLException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Malformed URL", e);
        } catch (SupportedProtocols.UnsupportedProtocolException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Unsupported protocol", e);
        }

        try {
            Thread.sleep(sleepIntervalMs);
        } catch (InterruptedException e) {
            log.error("Sleep interrupted", e);
        }

        // get main mirror from current mirror base
        URI currentBaseMainMirror = prodSitaService.getMainMirrorCurrentBase(uri);

        // check unstick using online mirror check tool
        SitaMirroringResponse response = newSitaService.request(SitaMirroringRequest.createUnstickRequest(
                currentBaseMainMirror,
                Collections.singletonList(uri)));

        final String host1;
        final SitaMirroringHostStatusEnum status1;
        final String host2;
        final SitaMirroringHostStatusEnum status2;
        if (response.getHostResults().size() != 2) {
            host1 = null;
            status1 = null;
            host2 = null;
            status2 = null;
        } else {
            SitaMirroringResponse.THostResult h1 = Cu.first(response.getHostResults());
            host1 = h1.getHost().toString();
            status1 = h1.getStatus();
            SitaMirroringResponse.THostResult h2 = Cu.last(response.getHostResults());
            host2 = h2.getHost().toString();
            status2 = h2.getStatus();
        }

        MirrorGroupChangeStateEnum possibleDeclinedState =
                MirrorGroupChangeStateEnum.NEW.equals(request.getState()) ? MirrorGroupChangeStateEnum.DECLINED : MirrorGroupChangeStateEnum.RECHECK_DECLINED;
        switch(response.getStatus()) {
            case OK:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), MirrorGroupChangeStateEnum.CHECKED);
                return new MirrorGroupChangeRequest(request, MirrorGroupChangeStateEnum.CHECKED, request.getAttempts());
            case ERROR_TIMEOUT:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), possibleDeclinedState);
                return new MirrorGroupChangeRequest(request, possibleDeclinedState, response.getStatus(), host1, status1, host2, status2);
            case ERROR_USER:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), possibleDeclinedState);
                return new MirrorGroupChangeRequest(request, possibleDeclinedState, response.getStatus(), host1, status1, host2, status2);
            case ERROR_INTERNAL:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), possibleDeclinedState);
                return new MirrorGroupChangeRequest(request, possibleDeclinedState, response.getStatus(), host1, status1, host2, status2);
            default:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), possibleDeclinedState);
                return new MirrorGroupChangeRequest(request, possibleDeclinedState, response.getStatus(), host1, status1, host2, status2);
        }
    }

    private MirrorGroupChangeRequest checkRerankAllowed(MirrorGroupChangeRequest request) throws InternalException {
        final String desiredMainMirror = request.getDesiredMain();
        final BriefHostInfo briefHostInfo = tblHostsMainDao.getBriefHostInfoByHostId(request.getHostId());

        final URI uri;
        try {
            uri = SupportedProtocols.getURL(briefHostInfo.getName()).toURI();
        } catch (URISyntaxException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "URI syntax error", e);
        } catch (MalformedURLException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Malformed URL", e);
        } catch (SupportedProtocols.UnsupportedProtocolException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Unsupported protocol", e);
        }

        try {
            Thread.sleep(sleepIntervalMs);
        } catch (InterruptedException e) {
            log.error("Sleep interrupted", e);
        }

        // get main mirror from current mirror base
        URI currentBaseMainMirror = prodSitaService.getMainMirrorCurrentBase(uri);

        final URI desiredMainHostUri;
        try {
            desiredMainHostUri = SupportedProtocols.getURL(desiredMainMirror).toURI();
        } catch (URISyntaxException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "URI syntax error", e);
        } catch (MalformedURLException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Malformed URL", e);
        } catch (SupportedProtocols.UnsupportedProtocolException e) {
            throw new InternalException(InternalProblem.INTERNAL_PROBLEM, "Unsupported protocol", e);
        }

        if (currentBaseMainMirror.equals(desiredMainHostUri)) {
            log.info("currentBaseMainMirror equals currentBaseMainMirror. Skip MirroringAction sita request");
            log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), MirrorGroupChangeStateEnum.CHECKED);
            return new MirrorGroupChangeRequest(request, MirrorGroupChangeStateEnum.CHECKED, request.getAttempts());
        }

        // check rerank using online mirror check tool
        SitaMirroringResponse response = newSitaService.request(SitaMirroringRequest.createRerankRequest(
                currentBaseMainMirror,
                desiredMainHostUri));

        final String host1;
        final SitaMirroringHostStatusEnum status1;
        final String host2;
        final SitaMirroringHostStatusEnum status2;
        if (response.getHostResults().size() != 2) {
            host1 = null;
            status1 = null;
            host2 = null;
            status2 = null;
        } else {
            SitaMirroringResponse.THostResult h1 = Cu.first(response.getHostResults());
            host1 = h1.getHost().toString();
            status1 = h1.getStatus();
            SitaMirroringResponse.THostResult h2 = Cu.last(response.getHostResults());
            host2 = h2.getHost().toString();
            status2 = h2.getStatus();
        }

        MirrorGroupChangeStateEnum possibleDeclinedState =
                MirrorGroupChangeStateEnum.NEW.equals(request.getState()) ? MirrorGroupChangeStateEnum.DECLINED : MirrorGroupChangeStateEnum.RECHECK_DECLINED;
        switch(response.getStatus()) {
            case OK:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), MirrorGroupChangeStateEnum.CHECKED);
                return new MirrorGroupChangeRequest(request, MirrorGroupChangeStateEnum.CHECKED, request.getAttempts());
            case ERROR_TIMEOUT:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), possibleDeclinedState);
                return new MirrorGroupChangeRequest(request, possibleDeclinedState, response.getStatus(), host1, status1, host2, status2);
            case ERROR_USER:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), possibleDeclinedState);
                return new MirrorGroupChangeRequest(request, possibleDeclinedState, response.getStatus(), host1, status1, host2, status2);
            case ERROR_INTERNAL:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), possibleDeclinedState);
                return new MirrorGroupChangeRequest(request, possibleDeclinedState, response.getStatus(), host1, status1, host2, status2);
            default:
                log.info("new status for main mirror request {} is {}", briefHostInfo.getName(), possibleDeclinedState);
                return new MirrorGroupChangeRequest(request, possibleDeclinedState, response.getStatus(), host1, status1, host2, status2);
        }
    }

    @Required
    public void setMirrorGroupsChangeService(MirrorGroupsChangeService mirrorGroupsChangeService) {
        this.mirrorGroupsChangeService = mirrorGroupsChangeService;
    }

    @Required
    public void setNewSitaService(SitaService newSitaService) {
        this.newSitaService = newSitaService;
    }

    @Required
    public void setProdSitaService(SitaService prodSitaService) {
        this.prodSitaService = prodSitaService;
    }

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

    public void setSleepIntervalMs(int sleepIntervalMs) {
        this.sleepIntervalMs = sleepIntervalMs;
    }
}
