package ru.yandex.webmaster3.storage.verification;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedList;
import java.util.Set;
import java.util.stream.Collectors;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.client.utils.URIUtils;

import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.host.verification.PageUnavailableReason;
import ru.yandex.webmaster3.core.host.verification.VerificationFailInfo;
import ru.yandex.webmaster3.core.host.verification.VerificationHttpResponseInfo;
import ru.yandex.webmaster3.core.host.verification.fail.PageUnavailableFailInfo;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.util.Either;
import ru.yandex.webmaster3.core.util.WwwUtil;
import ru.yandex.webmaster3.core.zora.GoZoraService;
import ru.yandex.webmaster3.core.zora.ZoraConversionUtil;
import ru.yandex.webmaster3.core.zora.data.response.ZoraUrlFetchResponse;
import ru.yandex.webmaster3.core.zora.go_data.request.GoZoraRequest;
import ru.yandex.wmtools.common.sita.SitaException;
import ru.yandex.wmtools.common.sita.SitaRedirectInfo;
import ru.yandex.wmtools.common.util.http.YandexHttpStatus;

/**
 * @author avhaliullin
 */
@Slf4j
public class VerificationPageDownloadService {

    @Setter
    private int socketTimeoutMillis;

    @Setter
    private GoZoraService goZoraService;

    public Either<ZoraUrlFetchResponse, VerificationFailInfo> getVerificationPage(URL urlAddress, int maxRedirects, Set<RedirectResolveFeature> redirectFeatures) {
        ZoraUrlFetchResponse response = null;

        GoZoraRequest request = GoZoraRequest.of(urlAddress.toString());
        // Inlined redirects traversing logic in order to get full redirects chain in case of redirect loop (SitaRedirectCycleException doesn't provide this information)
        {
            LinkedList<URI> redirects = new LinkedList<>();
            redirects.add(request.getUri());
            for (int i = 0; i < maxRedirects + 1; i++) {
                try {
                    response = ZoraConversionUtil.toUrlFetchResponse(goZoraService.executeRequestFetchResponse(request));
                } catch (Exception e) {
                    throw new WebmasterException("Failed to download verification document for url " + request.getUrl(),
                            new WebmasterErrorResponse.SitaErrorResponse(getClass(), e), e);
                }
                if (!isRedirect(response)) {
                    break;
                }
                URI newUri = getRedirectUrl(request.getUri(), response, redirectFeatures);
                if (newUri == null) {
                    break;
                }
                if (redirects.contains(newUri)) {
                    return Either.right(
                            new PageUnavailableFailInfo(
                                    urlAddress,
                                    new VerificationHttpResponseInfo(
                                            null,
                                            redirects.stream().map(Object::toString).collect(Collectors.toList()),
                                            false
                                    ),
                                    PageUnavailableReason.REDIRECT_LOOP)
                    );
                }
                redirects.add(newUri);
                log.debug("Follow redirect: {} -> {}", request.getUrl(), newUri);
                request = request.withUrl(newUri.toString());
            }
            //suppress idea warning
            if (response == null) {
                throw new RuntimeException("Unexpected error: sita response is null for url " + urlAddress);
            }
            response.setSitaRedirectInfo(new SitaRedirectInfo(redirects));
            if (isRedirect(response)) {
                return Either.right(
                        new PageUnavailableFailInfo(
                                urlAddress,
                                VerificationHttpResponseInfo.createFromZoraResponse(response),
                                PageUnavailableReason.TOO_MANY_REDIRECTS
                        )
                );
            }
        }

        log.info("Zora status {} for url {}", response.getExtendedHttpStatus(), urlAddress);
        switch (response.getExtendedHttpStatus()) {
            case HTTP_200_OK:
            case HTTP_1003_ROBOTS_TXT_DISALLOW:
            case EXT_HTTP_2004_REFRESH:
            case EXT_HTTP_2005_NOINDEX:
            case EXT_HTTP_2014_EMPTYDOC:
                return Either.left(response);

            case HTTP_1001_CONNECTION_LOST:
            case HTTP_1010_CONNECT_FAILED:
            case HTTP_1033_PROXY_CONNECT_FAILED:
            case HTTP_1034_PROXY_CONNECTION_LOST:
                return Either.right(
                        new PageUnavailableFailInfo(
                                urlAddress,
                                VerificationHttpResponseInfo.createFromZoraResponse(response),
                                PageUnavailableReason.CONNECTION_ERROR
                        )
                );

            case HTTP_1006_DNS_FAILURE:
                return Either.right(
                        new PageUnavailableFailInfo(
                                urlAddress,
                                VerificationHttpResponseInfo.createFromZoraResponse(response),
                                PageUnavailableReason.DNS_ERROR
                        )
                );

            default:
                if (YandexHttpStatus.isStandardHttpCode(response.getExtendedHttpStatus())) {
                    return Either.right(
                            new PageUnavailableFailInfo(
                                    urlAddress,
                                    VerificationHttpResponseInfo.createFromZoraResponse(response),
                                    PageUnavailableReason.BAD_HTTP_STATUS
                            )
                    );
                } else {
                    return Either.right(
                            new PageUnavailableFailInfo(
                                    urlAddress,
                                    VerificationHttpResponseInfo.createFromZoraResponse(response),
                                    PageUnavailableReason.CONTENT_PROCESSING_ERROR
                            )
                    );
                }
        }
    }

    private boolean isRedirect(ZoraUrlFetchResponse urlFetchResponse) {
        if (urlFetchResponse.isAllowedInRobotsTxt() != null && !urlFetchResponse.isAllowedInRobotsTxt()) {
            return false;
        }
        YandexHttpStatus sitaHttpStatus = urlFetchResponse.getExtendedHttpStatus();
        switch (sitaHttpStatus) {
            case HTTP_301_MOVED_PERMANENTLY:
            case HTTP_302_FOUND:
            case HTTP_303_SEE_OTHER:
            case HTTP_307_TEMPORARY_REDIRECT:
                return true;

            default:
                return false;
        }
    }

    private URI getRedirectUrl(URI originalUri, ZoraUrlFetchResponse response, Set<RedirectResolveFeature> features) {
        if (!StringUtils.isEmpty(response.getRedirTarget())) {
            try {
                log.debug("Redirect from redirTarget: {}", response.getRedirTarget());
                return new URI(response.getRedirTarget());
            } catch (URISyntaxException e) {
                throw new SitaException("Unable to parse redirect location", e);
            }
        }
        if (!response.hasDocument()) {
            throw new SitaException("Response does not have http content");
        }
        Header location = response.getHttpResponse().getFirstHeader("location");
        if (location == null || StringUtils.isEmpty(location.getValue())) {
            throw new SitaException("Response does not have Location header");
        }

        if (StringUtils.isEmpty(originalUri.getPath())) {
            originalUri = originalUri.resolve("/");
        }

        URI newUri;
        try {
            newUri = new URI(location.getValue()).normalize();
            newUri = URIUtils.rewriteURI(newUri);
            if (!newUri.isAbsolute()) {
                if (!features.contains(RedirectResolveFeature.ALLOW_RELATIVE)) {
                    log.info("Redirect location should be absolute URI: " + newUri);
                    return null;
                }
                newUri = URIUtils.resolve(originalUri, newUri);
            }
        } catch (URISyntaxException e) {
            throw new SitaException("Unable to parse redirect location", e);
        }

        if (!features.contains(RedirectResolveFeature.ALLOW_OTHER_ORIGIN)) {
            if (!features.contains(RedirectResolveFeature.ALLOW_SWITCH_SCHEMA) && !originalUri.getScheme().equals(newUri.getScheme())) {
                log.info("Redirect should have same origin, but schema mismatch: from=" + originalUri + ", to=" + newUri);
                return null;
            }
            if (!originalUri.getHost().equals(newUri.getHost())) {
                if (!features.contains(RedirectResolveFeature.ALLOW_SWITCH_WWW) || !WwwUtil.equalsIgnoreWww(originalUri.getHost(), newUri.getHost())) {
                    log.info("Redirect should have same origin, but host mismatch: from=" + originalUri + ", to=" + newUri);
                    return null;
                }
            }
            if (!features.contains(RedirectResolveFeature.ALLOW_SWITCH_PORT) && originalUri.getPort() != newUri.getPort()) {
                log.info("Redirect should have same origin, but port mismatch: from=" + originalUri + ", to=" + newUri);
                return null;
            }
        }
        return newUri;
    }


}
