package io.cloudtrust.keycloak.export;

import io.cloudtrust.keycloak.export.dto.BetterCredentialRepresentation;
import io.cloudtrust.keycloak.export.dto.BetterRealmRepresentation;
import org.jboss.logging.Logger;
import org.keycloak.common.ClientConnection;
import org.keycloak.credential.CredentialModel;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.JWSInputException;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.policy.PasswordPolicyNotMetException;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.managers.AppAuthManager;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resource.RealmResourceProvider;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.services.resources.admin.AdminAuth;
import org.keycloak.services.resources.admin.AdminRoot;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;

import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * ExportResourceProvider exposes two endpoints to import and export realms
 */
public class ExportResourceProvider implements RealmResourceProvider {

    protected static final Logger logger = Logger.getLogger(ExportResourceProvider.class);

    private KeycloakSession session;

    protected AppAuthManager authManager;

    @Context
    protected ClientConnection clientConnection;

    public ExportResourceProvider(KeycloakSession session) {
        this.session = session;
        this.authManager = new AppAuthManager();

    }

    @Override
    public Object getResource() {
        return this;
    }

    @GET
    @Path("realm")
    @Produces(MediaType.APPLICATION_JSON)
    public RealmRepresentation exportRealm(@Context final HttpHeaders headers, @Context final UriInfo uriInfo) {
        //retrieving the realm should be done before authentication
        // authentication overrides the value with master inside the context
        // this is done this way to avoid changing the copied code below (authenticateRealmAdminRequest)
        RealmModel realm = session.getContext().getRealm();
        AdminAuth adminAuth = authenticateRealmAdminRequest(headers, uriInfo);
        RealmManager realmManager = new RealmManager(session);
        RoleModel roleModel = adminAuth.getRealm().getRole(AdminRoles.ADMIN);
        AdminPermissionEvaluator realmAuth = AdminPermissions.evaluator(session, realm, adminAuth);
        if (roleModel != null && adminAuth.getUser().hasRole(roleModel)
                && adminAuth.getRealm().equals(realmManager.getKeycloakAdminstrationRealm())
                && realmAuth.realm().canManageRealm()) {
            RealmRepresentation realmRep = ExportUtils.exportRealm(session, realm, true, true);
            //correct users
            if (realmRep.getUsers() != null) {
                setCorrectCredentials(realmRep.getUsers(), realm);
            }
            return realmRep;
        } else {
            throw new ForbiddenException();
        }
    }

    /**
     * This method rewrites the credential list for the users, including the Id (which is missing by default).
     * Unfortunately, due to the limitations in the keycloak API, there is no way to unit test this.
     *
     * @param users The user representations to correct
     * @param realm the realm being exported
     */
    private void setCorrectCredentials(List<UserRepresentation> users, RealmModel realm) {
        Map<String, UserRepresentation> userRepMap = new HashMap<>(users.size());
        for (UserRepresentation userRep : users) {
            userRepMap.put(userRep.getId(), userRep);
        }

        for (UserModel user : session.users().getUsers(realm, true)) {
            UserRepresentation userRep = userRepMap.get(user.getId());
            if (userRep != null) {
                // Credentials
                List<CredentialModel> creds = session.userCredentialManager().getStoredCredentials(realm, user);
                List<CredentialRepresentation> credReps = creds.stream().map(this::exportCredential).collect(Collectors.toList());
                userRep.setCredentials(credReps);
            }
        }
    }

    private BetterCredentialRepresentation exportCredential(CredentialModel userCred) {
        BetterCredentialRepresentation credRep = new BetterCredentialRepresentation();
        credRep.setId(userCred.getId());
        credRep.setType(userCred.getType());
        credRep.setCreatedDate(userCred.getCreatedDate());
        credRep.setCredentialData(userCred.getCredentialData());
        credRep.setSecretData(userCred.getSecretData());
        credRep.setUserLabel(userCred.getUserLabel());
        return credRep;
    }

    @Override
    public void close() {
        // Nothing to close
    }

    /**
     * This code has been copied from keycloak org.keycloak.services.resources.admin.AdminRoot;
     * it allows to check if a user as realm/master admin
     * at each upgrade check that it hasn't been modified
     */
    private AdminAuth authenticateRealmAdminRequest(HttpHeaders headers, UriInfo uriInfo) {
        String tokenString = authManager.extractAuthorizationHeaderToken(headers);
        if (tokenString == null) throw new NotAuthorizedException("Bearer");
        AccessToken token;
        try {
            JWSInput input = new JWSInput(tokenString);
            token = input.readJsonContent(AccessToken.class);
        } catch (JWSInputException e) {
            throw new NotAuthorizedException("Bearer token format error", e);
        }
        String realmName = token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1);
        RealmManager realmManager = new RealmManager(session);
        RealmModel realm = realmManager.getRealmByName(realmName);
        if (realm == null) {
            throw new NotAuthorizedException("Unknown realm in token");
        }
        session.getContext().setRealm(realm);
        AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(session, realm, uriInfo, clientConnection, headers);
        if (authResult == null) {
            logger.debug("Token not valid");
            throw new NotAuthorizedException("Bearer");
        }

        ClientModel client = realm.getClientByClientId(token.getIssuedFor());
        if (client == null) {
            throw new NotFoundException("Could not find client for authorization");

        }

        return new AdminAuth(realm, authResult.getToken(), authResult.getUser(), client);
    }

    @POST
    @Path("realm")
    @Produces(MediaType.APPLICATION_JSON)
    public Response importRealm(@Context final HttpHeaders headers, @Context final UriInfo uriInfo, @Context KeycloakApplication keycloak, BetterRealmRepresentation rep) {
        try {
            AdminAuth auth = authenticateRealmAdminRequest(headers, uriInfo);
            AdminPermissions.realms(session, auth).requireCreateRealm();

            RealmModel realm = ImportExportUtils.importRealm(session, keycloak, rep, null, false);
            grantPermissionsToRealmCreator(auth, realm);

            URI location = AdminRoot.realmsUrl(session.getContext().getUri()).path(realm.getName()).build();
            logger.debugv("imported realm success, sending back: {0}", location);

            return Response.created(location).build();
        } catch (ModelDuplicateException e) {
            logger.error("Conflict detected", e);
            return ErrorResponse.exists("Conflict detected. See logs for details");
        } catch (PasswordPolicyNotMetException e) {
            logger.error("Password policy not met for user " + e.getUsername(), e);
            if (session.getTransactionManager().isActive()) session.getTransactionManager().setRollbackOnly();
            return ErrorResponse.error("Password policy not met. See logs for details", Response.Status.BAD_REQUEST);
        }
    }

    private void grantPermissionsToRealmCreator(AdminAuth auth, RealmModel realm) {
        if (auth.hasRealmRole(AdminRoles.ADMIN)) {
            return;
        }

        new RealmManager(session).getKeycloakAdminstrationRealm();
        ClientModel realmAdminApp = realm.getMasterAdminClient();
        for (String r : AdminRoles.ALL_REALM_ROLES) {
            RoleModel role = realmAdminApp.getRole(r);
            auth.getUser().grantRole(role);
        }
    }
}