package org.apereo.cas.oidc.web.controllers;

import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.authentication.principal.ServiceFactory;
import org.apereo.cas.authentication.principal.WebApplicationService;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.oidc.OidcConstants;
import org.apereo.cas.oidc.dynareg.OidcClientRegistrationRequest;
import org.apereo.cas.oidc.dynareg.OidcClientRegistrationResponse;
import org.apereo.cas.services.OidcRegisteredService;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.support.oauth.OAuth20GrantTypes;
import org.apereo.cas.support.oauth.OAuth20ResponseTypes;
import org.apereo.cas.support.oauth.profile.OAuth20ProfileScopeToAttributesFilter;
import org.apereo.cas.support.oauth.validator.OAuth20Validator;
import org.apereo.cas.support.oauth.web.endpoints.BaseOAuth20Controller;
import org.apereo.cas.ticket.accesstoken.AccessTokenFactory;
import org.apereo.cas.ticket.registry.TicketRegistry;
import org.apereo.cas.util.gen.RandomStringGenerator;
import org.apereo.cas.util.serialization.StringSerializer;
import org.apereo.cas.web.support.CookieRetrievingCookieGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * This is {@link OidcDynamicClientRegistrationEndpointController}.
 *
 * @author Misagh Moayyed
 * @since 5.1.0
 */
public class OidcDynamicClientRegistrationEndpointController extends BaseOAuth20Controller {
    private static final Logger LOGGER = LoggerFactory.getLogger(OidcDynamicClientRegistrationEndpointController.class);

    private final StringSerializer<OidcClientRegistrationRequest> clientRegistrationRequestSerializer;
    private final RandomStringGenerator clientIdGenerator;
    private final RandomStringGenerator clientSecretGenerator;

    public OidcDynamicClientRegistrationEndpointController(final ServicesManager servicesManager,
                                                           final TicketRegistry ticketRegistry,
                                                           final OAuth20Validator validator,
                                                           final AccessTokenFactory accessTokenFactory,
                                                           final PrincipalFactory principalFactory,
                                                           final ServiceFactory<WebApplicationService> webApplicationServiceServiceFactory,
                                                           final StringSerializer<OidcClientRegistrationRequest> clientRegistrationRequestSerializer,
                                                           final RandomStringGenerator clientIdGenerator,
                                                           final RandomStringGenerator clientSecretGenerator,
                                                           final OAuth20ProfileScopeToAttributesFilter scopeToAttributesFilter,
                                                           final CasConfigurationProperties casProperties,
                                                           final CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator) {
        super(servicesManager, ticketRegistry, validator, accessTokenFactory,
                principalFactory, webApplicationServiceServiceFactory,
                scopeToAttributesFilter, casProperties, ticketGrantingTicketCookieGenerator);
        this.clientRegistrationRequestSerializer = clientRegistrationRequestSerializer;
        this.clientIdGenerator = clientIdGenerator;
        this.clientSecretGenerator = clientSecretGenerator;
    }

    /**
     * Handle request.
     *
     * @param jsonInput the json input
     * @param request   the request
     * @param response  the response
     * @return the model and view
     * @throws Exception the exception
     */
    @PostMapping(value = '/' + OidcConstants.BASE_OIDC_URL + '/' + OidcConstants.REGISTRATION_URL,
            consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<OidcClientRegistrationResponse> handleRequestInternal(@RequestBody final String jsonInput,
                                                                                final HttpServletRequest request,
                                                                                final HttpServletResponse response) throws Exception {
        try {
            final OidcClientRegistrationRequest registrationRequest = this.clientRegistrationRequestSerializer.from(jsonInput);
            LOGGER.debug("Received client registration request [{}]", registrationRequest);

            if (registrationRequest.getScopes().isEmpty()) {
                throw new Exception("Registration request does not contain any scope values");
            }
            if (!registrationRequest.getScope().contains(OidcConstants.OPENID)) {
                throw new Exception("Registration request scopes do not contain [{}]" + OidcConstants.OPENID);
            }

            final OidcRegisteredService registeredService = new OidcRegisteredService();
            registeredService.setName(registrationRequest.getClientName());

            if (StringUtils.isNotBlank(registrationRequest.getJwksUri())) {
                registeredService.setJwks(registrationRequest.getJwksUri());
                registeredService.setSignIdToken(true);
            }
            final String uri = registrationRequest.getRedirectUris().stream().findFirst().get();
            registeredService.setServiceId(uri);

            registeredService.setClientId(clientIdGenerator.getNewString());
            registeredService.setClientSecret(clientSecretGenerator.getNewString());
            registeredService.setEvaluationOrder(Integer.MIN_VALUE);

            final Set<String> supportedScopes = new HashSet<>(casProperties.getAuthn().getOidc().getScopes());
            supportedScopes.retainAll(registrationRequest.getScopes());

            final OidcClientRegistrationResponse clientResponse = getClientRegistrationResponse(registrationRequest, registeredService);
            registeredService.setScopes(supportedScopes);
            final Set<String> processedScopes = new LinkedHashSet<>(supportedScopes);
            registeredService.setScopes(processedScopes);
            registeredService.setDescription("Dynamically registered service "
                    .concat(registeredService.getName())
                    .concat(" with grant types ")
                    .concat(clientResponse.getGrantTypes().stream().collect(Collectors.joining(",")))
                    .concat(" and with scopes ")
                    .concat(registeredService.getScopes().stream().collect(Collectors.joining(",")))
                    .concat(" and response types ")
                    .concat(clientResponse.getResponseTypes().stream().collect(Collectors.joining(","))));
            registeredService.setDynamicallyRegistered(true);
            scopeToAttributesFilter.reconcile(registeredService);

            return new ResponseEntity<>(clientResponse, HttpStatus.CREATED);
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
            final Map<String, String> map = new HashMap<>();
            map.put("error", "invalid_client_metadata");
            map.put("error_message", e.getMessage());
            return new ResponseEntity(map, HttpStatus.BAD_REQUEST);
        }
    }

    /**
     * Gets client registration response.
     *
     * @param registrationRequest the registration request
     * @param registeredService   the registered service
     * @return the client registration response
     */
    protected OidcClientRegistrationResponse getClientRegistrationResponse(final OidcClientRegistrationRequest registrationRequest,
                                                                           final OidcRegisteredService registeredService) {
        final OidcClientRegistrationResponse clientResponse = new OidcClientRegistrationResponse();
        clientResponse.setApplicationType("web");
        clientResponse.setClientId(registeredService.getClientId());
        clientResponse.setClientSecret(registeredService.getClientSecret());
        clientResponse.setSubjectType("public");
        clientResponse.setTokenEndpointAuthMethod(registrationRequest.getTokenEndpointAuthMethod());
        clientResponse.setClientName(registeredService.getName());
        clientResponse.setGrantTypes(Arrays.asList(OAuth20GrantTypes.AUTHORIZATION_CODE.name().toLowerCase(),
                OAuth20GrantTypes.REFRESH_TOKEN.name().toLowerCase()));
        clientResponse.setRedirectUris(Collections.singletonList(registeredService.getServiceId()));
        clientResponse.setResponseTypes(Collections.singletonList(OAuth20ResponseTypes.CODE.name().toLowerCase()));
        return clientResponse;
    }
}
