package org.keycloak.services.managers;

import java.util.Map;

import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.constants.KerberosConstants;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.flows.Flows;

/**
 * Handle HTTP authentication types requiring complex handshakes with multiple HTTP request/responses
 *
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
public class HttpAuthenticationManager {

    private static final Logger logger = Logger.getLogger(HttpAuthenticationManager.class);

    private KeycloakSession session;
    private RealmModel realm;
    private UriInfo uriInfo;
    private HttpRequest request;
    private EventBuilder event;
    private ClientConnection clientConnection;
    private ClientSessionModel clientSession;

    public HttpAuthenticationManager(KeycloakSession session, ClientSessionModel clientSession, RealmModel realm, UriInfo uriInfo,
                                     HttpRequest request,
                                     ClientConnection clientConnection,
                                     EventBuilder event) {
        this.session = session;
        this.realm = realm;
        this.uriInfo = uriInfo;
        this.request = request;
        this.event = event;
        this.clientConnection = clientConnection;
        this.clientSession = clientSession;
    }


    public HttpAuthOutput spnegoAuthenticate(HttpHeaders headers) {
        boolean kerberosSupported = false;
        for (RequiredCredentialModel c : realm.getRequiredCredentials()) {
            if (c.getType().equals(CredentialRepresentation.KERBEROS)) {
                kerberosSupported = true;
            }
        }

        if (logger.isTraceEnabled()) {
            String log = kerberosSupported ? "SPNEGO authentication is supported" : "SPNEGO authentication is not supported";
            logger.trace(log);
        }

        if (!kerberosSupported) {
            return new HttpAuthOutput(null, null);
        }

        String authHeader = request.getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);

        // Case when we don't yet have any Negotiate header
        if (authHeader == null) {
            return challengeNegotiation(null);
        }

        String[] tokens = authHeader.split(" ");
        if (tokens.length != 2) {
            logger.warn("Invalid length of tokens: " + tokens.length);
            return challengeNegotiation(null);
        } else if (!KerberosConstants.NEGOTIATE.equalsIgnoreCase(tokens[0])) {
            logger.warn("Unknown scheme " + tokens[0]);
            return challengeNegotiation(null);
        } else {
            String spnegoToken = tokens[1];
            UserCredentialModel spnegoCredential = UserCredentialModel.kerberos(spnegoToken);

            CredentialValidationOutput output = session.users().validCredentials(realm, spnegoCredential);

            if (output.getAuthStatus() == CredentialValidationOutput.Status.AUTHENTICATED) {
                return sendResponse(output.getAuthenticatedUser(), output.getState(), "spnego", headers);
            }  else {
                String spnegoResponseToken = (String) output.getState().get(KerberosConstants.RESPONSE_TOKEN);
                return challengeNegotiation(spnegoResponseToken);
            }
        }
    }


    // Send response after successful authentication
    private HttpAuthOutput sendResponse(UserModel user, Map<String, String> authState, String authMethod, HttpHeaders headers) {
        if (logger.isTraceEnabled()) {
            logger.trace("User " + user.getUsername() + " authenticated with " + authMethod);
        }

        Response response;
        if (!user.isEnabled()) {
            event.error(Errors.USER_DISABLED);
            response = Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.ACCOUNT_DISABLED);
        } else {
            UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), authMethod, false, null, null);

            // Propagate state (like kerberos delegation credentials etc) as attributes of userSession
            for (Map.Entry<String, String> entry : authState.entrySet()) {
                userSession.setNote(entry.getKey(), entry.getValue());
            }

            TokenManager.attachClientSession(userSession, clientSession);
            event.user(user)
                    .session(userSession)
                    .detail(Details.AUTH_METHOD, authMethod)
                    .detail(Details.USERNAME, user.getUsername());
            response = AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event);
        }

        return new HttpAuthOutput(response, null);
    }


    private HttpAuthOutput challengeNegotiation(final String negotiateToken) {
        return new HttpAuthOutput(null, new HttpAuthChallenge() {

            @Override
            public void sendChallenge(LoginFormsProvider loginFormsProvider) {
                String negotiateHeader = negotiateToken == null ? KerberosConstants.NEGOTIATE : KerberosConstants.NEGOTIATE + " " + negotiateToken;

                if (logger.isTraceEnabled()) {
                    logger.trace("Sending back " + HttpHeaders.WWW_AUTHENTICATE + ": " + negotiateHeader);
                }

                loginFormsProvider.setStatus(Response.Status.UNAUTHORIZED);
                loginFormsProvider.setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader);
            }

        });
    }


    public class HttpAuthOutput {

        // It's non-null if we want to immediately send response to user
        private final Response response;

        // It's non-null if challenge should be attached to rendered login form
        private final HttpAuthChallenge challenge;

        public HttpAuthOutput(Response response, HttpAuthChallenge challenge) {
            this.response = response;
            this.challenge = challenge;
        }

        public Response getResponse() {
            return response;
        }

        public HttpAuthChallenge getChallenge() {
            return challenge;
        }
    }


    public interface HttpAuthChallenge {

        void sendChallenge(LoginFormsProvider loginFormsProvider);

    }

}
