package com.atlassian.stash.internal.web;

import com.atlassian.security.random.SecureRandomFactory;
import com.atlassian.bitbucket.env.SystemProperties;
import com.atlassian.bitbucket.request.RequestInfoProvider;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.security.SecureRandom;
import java.util.zip.CRC32;

/**
 * A default implementation of the {@link HttpRequestInfoHelper}.
 * <p>
 * This implementation is loosely based on code in JIRA. Much of that code can be found in the
 * {@code com.atlassian.jira.web.filters.accesslog} package in jira-components/jira-core.
 * Specifically of interest are:
 * <ul>
 *     <li>AccessLogIPAddressUtil.java</li>
 *     <li>AccessLogRequestInfo.java</li>
 *     <li>AtlassianSessionIdUtil.java</li>
 * </ul>
 */
@Component("httpRequestInfoHelper")
public class DefaultHttpRequestInfoHelper implements HttpRequestInfoHelper {

    private static final Logger log = LoggerFactory.getLogger(DefaultHttpRequestInfoHelper.class);
    /**
     * Attribute set on a {@code HttpSession} containing the unique identifier generated for the session by
     * {@link #getSessionId(javax.servlet.http.HttpServletRequest)}, allowing the same ID to be returned repeatably without the
     * computational expense of regenerating it each time.
     */
    static final String ATTR_SESSION_ID = SystemProperties.PROP_PREFIX + ".session-id";

    private final SecureRandom secureRandom;

    /**
     * Constructs a new {@code DefaultHttpRequestInfoHelper} and initialises the {@code SecureRandom} generator which
     * will be used to create new session IDs.
     */
    public DefaultHttpRequestInfoHelper() {
        this.secureRandom = SecureRandomFactory.newInstance();
    }

    @Override
    public RequestInfoProvider createRequestInfoProvider(HttpServletRequest request, HttpServletResponse response) {
        return new HttpRequestInfoProvider(request, response);
    }

    /**
     * Examines the request for an "X-Forwarded-For" header and, if present, prepends that to the request's
     * {@code getRemoteAddr()} to produce a comma-separated list with addresses for the original client and
     * any proxies which have forwarded the request.
     *
     * @param request the working request
     * @return a comma-separated list containing 1 or more IP addresses
     */
    @Override
    public String getRemoteAddress(HttpServletRequest request) {
        StringBuilder builder = new StringBuilder();

        String header = request.getHeader("X-Forwarded-For");
        if (header != null) {
            //In the presence of X-Forwarded-For, the remote address is the final proxy in the chain, so we need both
            //the value of the header _and_ the remote address.
            builder.append(header).append(",");
        }
        builder.append(request.getRemoteAddr());

        //Remove all whitespace from the address string to make the log file more parseable
        return builder.toString().replaceAll("\\s", "");
    }

    /**
     * Returns the provided request's {@code getRequestURI()} with any context path removed.
     *
     * @param request the working request
     * @return the request URI without any context path
     */
    @Override
    public String getRequestUrl(HttpServletRequest request) {
        String url = request.getRequestURI();
        if (StringUtils.isNotBlank(request.getContextPath())) {
            url = url.substring(request.getContextPath().length());
        }
        return url;
    }

    /**
     * Uses the CRC32 of a randomly generated session ID to produce a unique identifier and sets it under
     * {@link #ATTR_SESSION_ID} on the session. Subsequent calls return the value of the attribute rather than
     * regenerating it (which is computationally expensive). If no session is available, {@code null} is returned.
     *
     * @param request the working request
     * @return a consistent unique identifier for the session, if any, associated with the provided request
     */
    @Override
    public String getSessionId(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return null;
        }

        String sessionId = null;
        try {
            sessionId = (String) session.getAttribute(ATTR_SESSION_ID);
            if (sessionId == null) {
                byte[] bytes = new byte[40];
                secureRandom.nextBytes(bytes);

                CRC32 crc = new CRC32();
                crc.update(bytes);

                sessionId = Long.toString(crc.getValue(), Character.MAX_RADIX);
                session.setAttribute(ATTR_SESSION_ID, sessionId);
            }
        } catch (IllegalStateException e) {
            // the session has been invalidated; we cannot access the attributes
            log.debug("Could not retrieve sessionId: {}", StringUtils.defaultString(e.getMessage()));
        }
        return sessionId;
    }

    private class HttpRequestInfoProvider implements RequestInfoProvider {

        private final HttpServletRequest request;
        private final HttpServletResponse response;

        private String action;
        private String requestDetails;
        private String sessionId;

        private HttpRequestInfoProvider(HttpServletRequest request, HttpServletResponse response) {
            this.request = request;
            this.response = response;
        }

        @Nonnull
        @Override
        public String getAction() {
            if (action == null) {
                action = "\"" + request.getMethod() + " " + getRequestUrl(request) + " " + request.getProtocol() + "\"";
            }
            return action;
        }

        @Override
        public String getDetails() {
            if (requestDetails == null) {
                requestDetails = String.format("\"%1$s\" \"%2$s\"",
                        StringUtils.defaultString(StringUtils.substringBefore(request.getHeader("referer"), "?")),
                        StringUtils.defaultString(request.getHeader("user-agent")));
            }
            return requestDetails;
        }

        @Nonnull
        @Override
        public String getProtocol() {
            return request.getScheme();
        }

        @Nonnull
        @Override
        public Object getRawRequest() {
            return request;
        }

        @Nonnull
        @Override
        public Object getRawResponse() {
            return response;
        }

        @Override
        public String getRemoteAddress() {
            return DefaultHttpRequestInfoHelper.this.getRemoteAddress(request);
        }

        @Override
        public String getSessionId() {
            if (sessionId == null) {
                sessionId = DefaultHttpRequestInfoHelper.this.getSessionId(request);
            }
            return sessionId;
        }

        @Override
        public boolean hasSessionId() {
            return getSessionId() != null;
        }

        @Override
        public boolean isSecure() {
            return request.isSecure();
        }
    }
}
