package ru.yandex.webmaster3.viewer.http.metrika.counter;

import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.Action;
import ru.yandex.webmaster3.core.http.WriteAction;
import ru.yandex.webmaster3.core.metrics.Category;
import ru.yandex.webmaster3.core.metrika.counters.*;
import ru.yandex.webmaster3.core.util.Either;
import ru.yandex.webmaster3.core.util.TimeUtils;
import ru.yandex.webmaster3.core.util.environment.YandexEnvironmentProvider;
import ru.yandex.webmaster3.core.util.environment.YandexEnvironmentType;
import ru.yandex.webmaster3.core.worker.client.WorkerClient;
import ru.yandex.webmaster3.core.worker.task.MetrikaCounterBindingStateChangeTaskData;
import ru.yandex.webmaster3.core.worker.task.MetrikaCounterBindingTaskData;
import ru.yandex.webmaster3.storage.events.service.WMCEventsService;
import ru.yandex.webmaster3.storage.metrika.MetrikaCounterBindingService;

import static ru.yandex.webmaster3.core.metrika.counters.MetrikaCountersInternalService.ErrorCodeEnum;
import static ru.yandex.webmaster3.core.metrika.counters.MetrikaCountersInternalService.ResponseStatusEnum;


/**
 * Created by ifilippov5 on 24.10.17.
 */
@WriteAction
@Category("metrika")
public class BindCounterAction extends Action<BindCounterRequest, BindCounterResponse> {
    private static final Logger log = LoggerFactory.getLogger(BindCounterAction.class);
    public static final String TEST_DOMAIN = "bloodygame08.narod.ru";

    private final MetrikaCounterBindingService metrikaCounterBindingService;
    private final MetrikaCountersInternalService metrikaCountersInternalService;
    private final WMCEventsService wmcEventsService;
    private final WorkerClient workerClient;

    private CounterRequestTypeEnum requestType;

    @Autowired
    public BindCounterAction(
            MetrikaCounterBindingService metrikaCounterBindingService,
            MetrikaCountersInternalService metrikaCountersInternalService,
            WMCEventsService wmcEventsService,
            WorkerClient workerClient) {
        this.metrikaCounterBindingService = metrikaCounterBindingService;
        this.metrikaCountersInternalService = metrikaCountersInternalService;
        this.wmcEventsService = wmcEventsService;
        this.workerClient = workerClient;
    }

    @Override
    public BindCounterResponse process(BindCounterRequest request) throws WebmasterException {
        long counterId = request.getCounterId();
        long userId = request.getUserId();
        WebmasterHostId hostId = request.getHostId();
        String domain = MetrikaCountersUtil.hostToPunycodeDomain(hostId);
        String counterLogName = domain + ":" + counterId;

        log.info("Action {}:{} for {}", requestType, requestType.getPath(), counterLogName);

        CounterBindingStateEnum curState = metrikaCounterBindingService.getState(
                MetrikaCountersUtil.hostToPunycodeDomain(hostId), counterId);
        log.info("Counter {} current state: {}", counterLogName, curState);

        CounterInfo counterInfo = metrikaCounterBindingService.getCounterInfo(counterId, hostId.getReadableHostname());
        log.info("Counter {} status in Metrika: {}", counterLogName, counterInfo.getStatus());

        CounterTransitionHandler handler = new CounterTransitionHandler(hostId, userId, counterInfo, curState);
        BindCounterResponse err = handler.handleTransition();
        if (err != null) {
            return err;
        }

        // сохраним новое состояние
        CounterBindingStateEnum nextState = handler.getNextState();
        DateTime now = DateTime.now(TimeUtils.EUROPE_MOSCOW_ZONE);
        String userLogin = metrikaCounterBindingService.getUserLogin(userId);

        String origin = requestType == CounterRequestTypeEnum.CREATE ? AnonymousCounterBinding.ORIGIN_WEBMASTER : null;
        if (handler.isAutoApproved()) {
            metrikaCounterBindingService.updateStateWithMetrikaAndWebmasterUser(domain, nextState, counterId, userLogin,
                    now, userId, origin);
        } else {
            metrikaCounterBindingService.updateStateWithWebmasterUser(domain, nextState, counterId, userLogin,
                    now, userId, origin);
        }

        MetrikaCounterBindingInfo bindingInfo = new MetrikaCounterBindingInfo(
                counterId,
                counterInfo.getCounterName(),
                counterInfo.getCounterSite(),
                userLogin,
                now.toLocalDate(),
                nextState);

        if (nextState != curState) {
            if (requestType == CounterRequestTypeEnum.DISMISS) {
                workerClient.checkedEnqueueTask(new MetrikaCounterBindingTaskData(requestType, domain, userId, userLogin, counterId));
            }

            workerClient.checkedEnqueueTask(new MetrikaCounterBindingStateChangeTaskData(
                    requestType,
                    domain,
                    userId,
                    userLogin,
                    counterId,
                    curState,
                    nextState,
                    DateTime.now()
            ));
        }


        return new BindCounterResponse.NormalResponse(bindingInfo);
    }

    public CounterRequestTypeEnum getRequestType() {
        return requestType;
    }


    @Required
    public void setRequestType(CounterRequestTypeEnum requestType) {
        this.requestType = requestType;
    }

    private class CounterTransitionHandler {
        private final WebmasterHostId hostId;
        private final long userId;
        private final CounterInfo counterInfo;
        private final CounterBindingStateEnum curState;
        private final String counterLogName;

        private CounterBindingStateEnum nextState = null;
        private boolean isAutoApproved = false;

        CounterTransitionHandler(WebmasterHostId hostId, long userId,
                                 CounterInfo counterInfo, CounterBindingStateEnum curState) {
            this.hostId = hostId;
            this.userId = userId;
            this.counterInfo = counterInfo;
            this.curState = curState;
            this.counterLogName = MetrikaCountersUtil.hostToPunycodeDomain(hostId) + ":" + counterInfo.getCounterId();
        }

        CounterBindingStateEnum getNextState() {
            return nextState;
        }

        boolean isAutoApproved() {
            return isAutoApproved;
        }

        @Nullable
        BindCounterResponse handleTransition() {
            BindCounterResponse err = verifyCounterStateForRequest();
            if (err != null) {
                return err;
            }

            CounterActionTypeEnum actionType = MetrikaCountersUtil.getActionFromWebmasterType(requestType);
            nextState = MetrikaCountersUtil.nextState(curState, actionType);
            log.info("Counter {} next state: {}", counterLogName, nextState);
            if (nextState == curState) {
                log.info("Current and next state for counter {} are the same: {}", counterLogName, curState);
                return null;
            }

            if (actionType == CounterActionTypeEnum.W_REQUEST) {
                return handleWebmasterRequestTransition();
            } else if (actionType == CounterActionTypeEnum.W_REJECT) {
                return handleWebmasterRejectTransition();
            } else {
                log.error("Unexpected action type {} for counter {}", actionType, counterLogName);
                throw new RuntimeException("Unexpected action type: " + actionType);
            }
        }

        @Nullable
        private BindCounterResponse handleWebmasterRejectTransition() {
            if (curState == CounterBindingStateEnum.METRIKA_REJECT) {
                // В Метрике уже удалено, не будем ее оповещать
                log.info("Not calling Metrika API, counter {} current state is METRIKA_REJECT", counterLogName);
                return null;
            }

            // пойдем в API Метрики
            log.info("Sending Metrika request {} for counter {}", requestType.getPath(), counterLogName);
            Either<ResponseStatusEnum, ErrorCodeEnum> response = metrikaCountersInternalService.sendRequest(
                    MetrikaCountersUtil.hostToReadableDomain(hostId), userId, counterInfo.getCounterId(), requestType);

            ErrorCodeEnum error = response.getRight().orElse(null);
            if (error != null) {
                log.error("Error response {} from Metrika for counter {}", error, counterLogName);

                switch (error) {
                    case WRONG_DOMAIN:
                        return new BindCounterResponse.WrongDomainForCounterError();

                    case ALREADY_DELETED:
                        // игнорируем: пытались удалить на нашей стороне уже удаленный на стороне Метрики счетчик
                        log.info("Ignoring error {} for counter {}", error, counterLogName);
                        break;

                    default:
                        log.error("Unknown error response {} from Metrika for counter {}", error, counterLogName);
                        throw new RuntimeException("Unknown error code " + error);
                }
            }

            return null;
        }

        @Nullable
        private BindCounterResponse handleWebmasterRequestTransition() {
            // for testing
            if (YandexEnvironmentProvider.getEnvironmentType() != YandexEnvironmentType.PRODUCTION) {
                if (MetrikaCountersUtil.hostToReadableDomain(hostId).equals(TEST_DOMAIN)) {
                    log.info("Counter {} auto approved for testing", counterLogName);
                    nextState = CounterBindingStateEnum.APPROVED;
                    isAutoApproved = true;

                    return null;
                }
            }

            // пойдем в API Метрики
            log.info("Sending Metrika request {} for counter {}", requestType.getPath(), counterLogName);
            Either<ResponseStatusEnum, ErrorCodeEnum> response = metrikaCountersInternalService.sendRequest(
                    MetrikaCountersUtil.hostToReadableDomain(hostId), userId, counterInfo.getCounterId(), requestType);

            ErrorCodeEnum error = response.getRight().orElse(null);
            if (error != null) {
                log.error("Error response {} from Metrika for counter {}", error, counterLogName);

                switch (error) {
                    case WRONG_DOMAIN:
                        return new BindCounterResponse.WrongDomainForCounterError();
                    case ALREADY_DELETED:
                        return new BindCounterResponse.MetrikaCounterDeletedError("Counter " + counterLogName + " deleted in Metrika");
                    case ALREADY_EXISTS:
                        return null; //ignore this error
                    default:
                        log.error("Unknown error response {} from Metrika for counter {}", error, counterLogName);
                        throw new RuntimeException("Unknown error code " + error);
                }
            }

            // https://st.yandex-team.ru/METR-30730
            // Метрика может автоматически подтвердить счетчик, о чем они сообщают специальным статусом ответа
            ResponseStatusEnum status = response.getLeft().orElse(ResponseStatusEnum.UNKNOWN);
            if (status == ResponseStatusEnum.OK) {
                log.info("Counter {} auto approved by Metrika", counterLogName);
                nextState = CounterBindingStateEnum.APPROVED;
                isAutoApproved = true;
            } else {
                log.info("Counter {} needs to be manually approved by Metrika user", counterLogName);
            }

            return null;
        }

        @Nullable
        private BindCounterResponse verifyCounterStateForRequest() {
            // for testing
            if (YandexEnvironmentProvider.getEnvironmentType() != YandexEnvironmentType.PRODUCTION) {
                if (MetrikaCountersUtil.hostToReadableDomain(hostId).equals(TEST_DOMAIN)) {
                    return null;
                }
            }

            if (requestType == CounterRequestTypeEnum.CREATE && curState != CounterBindingStateEnum.NONE) {
                log.error("Inconsistent counter {} state {} for action {}", counterLogName, curState, requestType);
                return new BindCounterResponse.BindingAlreadyExistsError("Binding has state = " + curState);
            }

            if (counterInfo.getStatus() == CounterInfo.CounterStatusEnum.DELETED && !requestType.isDelete()) {
                log.error("Inconsistent counter {} state for action {}, deleted in Metrika", counterLogName, requestType);
                return new BindCounterResponse.MetrikaCounterDeletedError("Counter " + counterLogName + " deleted in Metrika");
            }

            return null;
        }
    }
}
