package cz.muni.ics.oauth2.service.impl;

import com.nimbusds.jose.EncryptionMethod;
import com.nimbusds.jose.JWEAlgorithm;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jwt.JWTParser;
import cz.muni.ics.jwt.encryption.service.JWTEncryptionAndDecryptionService;
import cz.muni.ics.jwt.signer.service.JWTSigningAndValidationService;
import cz.muni.ics.oauth2.model.ClientDetailsEntity;
import cz.muni.ics.oauth2.model.DynamicallyRegisteredRequestBody;
import cz.muni.ics.oauth2.model.PKCEAlgorithm;
import cz.muni.ics.oauth2.model.SystemScope;
import cz.muni.ics.oauth2.model.enums.AppType;
import cz.muni.ics.oauth2.model.enums.AuthMethod;
import cz.muni.ics.oauth2.model.enums.SubjectType;
import cz.muni.ics.oauth2.service.ClientDetailsEntityService;
import cz.muni.ics.oauth2.service.DynamicClientRegistrationService;
import cz.muni.ics.oauth2.service.SystemScopeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.text.ParseException;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

@Service
@Slf4j
public class DynamicClientRegistrationServiceImpl implements DynamicClientRegistrationService {
    
    private final ClientDetailsEntityService clientDetailsEntityService;
    private final JWTSigningAndValidationService signService;
    private final JWTEncryptionAndDecryptionService encService;
    private final SystemScopeService scopeService;

    public DynamicClientRegistrationServiceImpl(ClientDetailsEntityService clientDetailsEntityService,
                                                JWTSigningAndValidationService signService,
                                                JWTEncryptionAndDecryptionService encService,
                                                SystemScopeService scopeService)
    {
        this.clientDetailsEntityService = clientDetailsEntityService;
        this.signService = signService;
        this.encService = encService;
        this.scopeService = scopeService;
    }

    @Override
    public ClientDetailsEntity saveClient(String tokenClientId, DynamicallyRegisteredRequestBody requestedRegistration) {
        ClientDetailsEntity tokenClient = clientDetailsEntityService.loadClientByClientId(tokenClientId);

        if (tokenClient == null) {
            throw new InvalidClientException("Client for ID " + tokenClientId + " not found");
        }

        validateOptions(requestedRegistration);
        validateWithParentClient(tokenClient, requestedRegistration);

        ClientDetailsEntity client;
        try {
            client = mapToClient(tokenClient, requestedRegistration);
        } catch (ParseException e) {
            // should never happen, but just in case, we should respond back correctly with an error message.
            throw new InvalidRequestException("Failure when parsing JWT object");
        }

        return clientDetailsEntityService.saveNewClient(client);
    }

    @Override
    public void removeClient(String tokenClientId, String dynregClientId) {
        ClientDetailsEntity tokenClient = clientDetailsEntityService.loadClientByClientId(tokenClientId);
        if (tokenClient == null) {
            throw new InvalidClientException("Client for ID " + tokenClientId + " not found");
        }
        ClientDetailsEntity dynregClient = clientDetailsEntityService.loadClientByClientId(dynregClientId);
        if (dynregClient == null) {
            throw new InvalidClientException("Client for ID " + dynregClientId + " not found");
        } else if (!dynregClient.isDynamicallyRegistered()) {
            throw new InvalidClientException("Client "
                    + dynregClientId + " has not been registered dynamically, thus cannot be removed");
        }

        if (!tokenClient.equals(dynregClient)) {
            throw new InvalidClientException("Client "
                    + dynregClientId + " has not been registered by the caller client, thus cannot be removed");
        }

        clientDetailsEntityService.removeClient(dynregClient);
    }

    private void validateWithParentClient(ClientDetailsEntity tokenClient, DynamicallyRegisteredRequestBody requestedRegistration) {
        Set<String> requestedScope = requestedRegistration.getScope();
        Set<String> authScope = tokenClient.getScope();
        if (requestedScope != null && authScope != null && !authScope.containsAll(requestedScope)) {
            throw new InvalidRequestException(
                    "Configuration error - cannot register more scopes than the token client has"
            );
        }

        Set<String> requestedRedirects = requestedRegistration.getRedirectUris();
        Set<String> authRedirects = tokenClient.getRedirectUris();
        if (requestedRedirects != null && authRedirects != null &&  !authRedirects.containsAll(requestedRedirects)) {
            throw new InvalidRequestException(
                    "Configuration error - cannot register other redirects than the token client has"
            );
        }

        Set<String> requestedGrantTypes = requestedRegistration.getGrantTypes();
        Set<String> authGrantTypes = tokenClient.getGrantTypes();
        if (requestedGrantTypes != null && authGrantTypes != null
                && !authGrantTypes.containsAll(requestedGrantTypes)
        ) {
            throw new InvalidRequestException(
                    "Configuration error - cannot register other grants than the token client has"
            );
        }

        Set<String> requestedPostLogoutRedirects = requestedRegistration.getPostLogoutRedirectUris();
        Set<String> authPostLogoutRedirects = tokenClient.getPostLogoutRedirectUris();
        if (requestedPostLogoutRedirects != null && authPostLogoutRedirects != null
                && !authPostLogoutRedirects.containsAll(requestedPostLogoutRedirects)
        ) {
            throw new InvalidRequestException(
                    "Configuration error - cannot register other post logout redirects than the token client has"
            );
        }

        Set<String> requestedResourceIds = requestedRegistration.getResource();
        Set<String> authResourceIds = tokenClient.getResourceIds();
        if (requestedResourceIds != null && authResourceIds != null
                && !authResourceIds.containsAll(requestedResourceIds)
        ) {
            throw new InvalidRequestException(
                    "Configuration error - cannot register other resources than the token client has"
            );
        }

        log.debug("Requested client registration validation passed");
    }

    private void validateOptions(DynamicallyRegisteredRequestBody clientRequest) {
        String tokenEndpointAuthMethod = clientRequest.getTokenEndpointAuthMethod();
        if (!AuthMethod.isSupported(tokenEndpointAuthMethod)) {
            throw new InvalidRequestException("Unsupported token endpoint auth method: " + tokenEndpointAuthMethod);
        }

        Set<String> scope = clientRequest.getScope();
        if (scope == null) {
            clientRequest.setScope(new HashSet<>());
        } else if (!scope.isEmpty()) {
            Set<SystemScope> allScopes = scopeService.getAll();
            for (String scopeStr: scope) {
                if (!allScopes.contains(scopeService.fromString(scopeStr))) {
                    throw new InvalidRequestException("Unsupported scope requested: " + scopeStr);
                }
            }
        }

        //TODO: check grants are supported by injecting the configuration of supported grants
        Set<String> grants = clientRequest.getGrantTypes();
        Set<String> supportedGrants = Set.of(
                "authorization_code",
                "implicit",
                "client_credentials",
                "refresh_token",
                "urn:ietf:params:oauth:grant-type:token-exchange",
                "urn:ietf:params:oauth:grant-type:device_code");
        if (grants == null) {
            clientRequest.setGrantTypes(new HashSet<>());
            grants = clientRequest.getGrantTypes();
        } else if (!grants.isEmpty()) {
            for (String grant : grants) {
                if (!supportedGrants.contains(grant)) {
                    throw new InvalidRequestException("Unsupported grant type requested: " + grant);
                }
            }
        }

        //TODO: check grants are supported by injecting the configuration of supported response types
        Set<String> responseTypes = clientRequest.getResponseTypes();
        Set<String> supportedResponseTypes = Set.of("code", "token id_token", "id_token token");
        if (responseTypes == null) {
            clientRequest.setResponseTypes(new HashSet<>());
            responseTypes = clientRequest.getResponseTypes();
        } else if (!responseTypes.isEmpty()) {
            for (String responseType : responseTypes) {
                if (!supportedResponseTypes.contains(responseType)) {
                    throw new InvalidRequestException("Unsupported response type requested: " + responseType);
                }
            }
        }

        if (grants.contains("authorization_code") && (!responseTypes.contains("code"))) {
            throw new InvalidRequestException("Grant 'authorization_code' requires response type 'code'");
        }
        if (grants.contains("implicit")
                && !responseTypes.contains("token")
                && !responseTypes.contains("token id_token")
                && !responseTypes.contains("id_token token")
        ) {
            throw new InvalidRequestException("Grant 'implicit' requires response type 'token id_token' or 'id_token'");
        }
        if (!grants.contains("authorization_code") && !grants.contains("implicit") && !responseTypes.isEmpty()) {
            throw new InvalidRequestException("Requested grant types do not match with response types (should be empty)");
        }
        if (StringUtils.hasText(clientRequest.getJwks())) {
            try {
                JWKSet.parse(clientRequest.getJwks());
            } catch (ParseException e) {
                throw new InvalidRequestException("Could not parse JWK set");
            }
        }
        String appType = clientRequest.getApplicationType();
        if (StringUtils.hasText(appType) && !AppType.isSupported(appType)) {
            throw new InvalidRequestException("Unsupported application type: " + appType);
        }
        String subjectType = clientRequest.getSubjectType();
        if (StringUtils.hasText(subjectType) && !SubjectType.isSupported(subjectType)) {
            throw new InvalidRequestException("Unsupported subject type: " + subjectType);
        }
        if (StringUtils.hasText(clientRequest.getRequestObjectSigningAlg())) {
            JWSAlgorithm alg = JWSAlgorithm.parse(clientRequest.getRequestObjectSigningAlg());
            if (!signService.supportsAlgorithm(alg)) {
                throw new InvalidRequestException("Unsupported request object signing algorithm: " + alg.getName());
            }
        }
        if (StringUtils.hasText(clientRequest.getUserInfoSignedResponseAlg())) {
            JWSAlgorithm alg = JWSAlgorithm.parse(clientRequest.getUserInfoSignedResponseAlg());
            if (!signService.supportsAlgorithm(alg)) {
                throw new InvalidRequestException("Unsupported userinfo signing algorithm: " + alg.getName());
            }
        }
        if (StringUtils.hasText(clientRequest.getUserInfoEncryptedResponseAlg())) {
            JWEAlgorithm alg = JWEAlgorithm.parse(clientRequest.getUserInfoEncryptedResponseAlg());
            if (!encService.supportsEncryptionAlgorithm(alg)) {
                throw new InvalidRequestException("Unsupported userinfo encryption algorithm: " + alg.getName());
            }
        }
        if (StringUtils.hasText(clientRequest.getUserInfoEncryptedResponseEnc())) {
            EncryptionMethod encMethod = EncryptionMethod.parse(clientRequest.getUserInfoEncryptedResponseEnc());
            if (!encService.supportsEncryptionMethod(encMethod)) {
                throw new InvalidRequestException("Unsupported ID token encryption method: " + encMethod.getName());
            }
        }
        if (StringUtils.hasText(clientRequest.getIdTokenSignedResponseAlg())) {
            JWSAlgorithm alg = JWSAlgorithm.parse(clientRequest.getIdTokenSignedResponseAlg());
            if (!signService.supportsAlgorithm(alg)) {
                throw new InvalidRequestException("Unsupported ID token signing algorithm: " + alg.getName());
            }
        }
        if (StringUtils.hasText(clientRequest.getIdTokenEncryptedResponseAlg())) {
            JWEAlgorithm alg = JWEAlgorithm.parse(clientRequest.getIdTokenEncryptedResponseAlg());
            if (!encService.supportsEncryptionAlgorithm(alg)) {
                throw new InvalidRequestException("Unsupported ID token encryption algorithm: " + alg.getName());
            }
        }
        if (StringUtils.hasText(clientRequest.getIdTokenEncryptedResponseEnc())) {
            EncryptionMethod encMethod = EncryptionMethod.parse(clientRequest.getIdTokenEncryptedResponseEnc());
            if (!encService.supportsEncryptionMethod(encMethod)) {
                throw new InvalidRequestException("Unsupported ID token encryption method: " + encMethod.getName());
            }
        }
        if (StringUtils.hasText(clientRequest.getTokenEndpointAuthSigningAlg())) {
            JWSAlgorithm alg = JWSAlgorithm.parse(clientRequest.getTokenEndpointAuthSigningAlg());
            if (!signService.supportsAlgorithm(alg)) {
                throw new InvalidRequestException("Unsupported token endpoint signing algorithm: " + alg.getName());
            }
        }

        Set<String> defaultACRvalues = clientRequest.getDefaultACRvalues();
        if (defaultACRvalues != null && !defaultACRvalues.isEmpty()) {
            //TODO: check all ACR values are supported
        }

        if (StringUtils.hasText(clientRequest.getSoftwareStatement())) {
            try {
                JWTParser.parse(clientRequest.getSoftwareStatement());
            } catch (ParseException e) {
                throw new InvalidRequestException("Could not parse software statement as JWT");
            }
        }

        String pkceAlg = clientRequest.getCodeChallengeMethod();
        if (StringUtils.hasText(clientRequest.getCodeChallengeMethod())
                && !PKCEAlgorithm.isSupported(pkceAlg)
        ) {
            throw new InvalidRequestException("Unsupported PKCE algorithm requested: " + pkceAlg);
        }
    }

    private ClientDetailsEntity mapToClient(ClientDetailsEntity tokenClient,
                                            DynamicallyRegisteredRequestBody requestedRegistration)
            throws ParseException
    {
        ClientDetailsEntity client = new ClientDetailsEntity();

        client.setClientName(requestedRegistration.getClientName());
        client.setClientDescription(requestedRegistration.getClientDescription());
        client.setRedirectUris(requestedRegistration.getRedirectUris());
        client.setClientUri(requestedRegistration.getClientUri());
        client.setContacts(requestedRegistration.getContacts());
        client.setTosUri(requestedRegistration.getTosUri());
        if (client.getTokenEndpointAuthMethod() != null) {
            client.setTokenEndpointAuthMethod(AuthMethod.getByValue(requestedRegistration.getTokenEndpointAuthMethod()));
        } else {
            client.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
        }
        if (AuthMethod.NONE != client.getTokenEndpointAuthMethod()) {
            String clientSecret = String.format("%s-%s", UUID.randomUUID(), UUID.randomUUID());
            client.setClientSecret(clientSecret);
        }
        client.setScope(requestedRegistration.getScope());
        client.setGrantTypes(requestedRegistration.getGrantTypes());
        client.setResponseTypes(requestedRegistration.getResponseTypes());
        client.setPolicyUri(requestedRegistration.getPolicyUri());
        client.setJwksUri(requestedRegistration.getJwksUri());
        if (StringUtils.hasText(requestedRegistration.getJwks())) {
            client.setJwks(JWKSet.parse(requestedRegistration.getJwks()));
        }
        client.setSoftwareId(requestedRegistration.getSoftwareId());
        client.setSoftwareVersion(requestedRegistration.getSoftwareVersion());
        if (StringUtils.hasText(requestedRegistration.getApplicationType())) {
            client.setApplicationType(AppType.getByValue(requestedRegistration.getApplicationType()));
        }
        client.setSectorIdentifierUri(requestedRegistration.getSectorIdentifierUri());
        if (StringUtils.hasText(requestedRegistration.getSubjectType())) {
            client.setSubjectType(SubjectType.getByValue(requestedRegistration.getSubjectType()));
        }
        if (StringUtils.hasText(requestedRegistration.getRequestObjectSigningAlg())) {
            client.setRequestObjectSigningAlg(JWSAlgorithm.parse(requestedRegistration.getRequestObjectSigningAlg()));
        }
        if (StringUtils.hasText(requestedRegistration.getUserInfoSignedResponseAlg())) {
            client.setUserInfoSignedResponseAlg(JWSAlgorithm.parse(requestedRegistration.getUserInfoSignedResponseAlg()));
        }
        if (StringUtils.hasText(requestedRegistration.getUserInfoEncryptedResponseAlg())) {
            client.setUserInfoEncryptedResponseAlg(JWEAlgorithm.parse(requestedRegistration.getUserInfoEncryptedResponseAlg()));
        }
        if (StringUtils.hasText(requestedRegistration.getUserInfoEncryptedResponseEnc())) {
            client.setUserInfoEncryptedResponseEnc(EncryptionMethod.parse(requestedRegistration.getUserInfoEncryptedResponseEnc()));
        }
        if (StringUtils.hasText(requestedRegistration.getIdTokenSignedResponseAlg())) {
            client.setIdTokenSignedResponseAlg(JWSAlgorithm.parse(requestedRegistration.getIdTokenSignedResponseAlg()));
        }
        if (StringUtils.hasText(requestedRegistration.getIdTokenEncryptedResponseAlg())) {
            client.setIdTokenEncryptedResponseAlg(JWEAlgorithm.parse(requestedRegistration.getIdTokenEncryptedResponseAlg()));
        }
        if (StringUtils.hasText(requestedRegistration.getIdTokenEncryptedResponseEnc())) {
            client.setIdTokenEncryptedResponseEnc(EncryptionMethod.parse(requestedRegistration.getIdTokenEncryptedResponseEnc()));
        }
        if (StringUtils.hasText(requestedRegistration.getTokenEndpointAuthSigningAlg())) {
            client.setTokenEndpointAuthSigningAlg(JWSAlgorithm.parse(requestedRegistration.getTokenEndpointAuthSigningAlg()));
        }
        client.setDefaultMaxAge(requestedRegistration.getDefaultMaxAge());
        client.setRequireAuthTime(requestedRegistration.getRequireAuthTime());
        client.setDefaultACRvalues(requestedRegistration.getDefaultACRvalues());
        client.setInitiateLoginUri(requestedRegistration.getInitiateLoginUri());
        client.setPostLogoutRedirectUris(requestedRegistration.getPostLogoutRedirectUris());
        client.setRequestUris(requestedRegistration.getRequestUris());
        client.setAccessTokenValiditySeconds(requestedRegistration.getAccessTokenValiditySeconds());
        client.setRefreshTokenValiditySeconds(requestedRegistration.getRefreshTokenValiditySeconds());
        client.setResourceIds(requestedRegistration.getResource());
        client.setReuseRefreshToken(requestedRegistration.isReuseRefreshToken());
        client.setIdTokenValiditySeconds(requestedRegistration.getIdTokenValiditySeconds());
        client.setClearAccessTokensOnRefresh(requestedRegistration.isClearAccessTokensOnRefresh());
        client.setDeviceCodeValiditySeconds(requestedRegistration.getDeviceCodeValiditySeconds());
        client.setClaimsRedirectUris(requestedRegistration.getClaimsRedirectUris());

        if (StringUtils.hasText(requestedRegistration.getSoftwareStatement())) {
            client.setSoftwareStatement(JWTParser.parse(requestedRegistration.getSoftwareStatement()));
        }

        if (StringUtils.hasText(requestedRegistration.getCodeChallengeMethod())) {
            client.setCodeChallengeMethod(PKCEAlgorithm.getByAlgorithmName(requestedRegistration.getCodeChallengeMethod()));
        }

        client.setDynamicallyRegistered(true);
        client.setAcceptedTos(tokenClient.isAcceptedTos());
        client.setJurisdiction(tokenClient.getJurisdiction());
        client.setParentClientId(tokenClient.getId());
        return client;
    }
    
    
}
