package com.atlassian.stash.internal.request;

import com.atlassian.bitbucket.auth.AuthenticationContext;
import com.atlassian.bitbucket.experimental.request.ExperimentalRequestContext;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestInfoProvider;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.stash.internal.logback.LoggingConstants;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.*;

import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;

/**
 * Default implementation of the {@link RequestContext} interface.
 *
 * @since 1.2.2
 */
class DefaultRequestContext implements ExperimentalRequestContext {

    private static final Logger log = LoggerFactory.getLogger(DefaultRequestContext.class);

    private final AuthenticationContext authenticationContext;
    private final List<Runnable> cleanupCallbacks;
    private final RequestInfoProvider delegate;
    private final String id;
    private final SortedSet<String> labels;

    private volatile boolean active;
    //The user gets updated when its available in the AuthenticationContext
    private volatile ApplicationUser authenticatedUser;
    private volatile long bytesRead;
    private volatile long bytesWritten;
    private volatile Duration duration;

    DefaultRequestContext(AuthenticationContext authenticationContext, RequestInfoProvider delegate, String id) {
        this.authenticationContext = authenticationContext;
        this.delegate = delegate;
        this.id = id;

        active = true;
        cleanupCallbacks = new ArrayList<>();
        labels = new TreeSet<>();
    }

    @Override
    public void addCleanupCallback(@Nonnull Runnable callback) {
        cleanupCallbacks.add(callback);
    }

    @Override
    public void addLabel(@Nonnull String label) {
        labels.add(label);
        MDC.put(LoggingConstants.MDC_REQUEST_LABELS, StringUtils.join(labels, ", ").replace("|", "\\|"));
    }

    @Nonnull
    @Override
    public String getAction() {
        return delegate.getAction();
    }

    @Nonnull
    @Override
    public Optional<ApplicationUser> getAuthenticatedUser() {
        return ofNullable(authenticatedUser);
    }

    @Nonnull
    @Override
    public AuthenticationContext getAuthenticationContext() {
        return authenticationContext;
    }

    @Override
    public String getDetails() {
        return delegate.getDetails();
    }

    @Nonnull
    @Override
    public String getId() {
        return id;
    }

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

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

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

    @Override
    public String getRemoteAddress() {
        return delegate.getRemoteAddress();
    }

    @Override
    public String getSessionId() {
        return delegate.getSessionId();
    }

    @Override
    public boolean hasSessionId() {
        return delegate.hasSessionId();
    }

    @Override
    public boolean isActive() {
        return active;
    }

    @Override
    public void setAuthenticatedUser(@Nonnull ApplicationUser user) {
        authenticatedUser = requireNonNull(user, "user");
    }

    @Override
    public long getBytesRead() {
        return bytesRead;
    }

    @Override
    public void setBytesRead(long bytesRead) {
        MDC.put(LoggingConstants.MDC_REQUEST_BYTES_READ, Long.toString(bytesRead));
        this.bytesRead = bytesRead;
    }

    @Override
    public long getBytesWritten() {
        return bytesWritten;
    }

    @Override
    public void setBytesWritten(long bytesWritten) {
        MDC.put(LoggingConstants.MDC_RESPONSE_BYTES_WRITTEN, Long.toString(bytesWritten));
        this.bytesWritten = bytesWritten;
    }

    @Override
    @Nonnull
    public Optional<Duration> getDuration() {
        return ofNullable(duration);
    }

    public void setDuration(@Nonnull Duration duration) {
        this.duration = requireNonNull(duration, "duration");
    }

    @Override
    public void setResponseCode(int responseCode) {
        MDC.put(LoggingConstants.MDC_STATUS_CODE, Integer.toString(responseCode));
    }

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

    public void runCleanupCallbacks() {
        for (Runnable callback : cleanupCallbacks) {
            try {
                callback.run();
            } catch (Exception e) {
                log.warn("Request cleanup callback failed", e);
            }
        }
        active = false;
    }
}
