package io.hawt.quarkus.filters;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;

import javax.security.auth.Subject;

import io.hawt.web.ForbiddenReason;
import io.quarkus.arc.InstanceHandle;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import io.hawt.quarkus.auth.HawtioQuarkusAuthenticator;
import io.hawt.system.AuthenticateResult;
import io.hawt.system.Authenticator;
import io.hawt.web.ServletHelpers;
import io.hawt.web.auth.AuthSessionHelpers;
import io.hawt.web.auth.AuthenticationFilter;
import io.quarkus.arc.Arc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Jolokia endpoint authentication filter for Quarkus.
 */
public class HawtioQuarkusAuthenticationFilter extends AuthenticationFilter {

    private static final Logger LOG = LoggerFactory.getLogger(HawtioQuarkusAuthenticationFilter.class);

    private InstanceHandle<HawtioQuarkusAuthenticator> handle;
    private HawtioQuarkusAuthenticator authenticator;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        handle = Arc.container().instance(HawtioQuarkusAuthenticator.class);
        authenticator = handle.get();
        super.init(filterConfig);
    }

    @Override
    public void destroy() {
        if (handle != null) {
            handle.close();
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        LOG.trace("Applying {}", getClass().getSimpleName());

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // CORS preflight requests should be ignored
        if ("OPTIONS".equals(httpRequest.getMethod())) {
            chain.doFilter(request, response);
            return;
        }

        String path = httpRequest.getServletPath();

        LOG.debug("Handling request for path: {}", path);

        // from here the flow differs from the base class (no JAAS)

        // TODO: is isKeycloakEnabled() valid here?
        if (!authConfiguration.isEnabled() || authConfiguration.isKeycloakEnabled()) {
            LOG.debug("No authentication needed for path: {}", path);
            chain.doFilter(request, response);
            return;
        }

        ProxyRequestType proxyMode = isProxyMode(httpRequest);

        if (proxyMode == ProxyRequestType.PROXY_ENABLED) {
            chain.doFilter(request, response);
            return;
        }

        HttpSession session = httpRequest.getSession(false);

        if (proxyMode == ProxyRequestType.PROXY && session == null) {
            if (!authConfiguration.isExternalAuthenticationEnabled()) {
                // simple - we need a session, we don't have one
                // we reject proxy requests without session, because Authorization header is targeted at remote Jolokia
                ServletHelpers.doForbidden(httpResponse, ForbiddenReason.SESSION_EXPIRED);
                return;
            }
        }

        if (session != null) {
            Subject subject = (Subject) session.getAttribute("subject");

            // When user is authenticated in Hawtio (has session) and uses /proxy, we can't match
            // current subject/name (from session) with Authorization header as this one is for remote Jolokia
            // we only require a subject to be present in session
            if (proxyMode == ProxyRequestType.PROXY) {
                if (subject != null) {
                    chain.doFilter(request, response);
                } else {
                    ServletHelpers.doForbidden(httpResponse);
                }
                return;
            }

            // Connecting from another Hawtio may have a different user authentication, so
            // let's check if the session user is the same as in the authorization header here
            if (AuthSessionHelpers.validate(httpRequest, session, subject)) {
                chain.doFilter(request, response);
                return;
            }
        }

        LOG.debug("Doing authentication and authorization for path: {}", path);

        // Quarkus authentication
        AtomicReference<String> username = new AtomicReference<>();
        AtomicReference<String> password = new AtomicReference<>();
        Authenticator.extractAuthHeader(httpRequest, (u, p) -> {
            username.set(u);
            password.set(p);
        });
        AuthenticateResult result = authenticator.authenticate(authConfiguration, username.get(), password.get());

        switch (result.getType()) {
        case AUTHORIZED:
            chain.doFilter(request, response);
            break;
        case NOT_AUTHORIZED:
            ServletHelpers.doForbidden(httpResponse);
            break;
        case NO_CREDENTIALS:
            if (authConfiguration.isNoCredentials401()) {
                // return auth prompt 401
                ServletHelpers.doAuthPrompt(httpResponse, authConfiguration.getRealm());
            } else {
                // return forbidden 403 so the browser login does not popup
                ServletHelpers.doForbidden(httpResponse);
            }
            break;
        case THROTTLED:
            ServletHelpers.doTooManyRequests(httpResponse, result.getRetryAfter());
            break;
        }
    }
}
