package cz.muni.ics.oauth2.token.exchange;

import cz.muni.ics.oauth2.model.OAuth2AccessTokenEntity;
import cz.muni.ics.oauth2.service.OAuth2TokenEntityService;
import cz.muni.ics.oauth2.token.TokenExchangeGranter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.util.StringUtils;

import java.util.Map;
import java.util.Set;

import static cz.muni.ics.oauth2.token.TokenExchangeGranter.PARAM_ACTOR_TOKEN_TYPE;
import static cz.muni.ics.oauth2.token.TokenExchangeGranter.PARAM_REQUESTED_TOKEN_TYPE;
import static cz.muni.ics.oauth2.token.TokenExchangeGranter.PARAM_SUBJECT_TOKEN;
import static cz.muni.ics.oauth2.token.TokenExchangeGranter.PARAM_SUBJECT_TOKEN_TYPE;

@Slf4j
public abstract class BaseTokenExchangeGranter {

    private final OAuth2TokenEntityService tokenServices;

    protected BaseTokenExchangeGranter(OAuth2TokenEntityService tokenServices,
                                       TokenExchangeGranter tokenExchangeGranter)
    {
        this.tokenServices = tokenServices;
        tokenExchangeGranter.registerTokenExchangeImplementation(this);
    }

    public boolean supportsByParams(Map<String, String> parameters) {
        if (parameters == null || parameters.isEmpty()) {
            log.debug("Missing parameters in request");
            return false;
        }
        String subjectToken = parameters.getOrDefault(PARAM_SUBJECT_TOKEN, null);
        if (!StringUtils.hasText(subjectToken)) {
            log.debug("No subject_token parameter value provided");
            return false;
        }
        String subjectTokenType = parameters.getOrDefault(PARAM_SUBJECT_TOKEN_TYPE, null);
        if (!StringUtils.hasText(subjectTokenType) || !getSupportedSubjectTokenTypes().contains(subjectTokenType)) {
            log.debug("Value passed in subject_token_type parameter is not supported token type");
            return false;
        }

        String requestedTokenType = parameters.getOrDefault(PARAM_REQUESTED_TOKEN_TYPE, null);
        if (!StringUtils.hasText(requestedTokenType) || !getSupportedRequestedTokenTypes().contains(requestedTokenType)) {
            log.debug("Value passed in requested_token_type parameter is not supported token type");
            return false;
        }

        String actorTokenType = parameters.getOrDefault(PARAM_ACTOR_TOKEN_TYPE, null);
        if (actorTokenType != null && !getSupportedActorTokenTypes().contains(actorTokenType)) {
            log.debug("Value passed in actor_token_type parameter is not supported token type");
            return false;
        }
        return true;
    }

    public final OAuth2AccessToken grant(ClientDetails client, TokenRequest tokenRequest) {
        if (!isSupported()) {
            return null;
        }
        if (!validateParameters(tokenRequest.getRequestParameters())) {
            return null;
        }
        if (!validateCallingClient(client)) {
            return null;
        }
        OAuth2AccessTokenEntity incomingAccessToken = resolveAccessToken(tokenRequest.getRequestParameters());
        if (!validateIncomingAccessToken(incomingAccessToken)) {
            return null;
        }
        // if we got this far, we can let the exceptions go through
        String requestedTokenType = tokenRequest.getRequestParameters().get(PARAM_REQUESTED_TOKEN_TYPE);
        return getAccessToken(client, incomingAccessToken, requestedTokenType, tokenRequest);
    }

    protected abstract boolean isSupported();

    protected abstract Set<String> getSupportedSubjectTokenTypes();

    protected abstract Set<String> getSupportedRequestedTokenTypes();

    protected abstract Set<String> getSupportedActorTokenTypes();

    protected abstract boolean validateCallingClient(ClientDetails client);

    protected abstract OAuth2AccessToken getAccessToken(ClientDetails clientDetails,
                                                        OAuth2AccessTokenEntity accessToken,
                                                        String requestedTokenType,
                                                        TokenRequest tokenRequest);

    protected final OAuth2TokenEntityService getTokenServices() {
        return tokenServices;
    }

    protected boolean validateParameters(Map<String, String> parameters) {
        return true;
    }

    protected boolean validateIncomingAccessToken(OAuth2AccessTokenEntity incomingToken) {
        if (incomingToken == null) {
            log.debug("Invalid (none or revoked) access token provided");
            return false;
        } else if (incomingToken.isExpired()) {
            log.debug("Provided access token is expired");
            return false;
        }
        return true;
    }

    private OAuth2AccessTokenEntity resolveAccessToken(Map<String, String> requestParameters) {
        String incomingAccessToken = requestParameters.get(PARAM_SUBJECT_TOKEN);
        OAuth2AccessTokenEntity incomingToken = tokenServices.readAccessToken(incomingAccessToken);
        validateIncomingAccessToken(incomingToken);
        return incomingToken;
    }

}
