package ru.yandex.webmaster.viewer.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequest;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.jetbrains.annotations.Nullable;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import ru.yandex.webmaster.viewer.service.webmaster3.HostVerificationInfo;
import ru.yandex.wmconsole.data.DisplayNameModerationStateEnum;
import ru.yandex.wmconsole.data.HostRegionChangeRequestStateEnum;
import ru.yandex.wmconsole.data.VerificationStateEnum;
import ru.yandex.wmconsole.data.VirusRecheckEnum;
import ru.yandex.wmconsole.data.info.BriefHostInfo;
import ru.yandex.wmconsole.data.info.RecheckInfo;
import ru.yandex.wmconsole.data.info.UserHostRegionInfo;
import ru.yandex.wmconsole.service.error.WMCUserProblem;
import ru.yandex.wmconsole.util.BackportedIdUtils;
import ru.yandex.wmconsole.verification.VerificationTypeEnum;
import ru.yandex.wmtools.common.data.RegionTypeEnum;
import ru.yandex.wmtools.common.data.info.HostRegionInfo;
import ru.yandex.wmtools.common.data.info.RegionInfo;
import ru.yandex.wmtools.common.error.ExtraTagInfo;
import ru.yandex.wmtools.common.error.ExtraTagNameEnum;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.error.UserProblem;
import ru.yandex.wmtools.common.service.RegionsTreeCacheService;
import ru.yandex.wmtools.common.sita.UserAgentEnum;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author avhaliullin
 */
public class NewWebmasterProxyService {
    private static final Logger log = LoggerFactory.getLogger(NewWebmasterProxyService.class);

    private static final ObjectMapper OM = new ObjectMapper();
    private static final String PATH_BASE = "/old-webmaster/api/";
    private static final DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");

    private final CloseableHttpClient httpClient;

    private RegionsTreeCacheService regionsTreeCacheService;
    private String webmasterUrl;
    private boolean readOnly;

    public NewWebmasterProxyService() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(2000)
                .setConnectTimeout(500)
                .setSocketTimeout(5000)
                .build();

        httpClient = HttpClients.custom()
                .setMaxConnTotal(16)
                .setMaxConnPerRoute(16)
                .setConnectionTimeToLive(10, TimeUnit.SECONDS)
                .setDefaultRequestConfig(requestConfig)
                .setUserAgent(UserAgentEnum.WEBMASTER.getValue())
                .build();
    }

    public void changeHostNameInfo(long userId, BriefHostInfo hostInfo, String displayName) throws InternalException, UserException {
        if (readOnly) {
            log.debug("New wm proxy: read only mode enabled");
            return;
        }
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "changeHostDisplayName.json?" +
                "hostId=" + hostInfo2HostIdString(hostInfo) +
                "&userId=" + userId +
                "&displayName=" + urlencode(displayName)
        );

        WebmasterResponse response = doRequest(request);

        if (!response.errors.isEmpty()) {
            for (NewWebmasterError error : response.errors) {
                switch (error.code) {
                    case "CHANGE_DISPLAY_NAME__IDN_HOSTS_ARE_NOT_SUPPORTED":
                        throw new UserException(WMCUserProblem.IDN_ARE_NOT_SUPPORTED,
                                "Can't change display name of a host with IDN: " + hostInfo.getName());
                    case "CHANGE_DISPLAY_NAME__INVALID_DISPLAY_NAME":
                        throw new UserException(WMCUserProblem.INVALID_DISPLAY_NAME,
                                "Specified hostname " + displayName + " hasn't passed name check");
                    case "CHANGE_DISPLAY_NAME__NON_MATCHING":
                        throw new UserException(WMCUserProblem.NOT_MATCHING_DISPLAY_NAME,
                                "Specified hostname " + displayName + " has wrong protocol");
                }
            }
            throw errorResponse(response.errors);
        }
    }

    public HostNameInfoResponse getHostNameInfo(BriefHostInfo hostInfo) throws InternalException {
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "hostDisplayNameInfo.json?hostId=" + hostInfo2HostIdString(hostInfo));
        WebmasterResponse response = doRequest(request);

        if (!response.errors.isEmpty()) {
            for (NewWebmasterError error : response.errors) {
                if (error.code.equals("DISPLAY_NAME_INFO__NO_DATA_FOR_HOST")) {
                    return null;
                }
            }
            throw errorResponse(response.errors);
        }

        if (response.data == null || !(response.data instanceof ObjectNode)) {
            throw unexpectedResponse(request, "data expected to be object, but found: " + String.valueOf(response.data));
        }

        ObjectNode rootObject = (ObjectNode) response.data;
        String displayName = rootObject.get("displayName").asText();
        DisplayNameModerationStateEnum state;
        String stateName = rootObject.get("state").asText();
        switch (stateName) {
            case "IN_PROGRESS":
                state = DisplayNameModerationStateEnum.IN_PROGRESS;
                break;
            case "ACCEPTED":
                state = DisplayNameModerationStateEnum.ACCEPTED;
                break;
            case "REFUSED":
                state = DisplayNameModerationStateEnum.REFUSED;
                break;
            case "CANCELLED":
                return null;
            default:
                throw unexpectedResponse(request, "Unknown host display name request state: " + stateName);
        }
        return new HostNameInfoResponse(displayName, state);
    }

    public void changeHostRegion(long userId, BriefHostInfo hostInfo, int regionId, URL evidenceUrl) throws InternalException, UserException {
        if (readOnly) {
            log.debug("New wm proxy: read only mode enabled");
            return;
        }
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "changeHostRegion.json?" +
                "hostId=" + hostInfo2HostIdString(hostInfo) +
                "&userId=" + userId +
                "&regionId=" + regionId +
                (evidenceUrl == null ? "" : "&evidence=" + urlencode(evidenceUrl.toExternalForm()))
        );

        WebmasterResponse response = doRequest(request);

        if (!response.errors.isEmpty()) {
            for (NewWebmasterError error : response.errors) {
                if (error.code.equals("CHANGE_HOST_REGION__INVALID_EVIDENCE")) {
                    throw new UserException(UserProblem.WRONG_HOSTNAME_USED, "URL must be from host: " + hostInfo.getName(),
                            new ExtraTagInfo(ExtraTagNameEnum.EXPECTED, hostInfo.getName()),
                            new ExtraTagInfo(ExtraTagNameEnum.GIVEN, evidenceUrl == null ? null : evidenceUrl.getAuthority()));
                }
            }
            throw errorResponse(response.errors);
        }
    }

    public RegionInfoResponse getHostRegionInfo(BriefHostInfo hostInfo) throws InternalException {
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "hostRegionsInfo.json?hostId=" + hostInfo2HostIdString(hostInfo));
        WebmasterResponse response = doRequest(request);
        if (!response.errors.isEmpty()) {
            for (NewWebmasterError error : response.errors) {
                if (error.code.equals("HOST_REGIONS_INFO__NO_REGIONS_INFO")) {
                    return null;
                }
            }
            throw errorResponse(response.errors);
        }
        if (response.data == null || !(response.data instanceof ObjectNode)) {
            throw unexpectedResponse(request, "data expected to be object, but found: " + String.valueOf(response.data));
        }
        ObjectNode dataObject = (ObjectNode) response.data;
        HostRegionInfo pendingRegion = getRegionFromNode(dataObject.get("pendingRegion"));
        HostRegionInfo webmasterRegion = getRegionFromNode(dataObject.get("webmasterRegion"));
        JsonNode moderationInfo = dataObject.get("moderationInfo");

        UserHostRegionInfo userHostRegionInfo = null;
        if (moderationInfo != null && !moderationInfo.isNull()) {
            boolean userClosed = moderationInfo.get("userClosed").asBoolean();
            String stateName = moderationInfo.get("status").asText();
            HostRegionChangeRequestStateEnum state;
            switch (stateName) {
                case "ACCEPTED":
                    state = HostRegionChangeRequestStateEnum.VERIFIED;
                    break;
                case "REFUSED":
                    state = HostRegionChangeRequestStateEnum.DENIED;
                    break;
                case "IN_MODERATION":
                    state = HostRegionChangeRequestStateEnum.WAITING;
                    break;
                case "CANCELLED":
                    return null;
                case "OVERRIDDEN":
                    state = HostRegionChangeRequestStateEnum.MODERATOR_ISSUED;
                    break;
                default:
                    throw unexpectedResponse(request, "Unknown host region request state: " + stateName);
            }
            HostRegionInfo moderatedRegion = getRegionFromNode(moderationInfo.get("regionId"));
            String updateDateString = moderationInfo.get("updateDate").asText();
            Date updateDate;
            try {
                updateDate = DATE_TIME_FORMAT.parse(updateDateString);
            } catch (ParseException e) {
                throw unexpectedResponse(request, "Date in unexpected format: " + updateDateString, e);
            }
            userHostRegionInfo = new UserHostRegionInfo(
                    hostInfo.getId(),
                    hostInfo.getName(),
                    0L, // не используется в верстке
                    moderatedRegion,
                    state,
                    updateDate,
                    updateDate,
                    null, // не используется в верстке
                    userClosed,
                    null, // не используется в верстке
                    null // не используется в верстке
            );
        }
        return new RegionInfoResponse(userHostRegionInfo, pendingRegion, webmasterRegion);
    }

    @Deprecated
    public
    @Nullable
    RecheckInfo getVirusRecheckInfo(long userId, BriefHostInfo hostInfo) throws InternalException {
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "virusRecheckInfo.json?" +
                "userId=" + userId + "&hostId=" + hostInfo2HostIdString(hostInfo));
        WebmasterResponse response = doRequest(request);
        if (!response.errors.isEmpty()) {
            throw errorResponse(response.errors);
        }
        return parseVirusRecheckInfo(request, response.data);
    }

    @Deprecated
    public
    @Nullable
    RecheckInfo recheckVirus(long userId, BriefHostInfo hostInfo) throws InternalException {
        if (readOnly) {
            log.debug("New wm proxy: read only mode enabled");
            return null;
        }
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "recheckVirus.json?" +
                "userId=" + userId + "&hostId=" + hostInfo2HostIdString(hostInfo));
        WebmasterResponse response = doRequest(request);
        if (!response.errors.isEmpty()) {
            throw errorResponse(response.errors);
        }
        return parseVirusRecheckInfo(request, response.data);
    }

    private RecheckInfo parseVirusRecheckInfo(HttpRequest request, JsonNode dataObject) throws InternalException {
        if (dataObject == null || !(dataObject instanceof ObjectNode)) {
            throw unexpectedResponse(request, "data expected to be object, but found: " + String.valueOf(dataObject));
        }
        JsonNode recheckRequest = dataObject.get("request");
        if (recheckRequest == null || recheckRequest.isNull()) {
            return null;
        }
        JsonNode timeOfRequest = recheckRequest.get("timeOfRequest");
        if (timeOfRequest == null || timeOfRequest.isNull()) {
            return null;
        }
        return new RecheckInfo(VirusRecheckEnum.REQUEST, new DateTime(timeOfRequest.asText()).toDate());
    }

    private HostRegionInfo getRegionFromNode(JsonNode node) throws InternalException {
        if (node == null || node.isNull()) {
            return null;
        }
        int regionId = node.asInt();
        RegionInfo info = regionsTreeCacheService.getRegionInfo(regionId);
        if (info == null) {
            info = RegionInfo.zeroRegion;
        }
        return new HostRegionInfo(info, RegionTypeEnum.WMCONSOLE, true);
    }

    private InternalException errorResponse(List<NewWebmasterError> errors) {
        StringBuilder sb = new StringBuilder();
        for (NewWebmasterError error : errors) {
            sb.append("{code: ").append(error.code).append(", message: ").append(String.valueOf(error.message)).append("};");
        }
        return new InternalException(InternalProblem.PROCESSING_ERROR, "New webmaster backend responded with error: " + sb.toString());
    }

    private WebmasterResponse doRequest(HttpUriRequest request) throws InternalException {
        try (CloseableHttpResponse response = httpClient.execute(request)) {
            HttpEntity entity = response.getEntity();
            if (entity == null) {
                throw unexpectedResponse(request, "Response entity not found");
            }
            JsonNode root = OM.readTree(entity.getContent());
            if (!(root instanceof ObjectNode)) {
                throw unexpectedResponse(request, "Root node expected to be object, but found " + root);
            }
            ObjectNode rootObject = (ObjectNode) root;
            JsonNode errors = rootObject.get("errors");

            List<NewWebmasterError> errorsList = new ArrayList<>();
            if (errors != null && errors.isArray()) {
                for (JsonNode errNode : errors) {
                    String code = errNode.get("code").textValue();
                    JsonNode messageNode = errNode.get("message");
                    String message = null;
                    if (messageNode != null && !messageNode.isNull()) {
                        message = messageNode.textValue();
                    }
                    errorsList.add(new NewWebmasterError(code, message));
                }
            }
            return new WebmasterResponse(errorsList, rootObject.get("data"));
        } catch (IOException e) {
            throw new InternalException(InternalProblem.CONNECTION_PROBLEM,
                    "New webmaster backend request failed: " + request.toString(), e);
        }
    }

    public void addSitemap(BriefHostInfo hostInfo, long userId, String url) throws InternalException {
        if (readOnly) {
            log.debug("New wm proxy: read only mode enabled");
            return;
        }
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "addSitemap.json?" +
                "hostId=" + hostInfo2HostIdString(hostInfo) +
                "&userId=" + userId +
                "&sitemapUrl=" + urlencode(url));
        WebmasterResponse response = doRequest(request);
        if (!response.errors.isEmpty()) {
            throw errorResponse(response.errors);
        }
    }

    public void removeSitemap(BriefHostInfo hostInfo, String sitemapUrl) throws InternalException {
        if (readOnly) {
            log.debug("New wm proxy: read only mode enabled");
            return;
        }
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "removeSitemap.json?" +
                "hostId=" + hostInfo2HostIdString(hostInfo) +
                "&sitemapUrl=" + sitemapUrl);
        WebmasterResponse response = doRequest(request);
        if (!response.errors.isEmpty()) {
            throw errorResponse(response.errors);
        }
    }

    public HostVerificationInfo addHost(long userId, BriefHostInfo hostInfo) throws InternalException {
        if (readOnly) {
            throw new RuntimeException("Read only mode");
        }
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "host/add.json?" +
                "hostId=" + hostInfo2HostIdString(hostInfo) +
                "&userId=" + userId);
        WebmasterResponse response = doRequest(request);
        return getHostVerficationInfo(response);
    }

    public void deleteHost(long userId, BriefHostInfo hostInfo) throws InternalException {
        if (readOnly) {
            log.debug("New wm proxy: read only mode enabled");
            return;
        }
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "host/delete.json?" +
                "hostId=" + hostInfo2HostIdString(hostInfo) +
                "&userId=" + userId);
        WebmasterResponse response = doRequest(request);
        if (!response.errors.isEmpty()) {
            throw errorResponse(response.errors);
        }
    }

    public HostVerificationInfo verifyHost(long userId, BriefHostInfo hostInfo, long uin, VerificationTypeEnum type) throws InternalException {
        if (readOnly) {
            throw new RuntimeException("Read only mode");
        }
        HttpGet request = new HttpGet(webmasterUrl + PATH_BASE + "host/verify.json?" +
                "hostId=" + hostInfo2HostIdString(hostInfo) +
                "&userId=" + userId +
                "&type=" + type.name() +
                "&uin=" + uin
        );
        WebmasterResponse response = doRequest(request);
        return getHostVerficationInfo(response);
    }

    public static class HostNameInfoResponse {
        public final String displayName;
        public final DisplayNameModerationStateEnum state;

        public HostNameInfoResponse(String displayName, DisplayNameModerationStateEnum state) {
            this.displayName = displayName;
            this.state = state;
        }
    }

    public static class RegionInfoResponse {
        public final UserHostRegionInfo userHostRegionInfo;
        public final HostRegionInfo pendingRegion;
        public final HostRegionInfo webmasterRegion;

        public RegionInfoResponse(UserHostRegionInfo userHostRegionInfo, HostRegionInfo pendingRegion, HostRegionInfo webmasterRegion) {
            this.userHostRegionInfo = userHostRegionInfo;
            this.pendingRegion = pendingRegion;
            this.webmasterRegion = webmasterRegion;
        }
    }

    private static class WebmasterResponse {
        public final List<NewWebmasterError> errors;
        public final JsonNode data;

        public WebmasterResponse(List<NewWebmasterError> errors, JsonNode data) {
            this.errors = errors;
            this.data = data;
        }
    }

    private static class NewWebmasterError {
        public final String code;
        public final String message;

        public NewWebmasterError(String code, String message) {
            this.code = code;
            this.message = message;
        }
    }


    private HostVerificationInfo getHostVerficationInfo(WebmasterResponse response) throws InternalException {
        if (!response.errors.isEmpty()) {
            throw errorResponse(response.errors);
        }
        JsonNode data = response.data;
//        if (data.has("error")){
//            CannotStartVerificationReason error = CannotStartVerificationReason.valueOf(data.get("error").asText());
//            switch (error){
//                case NOT_APPLICABLE:
//                    throw new ClientException(ClientProblem.)
//            }
//        }
        VerificationStateEnum state = null;
        VerificationTypeEnum type = null;
        Long uin = null;
        Date date = null;

        if (data.has("state")) {
            state = VerificationStateEnum.valueOf(data.get("state").asText());
        }
        if (data.has("type")) {
            type = VerificationTypeEnum.valueOf(data.get("type").asText());
        }
        if (data.has("uin")) {
            uin = data.get("uin").asLong();
        }
        if (data.has("date")) {
            date = new Date(data.get("date").asLong());
        }
        return new HostVerificationInfo(state, type, uin, date);
    }

    private InternalException unexpectedResponse(HttpRequest request, String message, Throwable t) {
        return new InternalException(InternalProblem.PROCESSING_ERROR, "Unexpected webmaster response for request " + request.toString() + ". " + message, t);
    }

    private InternalException unexpectedResponse(HttpRequest request, String message) {
        return unexpectedResponse(request, message, null);
    }

    private String urlencode(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Should never happen", e);
        }
    }

    private String hostInfo2HostIdString(BriefHostInfo hostInfo) throws InternalException {
        return urlencode(BackportedIdUtils.urlToHostId(hostInfo.getName()).toStringId());
    }

    @Required
    public void setReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
    }

    @Required
    public void setWebmasterUrl(String webmasterUrl) {
        this.webmasterUrl = webmasterUrl;
    }

    @Required
    public void setRegionsTreeCacheService(RegionsTreeCacheService regionsTreeCacheService) {
        this.regionsTreeCacheService = regionsTreeCacheService;
    }
}
