package cz.muni.ics.oauth2.model.proxied_introspection;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import cz.muni.ics.oauth2.model.proxied_introspection.auth.ProxiedIntrospectionAuthMethod;
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.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Map;

/**
 * Class representing remote introspection endpoint for Proxied Token Introspection (AARC-G052).
 *
 * Configuration of providers (replace [name] part with the name defined for the endpoint):
 * <ul>
 *     <li><b>proxied_introspection.provider.[name].issuer</b> - issuer string (must be present in JWT issuer claim)</li>
 *     <li><b>proxied_introspection.provider.[name].endpoint</b> - location of the remote introspection endpoint</li>
 *     <li><b>proxied_introspection.provider.[name].auth_method</b> - one of values defined in AuthMethod.class</li>
 * </ul>
 *
 * @see ProxiedIntrospectionAuthMethod for list of auth methods available
 * @see cz.muni.ics.oauth2.model.proxied_introspection.auth package for AuthnMethods and their configuration.
 *
 * @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
 */
@Slf4j
public class ProxiedIntrospectionEndpoint {

    // === CONSTANTS === //

    public static final String PARAM_TOKEN = "token";

    public static final String PARAM_TOKEN_TYPE_HINT = "token_type_hint";

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

    public static final String SUFFIX_ISSUER = ".issuer";

    public static final String SUFFIX_ENDPOINT = ".endpoint";

    public static final String SUFFIX_AUTH_METHOD = ".auth_method";

    // === FIELDS === //
    private final RestTemplate restTemplate = new RestTemplate();

    private final URI endpoint;

    private final static ObjectMapper MAPPER = new ObjectMapper();

    //TODO: add hook for transformation of response (e.g. map "sub")

    // === CONSTRUCTORS === //

    public ProxiedIntrospectionEndpoint(@NonNull String endpoint, @NonNull ProxiedIntrospectionEndpointAuthMethod authMethod) {
        try {
            this.endpoint = new URL(endpoint).toURI();
        } catch (MalformedURLException e) {
            throw new ConfigurationException("Invalid configuration of ProxiedIntrospection endpoint - endpoint "
                    + endpoint + " is not valid URL");
        } catch (URISyntaxException e) {
            throw new ConfigurationException("Invalid configuration of ProxiedIntrospection endpoint - endpoint "
                    + endpoint + " is not valid URI");
        }
        restTemplate.setInterceptors(List.of(authMethod));
    }

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

    /**
     * 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 or null in case of failure.
     */
    public Map<String, Object> execute(@NonNull String token, @NonNull String tokenTypeHint) {
        log.debug("Executing proxied token introspection at endpoint '{}' for token '{}' and token_type_hint '{}'",
                endpoint, IntrospectionEndpoint.getTokenForPrint(token), tokenTypeHint);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setAccept(List.of(MediaType.APPLICATION_JSON));

        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        map.add(PARAM_TOKEN, token);
        if (StringUtils.hasText(tokenTypeHint)) {
            map.add(PARAM_TOKEN_TYPE_HINT, tokenTypeHint);
        }

        HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
        try {
            JsonNode json = restTemplate.postForObject(endpoint, entity, JsonNode.class);
            if (json != null) {
                return MAPPER.convertValue(json, new TypeReference<>() {});
            }
        } catch (RestClientException ex) {
            log.debug("Caught RestClientException when performing proxied token introspection (endpoint '{}')", endpoint);
            return null;
        }
        return null;
    }

}
