package org.keycloak.services.managers;

import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.Constants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.SkeletonKeyToken;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.resources.AccountService;
import org.keycloak.services.resources.admin.AdminService;
import org.keycloak.services.resources.RealmsResource;

import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Stateless object that manages authentication
 *
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class AuthenticationManager {
    protected Logger logger = Logger.getLogger(AuthenticationManager.class);
    public static final String FORM_USERNAME = "username";
    public static final String KEYCLOAK_IDENTITY_COOKIE = "KEYCLOAK_IDENTITY";

    public SkeletonKeyToken createIdentityToken(RealmModel realm, String username) {
        SkeletonKeyToken token = new SkeletonKeyToken();
        token.id(KeycloakModelUtils.generateId());
        token.issuedNow();
        token.principal(username);
        token.audience(realm.getName());
        if (realm.getTokenLifespan() > 0) {
            token.expiration((System.currentTimeMillis() / 1000) + realm.getTokenLifespan());
        }
        return token;
    }


    public NewCookie createLoginCookie(RealmModel realm, UserModel user, UriInfo uriInfo) {
        String cookieName = KEYCLOAK_IDENTITY_COOKIE;
        String cookiePath = getIdentityCookiePath(realm, uriInfo);
        return createLoginCookie(realm, user, null, cookieName, cookiePath);
    }

    public NewCookie createSaasIdentityCookie(RealmModel realm, UserModel user, UriInfo uriInfo) {
        String cookieName = AdminService.SAAS_IDENTITY_COOKIE;
        URI uri = AdminService.saasCookiePath(uriInfo).build();
        String cookiePath = uri.getRawPath();
        return createLoginCookie(realm, user, null, cookieName, cookiePath);
    }

    public NewCookie createAccountIdentityCookie(RealmModel realm, UserModel user, UserModel client, URI uri) {
        String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE;
        String cookiePath = uri.getRawPath();
        return createLoginCookie(realm, user, client, cookieName, cookiePath);
    }

    protected NewCookie createLoginCookie(RealmModel realm, UserModel user, UserModel client, String cookieName, String cookiePath) {
        SkeletonKeyToken identityToken = createIdentityToken(realm, user.getLoginName());
        if (client != null) {
            identityToken.issuedFor(client.getLoginName());
        }
        String encoded = encodeToken(realm, identityToken);
        boolean secureOnly = !realm.isSslNotRequired();
        logger.debug("creatingLoginCookie - name: {0} path: {1}", cookieName, cookiePath);
        NewCookie cookie = new NewCookie(cookieName, encoded, cookiePath, null, null, NewCookie.DEFAULT_MAX_AGE, secureOnly, true);
        return cookie;
    }

    protected String encodeToken(RealmModel realm, Object token) {
        String encodedToken = new JWSBuilder()
                .jsonContent(token)
                .rsa256(realm.getPrivateKey());
        return encodedToken;
    }


    public void expireIdentityCookie(RealmModel realm, UriInfo uriInfo) {
        logger.debug("Expiring identity cookie");
        String path = getIdentityCookiePath(realm, uriInfo);
        String cookieName = KEYCLOAK_IDENTITY_COOKIE;
        expireCookie(cookieName, path);
    }

    protected String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) {
        URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName());
        return uri.getRawPath();
    }

    public void expireSaasIdentityCookie(UriInfo uriInfo) {
        URI uri = AdminService.saasCookiePath(uriInfo).build();
        String cookiePath = uri.getRawPath();
        expireCookie(AdminService.SAAS_IDENTITY_COOKIE, cookiePath);
    }

    public void expireAccountIdentityCookie(URI uri) {
        String cookiePath = uri.getRawPath();
        expireCookie(AccountService.ACCOUNT_IDENTITY_COOKIE, cookiePath);
    }

    public void expireCookie(String cookieName, String path) {
        HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class);
        if (response == null) {
            logger.debug("can't expire identity cookie, no HttpResponse");
            return;
        }
        logger.debug("Expiring cookie: {0} path: {1}", cookieName, path);
        NewCookie expireIt = new NewCookie(cookieName, "", path, null, "Expiring cookie", 0, false);
        response.addNewCookie(expireIt);
    }

    public UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
        String cookieName = KEYCLOAK_IDENTITY_COOKIE;
        Auth auth = authenticateIdentityCookie(realm, uriInfo, headers, cookieName);
        return auth != null ? auth.getUser() : null;
    }

    public UserModel authenticateSaasIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
        String cookieName = AdminService.SAAS_IDENTITY_COOKIE;
        Auth auth = authenticateIdentityCookie(realm, uriInfo, headers, cookieName);
        return auth != null ? auth.getUser() : null;
    }

    public Auth authenticateAccountIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
        String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE;
        return authenticateIdentityCookie(realm, uriInfo, headers, cookieName);
    }

    public UserModel authenticateSaasIdentity(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
        UserModel user = authenticateSaasIdentityCookie(realm, uriInfo, headers);
        if (user != null) return user;

        Auth auth = authenticateBearerToken(realm, headers);
        return auth != null ? auth.getUser() : null;
    }

    public Auth authenticateAccountIdentity(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
        Auth auth = authenticateAccountIdentityCookie(realm, uriInfo, headers);
        if (auth != null) return auth;

        return authenticateBearerToken(realm, headers);
    }


    protected Auth authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String cookieName) {
        Cookie cookie = headers.getCookies().get(cookieName);
        if (cookie == null) {
            logger.debug("authenticateCookie could not find cookie: {0}", cookieName);
            return null;
        }

        String tokenString = cookie.getValue();
        try {
            SkeletonKeyToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName());
            if (!token.isActive()) {
                logger.debug("identity cookie expired");
                expireIdentityCookie(realm, uriInfo);
                return null;
            }

            Auth auth = new Auth(token);

            UserModel user = realm.getUser(token.getSubject());
            if (user == null || !user.isEnabled()) {
                logger.debug("Unknown user in identity cookie");
                expireIdentityCookie(realm, uriInfo);
                return null;
            }
            auth.setUser(user);

            if (token.getIssuedFor() != null) {
                UserModel client = realm.getUser(token.getIssuedFor());
                if (client == null || !client.isEnabled()) {
                    logger.debug("Unknown client in identity cookie");
                    expireIdentityCookie(realm, uriInfo);
                    return null;
                }
                auth.setClient(client);
            }

            return auth;
        } catch (VerificationException e) {
            logger.debug("Failed to verify identity cookie", e);
            expireCookie(cookie.getName(), cookie.getPath());
        }
        return null;
    }

    public Auth authenticateBearerToken(RealmModel realm, HttpHeaders headers) {
        String tokenString = null;
        String authHeader = headers.getHeaderString(HttpHeaders.AUTHORIZATION);
        if (authHeader == null) {
            return null;
        } else {
            String[] split = authHeader.trim().split("\\s+");
            if (split == null || split.length != 2) throw new NotAuthorizedException("Bearer");
            if (!split[0].equalsIgnoreCase("Bearer")) throw new NotAuthorizedException("Bearer");
            tokenString = split[1];
        }


        try {
            SkeletonKeyToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName());
            if (!token.isActive()) {
                throw new NotAuthorizedException("token_expired");
            }

            Auth auth = new Auth(token);

            UserModel user = realm.getUser(token.getSubject());
            if (user == null || !user.isEnabled()) {
                throw new NotAuthorizedException("invalid_user");
            }
            auth.setUser(user);

            if (token.getIssuedFor() != null) {
                UserModel client = realm.getUser(token.getIssuedFor());
                if (client == null || !client.isEnabled()) {
                    throw new NotAuthorizedException("invalid_user");
                }
                auth.setClient(client);
            }

            return auth;
        } catch (VerificationException e) {
            logger.error("Failed to verify token", e);
            throw new NotAuthorizedException("invalid_token");
        }
    }

    public AuthenticationStatus authenticateForm(RealmModel realm, UserModel user, MultivaluedMap<String, String> formData) {
        if (user == null) {
            logger.debug("Not Authenticated! Incorrect user name");
            return AuthenticationStatus.INVALID_USER;
        }

        if (!user.isEnabled()) {
            logger.debug("Account is disabled, contact admin. " + user.getLoginName());
            return AuthenticationStatus.ACCOUNT_DISABLED;
        }

        Set<String> types = new HashSet<String>();

        List<RequiredCredentialModel> requiredCredentials = null;
        RoleModel applicationRole = realm.getRole(Constants.APPLICATION_ROLE);
        RoleModel identityRequesterRole = realm.getRole(Constants.IDENTITY_REQUESTER_ROLE);
        if (realm.hasRole(user, applicationRole)) {
            requiredCredentials = realm.getRequiredApplicationCredentials();
        } else if (realm.hasRole(user, identityRequesterRole)) {
            requiredCredentials = realm.getRequiredOAuthClientCredentials();
        } else {
            requiredCredentials = realm.getRequiredCredentials();
        }

        for (RequiredCredentialModel credential : requiredCredentials) {
            types.add(credential.getType());
        }

        if (types.contains(CredentialRepresentation.PASSWORD)) {
            String password = formData.getFirst(CredentialRepresentation.PASSWORD);
            if (password == null) {
                logger.warn("Password not provided");
                return AuthenticationStatus.MISSING_PASSWORD;
            }

            if (user.isTotp()) {
                String token = formData.getFirst(CredentialRepresentation.TOTP);
                if (token == null) {
                    logger.warn("TOTP token not provided");
                    return AuthenticationStatus.MISSING_TOTP;
                }
                logger.debug("validating TOTP");
                if (!realm.validateTOTP(user, password, token)) {
                    return AuthenticationStatus.INVALID_CREDENTIALS;
                }
            } else {
                logger.debug("validating password for user: " + user.getLoginName());
                if (!realm.validatePassword(user, password)) {
                    logger.debug("invalid password for user: " + user.getLoginName());
                    return AuthenticationStatus.INVALID_CREDENTIALS;
                }
            }

            if (!user.getRequiredActions().isEmpty()) {
                return AuthenticationStatus.ACTIONS_REQUIRED;
            } else {
                return AuthenticationStatus.SUCCESS;
            }
        } else if (types.contains(CredentialRepresentation.SECRET)) {
            String secret = formData.getFirst(CredentialRepresentation.SECRET);
            if (secret == null) {
                logger.warn("Secret not provided");
                return AuthenticationStatus.MISSING_PASSWORD;
            }
            if (!realm.validateSecret(user, secret)) {
                logger.debug("invalid secret for user: " + user.getLoginName());
                return AuthenticationStatus.INVALID_CREDENTIALS;
            }
            if (!user.getRequiredActions().isEmpty()) {
                return AuthenticationStatus.ACTIONS_REQUIRED;
            } else {
                return AuthenticationStatus.SUCCESS;
            }
        } else {
            logger.warn("Do not know how to authenticate user");
            return AuthenticationStatus.FAILED;
        }
    }

    public enum AuthenticationStatus {
        SUCCESS, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED
    }

    public static class Auth {
        private SkeletonKeyToken token;
        private UserModel user;
        private UserModel client;

        public Auth(SkeletonKeyToken token) {
            this.token = token;
        }

        public SkeletonKeyToken getToken() {
            return token;
        }

        public UserModel getUser() {
            return user;
        }

        public UserModel getClient() {
            return client;
        }

        void setUser(UserModel user) {
            this.user = user;
        }

        void setClient(UserModel client) {
            this.client = client;
        }
    }

}
