package ru.yandex.wmtools.common.sita;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedList;

import com.codahale.metrics.MetricRegistry;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.client.utils.URIUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.UserException;
import ru.yandex.wmtools.common.util.http.YandexHttpStatus;

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

    private SitaService sitaService;
    private MetricRegistry metricRegistry;

    public SitaUrlFetchResponse followRedirects(SitaUrlFetchRequest urlFetchRequest) throws InternalException,
            UserException
    {
        return followRedirects(urlFetchRequest, RedirectConfig.DEFAULT_REDIRECT_CONFIG);
    }

    public SitaUrlFetchResponse followRedirects(SitaUrlFetchRequest urlFetchRequest, RedirectConfig redirectConfig)
            throws InternalException, UserException
    {
        return followRedirects(urlFetchRequest, redirectConfig, RedirectListener.DEFAULT_LISTENER);
    }

    public SitaUrlFetchResponse followRedirects(SitaUrlFetchRequest urlFetchRequest, RedirectConfig redirectConfig,
            RedirectListener redirectListener) throws InternalException, UserException
    {
        if(urlFetchRequest.getDocumentFormat() != DocumentFormatEnum.DF_HTTP_RESPONSE) {
            throw new SitaException("Request does not ask for http content but redirect requires http response headers");
        }
        SitaUrlFetchRequest request = urlFetchRequest;
        SitaUrlFetchResponse response = null;
        LinkedList<URI> redirects = new LinkedList<URI>();
        redirects.add(urlFetchRequest.getUri());
        try {
            for (int i = 0; i < redirectConfig.getMaxRedirects(); i++) {
                response = sitaService.request(request);
                if(!isRedirect(response)) {
                    break;
                }
                URI newUri = getRedirectUrl(urlFetchRequest, response, redirectConfig);
                if (redirects.contains(newUri)) {
                    throw new SitaRedirectCycleException("Circular redirect", newUri);
                }
                redirects.add(newUri);
                log.debug("Follow redirect: {} -> {}", request.getUri(), newUri);
                redirectListener.beforeRedirect(request.getUri(), newUri);
                request = request.withUrl(newUri);
            }
        } finally {
            if (redirects.size() > 1) {
                metricRegistry.meter("monitoring.sita.redirectingCalls").mark();
                metricRegistry.histogram("monitoring.sita.redirectsPerCall").update(redirects.size());
            }
        }
        redirectListener.lastTarget(request.getUri());
        if (response != null) {
            response.setSitaRedirectInfo(new SitaRedirectInfo(redirects));
        }
        return response;
    }

    public SitaUrlFetchExtendedResponse followRedirectsExtended(SitaUrlFetchRequest urlFetchRequest,
            boolean retainJsonRequestResponse) throws InternalException, UserException
    {
        return followRedirectsExtended(urlFetchRequest, RedirectConfig.DEFAULT_REDIRECT_CONFIG,
                retainJsonRequestResponse);
    }

    public SitaUrlFetchExtendedResponse followRedirectsExtended(SitaUrlFetchRequest urlFetchRequest,
            RedirectConfig redirectConfig, boolean retainJsonRequestResponse)
            throws InternalException, UserException
    {
        return followRedirectsExtended(urlFetchRequest, redirectConfig, RedirectListener.DEFAULT_LISTENER,
                retainJsonRequestResponse);
    }

    public SitaUrlFetchExtendedResponse followRedirectsExtended(SitaUrlFetchRequest urlFetchRequest,
            RedirectConfig redirectConfig, RedirectListener redirectListener, boolean retainJsonRequestResponse)
            throws InternalException, UserException
    {
        if(urlFetchRequest.getDocumentFormat() != DocumentFormatEnum.DF_HTTP_RESPONSE) {
            throw new SitaException("Request does not ask for http content but redirect requires http response headers");
        }
        SitaUrlFetchRequest request = urlFetchRequest;
        SitaUrlFetchExtendedResponse response = null;
        LinkedList<URI> redirects = new LinkedList<URI>();
        redirects.add(urlFetchRequest.getUri());
        try {
            for (int i = 0; i < redirectConfig.getMaxRedirects(); i++) {
                response = sitaService.requestExtended(request, retainJsonRequestResponse);
                if (response.getSitaResponseError() != SitaUrlFetchExtendedResponse.SitaResponseError.NO_ERROR) {
                    break;
                }
                if(!isRedirect(response)) {
                    break;
                }
                URI newUri = getRedirectUrl(urlFetchRequest, response, redirectConfig);
                if (redirects.contains(newUri)) {
                    throw new SitaRedirectCycleException("Circular redirect", newUri);
                }
                redirects.add(newUri);
                log.debug("Follow redirect: {} -> {}", request.getUri(), newUri);
                redirectListener.beforeRedirect(request.getUri(), newUri);
                request = request.withUrl(newUri);
            }
        } finally {
            if (redirects.size() > 1) {
                metricRegistry.meter("monitoring.sita.redirectingCalls").mark();
                metricRegistry.histogram("monitoring.sita.redirectsPerCall").update(redirects.size());
            }
        }
        redirectListener.lastTarget(request.getUri());
        if (response != null) {
            response.setSitaRedirectInfo(new SitaRedirectInfo(redirects));
        }
        return response;
    }

    URI getRedirectUrl(SitaUrlFetchRequest request, SitaUrlFetchResponse response, RedirectConfig redirectConfig) {
        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.getParsedHttpHeaders().getFirstHeader("location");
        if (location == null || StringUtils.isEmpty(location.getValue())) {
            throw new SitaException("Response does not have Location header");
        }
        URI originalUri = request.getUri();

        // fix: WMCON-5098
        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 (redirectConfig.isRequireAbsoluteLocation()) {
                    throw new SitaException("Redirect location should be absolute URI: " + newUri);
                }
                newUri = URIUtils.resolve(originalUri, newUri);
            }
        } catch (URISyntaxException e) {
            throw new SitaException("Unable to parse redirect location", e);
        }

        if (redirectConfig.isRequireSameOrigin()) {
            if (!originalUri.getScheme().equals(newUri.getScheme())) {
                throw new SitaException(
                        "Redirect should have same origin, but schema mismatch: from=" + originalUri + ", to=" + newUri);
            }
            if (!originalUri.getHost().equals(newUri.getHost())) {
                throw new SitaException(
                        "Redirect should have same origin, but host mismatch: from=" + originalUri + ", to=" + newUri);
            }
            if (originalUri.getPort() != newUri.getPort()) {
                throw new SitaException(
                        "Redirect should have same origin, but port mismatch: from=" + originalUri + ", to=" + newUri);
            }
        }
        return newUri;
    }

    private boolean isRedirect(SitaUrlFetchResponse urlFetchResponse) {
        if (urlFetchResponse.isAllowedInRobotsTxt() != null && !urlFetchResponse.isAllowedInRobotsTxt()) {
            return false;
        }
        YandexHttpStatus sitaHttpStatus = urlFetchResponse.getSitaHttpStatus();
        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;
        }
    }

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

    @Required
    public void setMetricRegistry(MetricRegistry metricRegistry) {
        this.metricRegistry = metricRegistry;
    }
}
