package cz.muni.ics.oauth2.service;

import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import com.sun.istack.NotNull;
import cz.muni.ics.oauth2.exception.ProxiedIntrospectionRequestException;
import cz.muni.ics.oauth2.model.proxied_introspection.auth.BasicAuthProxiedIntrospectionAuthMethod;
import cz.muni.ics.oauth2.model.proxied_introspection.auth.ClientCredentialsViaBodyProxiedIntrospectionAuthMethod;
import cz.muni.ics.oauth2.model.proxied_introspection.auth.NoAuthProxiedIntrospectionAuthMethod;
import cz.muni.ics.oauth2.model.proxied_introspection.auth.ProxiedIntrospectionAuthMethod;
import cz.muni.ics.oauth2.model.proxied_introspection.ProxiedIntrospectionEndpoint;
import cz.muni.ics.oauth2.model.proxied_introspection.auth.ProxiedIntrospectionEndpointAuthMethod;
import cz.muni.ics.oauth2.web.endpoint.IntrospectionEndpoint;
import cz.muni.ics.oidc.exceptions.ConfigurationException;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import static cz.muni.ics.oauth2.model.proxied_introspection.ProxiedIntrospectionEndpoint.SUFFIX_AUTH_METHOD;
import static cz.muni.ics.oauth2.model.proxied_introspection.ProxiedIntrospectionEndpoint.SUFFIX_ENDPOINT;
import static cz.muni.ics.oauth2.model.proxied_introspection.ProxiedIntrospectionEndpoint.SUFFIX_ISSUER;

/**
 * Service class for executing proxied introspection (AARC-G052).
 *
 * Functionality configuration:
 * <ul>
 *     <li><b>proxied_introspection.enabled</b> - true/false - enables/disables functionality</li>
 *     <li><b>proxied_introspection.providers</b> - comma separated names of providers with whom we integrate</li>
 * </ul>
 *
 * @see ProxiedIntrospectionEndpoint for provider configuration
 *
 * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
 */
@Slf4j
public class ProxiedIntrospectionService {

    // === CONSTANTS - CONFIGURATION ===//

    public static final String PROXIED_INTROSPECTION = "proxied_introspection.";

    public static final String ENABLED = PROXIED_INTROSPECTION + "enabled";

    public static final String PROVIDERS = PROXIED_INTROSPECTION + "providers";

    public static final String PROVIDER_PREFIX = PROXIED_INTROSPECTION + "provider.";

    // === FIELDS ===//

    private final Map<String, ProxiedIntrospectionEndpoint> remoteEndpointsMap = new HashMap<>();

    // === PUBLIC METHODS ===//

    /**
     * Initialize endpoint.
     * @param properties Properties for initialization.
     * @return Initialized endpoint or null if functionality not enabled.
     */
    public static ProxiedIntrospectionService initialize(@NonNull Properties properties) {
        String enabledProp = properties.getProperty(ENABLED, null);
        boolean enabled = false;
        if (StringUtils.hasText(enabledProp)) {
            enabled = Boolean.parseBoolean(enabledProp);
        }
        if (!enabled) {
            log.debug("Proxied introspection - disabled");
            return null;
        } else {
            log.debug("Proxied introspection - enabled");
            return new ProxiedIntrospectionService(properties);
        }
    }

    /**
     * Execute remote introspection. If no original hint has been provided, pass empty string.
     * @param token Value of the token to be introspected
     * @param tokenTypeHint Hint of the token or empty string
     * @return Introspection result
     * @throws ProxiedIntrospectionRequestException When cannot call remote endpoint or call fails.
     */
    public Map<String, Object> executeIntrospection(@NonNull String token, @NonNull String tokenTypeHint)
            throws ProxiedIntrospectionRequestException
    {
        JWTClaimsSet claimsSet;
        try {
            claimsSet = JWTParser.parse(token).getJWTClaimsSet();
        } catch (ParseException ex) {
            throw new ProxiedIntrospectionRequestException(
                    "Cannot make introspection - JWT token could not be parse", ex);
        }
        if (claimsSet == null) {
            throw new ProxiedIntrospectionRequestException(
                    "Cannot make introspection - JWT token does not have claims (or cannot extract them)");
        }
        String issuer = claimsSet.getIssuer();
        if (!StringUtils.hasText(issuer)) {
            throw new ProxiedIntrospectionRequestException("Cannot make introspection - JWT token does not have issuer set");
        }
        ProxiedIntrospectionEndpoint endpoint = remoteEndpointsMap.getOrDefault(issuer, null);
        if (endpoint == null) {
            throw new ProxiedIntrospectionRequestException("Cannot make introspection - Token issuer not recognized");
        }
        log.debug("Executing proxied token introspection at remote issuer '{}'", issuer);
        return endpoint.execute(token, tokenTypeHint);
    }

    // === PRIVATE METHODS ===//

    private ProxiedIntrospectionService(@NonNull Properties properties) {
        String[] providers = getMandatoryStringProperty(properties, PROVIDERS).split(",");
        for (String provider: providers) {
            String propertyBase = PROVIDER_PREFIX + provider;
            String issuer = getMandatoryStringProperty(properties, propertyBase + SUFFIX_ISSUER);
            String endpoint = getMandatoryStringProperty(properties, propertyBase + SUFFIX_ENDPOINT);
            ProxiedIntrospectionEndpointAuthMethod authMethod = getAuthMethod(properties, propertyBase);
            remoteEndpointsMap.put(issuer, new ProxiedIntrospectionEndpoint(endpoint, authMethod));
            log.debug("Initialized proxied introspection - Issuer '{}', endpoint '{}' and auth method '{}",
                    issuer, endpoint, authMethod.getClass().getSimpleName());
        }
    }

    private ProxiedIntrospectionEndpointAuthMethod getAuthMethod(Properties properties, String propertyBase) {
        ProxiedIntrospectionAuthMethod authMethod = ProxiedIntrospectionAuthMethod.getByValue(
                getMandatoryStringProperty(properties, propertyBase + SUFFIX_AUTH_METHOD)
        );
        switch (authMethod) {
            case SECRET_BASIC:
                return new BasicAuthProxiedIntrospectionAuthMethod(propertyBase, properties);
            case SECRET_POST:
                return new ClientCredentialsViaBodyProxiedIntrospectionAuthMethod(propertyBase, properties);
            case NONE:
            default:
                return new NoAuthProxiedIntrospectionAuthMethod();
        }
    }

    @NotNull
    private String getMandatoryStringProperty(@NonNull Properties properties, @NonNull String key) {
        String prop = properties.getProperty(key, null);
        if (!StringUtils.hasText(prop)) {
            throw new ConfigurationException("Missing mandatory property - '" + key + "'");
        }
        return prop;
    }

}
