package cz.muni.ics.oauth2.token;

import cz.muni.ics.oauth2.service.OAuth2TokenEntityService;
import cz.muni.ics.oauth2.token.exchange.BaseTokenExchangeGranter;
import cz.muni.ics.openid.connect.request.OAuth2RequestFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.common.exceptions.InvalidRequestException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Component("tokenExchangeGranter")
@Slf4j
public class TokenExchangeGranter extends AbstractTokenGranter {

    public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";

    public static final String PARAM_SUBJECT_TOKEN = "subject_token";

    public static final String PARAM_SUBJECT_TOKEN_TYPE = "subject_token_type";

    public static final String PARAM_REQUESTED_TOKEN_TYPE = "requested_token_type";

    public static final String PARAM_ACTOR_TOKEN = "actor_token";

    public static final String PARAM_ACTOR_TOKEN_TYPE = "actor_token_type";

    public static final String TOKEN_TYPE_ACCESS_TOKEN = "urn:ietf:params:oauth:token-type:access_token";

    public static final String ISSUED_TOKEN_TYPE = "issued_token_type";

    private final ClientDetailsService clientDetailsService;

    private final List<BaseTokenExchangeGranter> tokenExchangeGranters = new ArrayList<>();

    protected TokenExchangeGranter(OAuth2TokenEntityService tokenServices,
                                   ClientDetailsService clientDetailsService,
                                   OAuth2RequestFactory requestFactory)
    {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.clientDetailsService = clientDetailsService;
    }

    @Override
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        if (!GRANT_TYPE.equals(grantType)) {
            return null;
        }
        ClientDetails clientDetails = validateCallingClient(tokenRequest.getClientId());
        validateGrantType(grantType, clientDetails);
        validateTokenExchangeRequestParams(tokenRequest.getRequestParameters());
        for (BaseTokenExchangeGranter granter: tokenExchangeGranters) {
            if (granter.supportsByParams(tokenRequest.getRequestParameters())) {
                OAuth2AccessToken token = granter.grant(clientDetails, tokenRequest);
                if (token != null) {
                    return token;
                }
            }
        }
        return null;
    }

    public void registerTokenExchangeImplementation(BaseTokenExchangeGranter baseTokenExchangeGranter) {
        this.tokenExchangeGranters.add(baseTokenExchangeGranter);
    }

    @Override
    protected void validateGrantType(String grantType, ClientDetails clientDetails) {
        if (!GRANT_TYPE.equals(grantType)) {
            throw OAuth2Exception.create(OAuth2Exception.INVALID_GRANT,
                    "Unrecognized grant " + grantType);
        }
        if (clientDetails == null) {
            throw OAuth2Exception.create(OAuth2Exception.INVALID_CLIENT, "No client identified");
        }
        Set<String> authorizedGrants = clientDetails.getAuthorizedGrantTypes();
        if (authorizedGrants == null || !authorizedGrants.contains(grantType)) {
            throw OAuth2Exception.create(OAuth2Exception.INVALID_GRANT,
                    "Client is not authorized to use grant " + grantType);
        }
    }

    @Override
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        throw new UnsupportedOperationException("We do not support this operation");
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        throw new UnsupportedOperationException("We do not support this operation");
    }

    private void validateTokenExchangeRequestParams(Map<String, String> parameters) {
        if (parameters == null || parameters.isEmpty()) {
            throw new InvalidRequestException("Missing parameters in request");
        }
        String subjectToken = parameters.getOrDefault(PARAM_SUBJECT_TOKEN, null);
        if (!StringUtils.hasText(subjectToken)) {
            throw new InvalidRequestException("No subject_token parameter value provided");
        }
        String subjectTokenType = parameters.getOrDefault(PARAM_SUBJECT_TOKEN_TYPE, null);
        if (!StringUtils.hasText(subjectTokenType)){
            throw new InvalidRequestException("No subject_token_type parameter value provided");
        }

        String requestedTokenType = parameters.getOrDefault(PARAM_REQUESTED_TOKEN_TYPE, null);
        if (!StringUtils.hasText(requestedTokenType)) {
            throw new InvalidRequestException("No requested_token_type parameter value provided");
        }

        String actorToken = parameters.getOrDefault(PARAM_ACTOR_TOKEN, null);
        String actorTokenType = parameters.getOrDefault(PARAM_ACTOR_TOKEN_TYPE, null);
        if (StringUtils.hasText(actorToken)) {
            if (!StringUtils.hasText(actorTokenType)) {
                throw new InvalidRequestException("Parameter actor_token passed without the value for actor_token_type parameter");
            }
        } else {
            if (StringUtils.hasText(actorTokenType)) {
                throw new InvalidRequestException("Parameter actor_token_type passed without the value for actor_token parameter");
            }
        }
    }

    private ClientDetails validateCallingClient(String clientId) {
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        if (client == null) {
            throw new InvalidClientException("Unknown client");
        }
        return client;
    }

}
