package org.gcube.keycloak.protocol.oidc.mapper;

import java.util.ArrayList;
import java.util.List;

import org.jboss.logging.Logger;
import org.keycloak.models.ClientSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Access;
import org.keycloak.representations.IDToken;

public class D4ScienceContextMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper {

    private static final Logger logger = Logger.getLogger(D4ScienceContextMapper.class);

    public static final String HTTP_REQUEST_HEADER_NAME = "d4scm.header-name";
    public static final String NARROW_RESOURCE_ACCESS = "d4scm.narrow-ra";

    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();

    // Assuring that the mapper is executed as last
    private static final int PRIORITY = Integer.MAX_VALUE;
    private static final String DISPLAY_TYPE = "OIDC D4Science Context Mapper";
    private static final String PROVIDER_ID = "oidc-d4scince-context-mapper";

    public static final String DEFAULT_HEADER_NAME = "X-D4Science-Context";
    public static final String DEFAULT_TOKEN_CLAIM = "aud";

    static {
        OIDCAttributeMapperHelper.addTokenClaimNameConfig(CONFIG_PROPERTIES);
        CONFIG_PROPERTIES.forEach(conf -> {
            if (OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME.equals(conf.getName()))
                conf.setDefaultValue(DEFAULT_TOKEN_CLAIM);
            conf.setReadOnly(true);
        });

        OIDCAttributeMapperHelper.addIncludeInTokensConfig(CONFIG_PROPERTIES, D4ScienceContextMapper.class);

        ProviderConfigProperty headerProperty = new ProviderConfigProperty();
        headerProperty.setName(HTTP_REQUEST_HEADER_NAME);
        headerProperty.setLabel("HTTP request header name with the requested context");
        headerProperty.setType(ProviderConfigProperty.STRING_TYPE);
        headerProperty.setHelpText("The HTTP header that contains the requested context to be mapped as the requested in the configured claim");
        headerProperty.setDefaultValue(DEFAULT_HEADER_NAME);
        headerProperty.setReadOnly(true);
        CONFIG_PROPERTIES.add(headerProperty);

        ProviderConfigProperty narrowProperty = new ProviderConfigProperty();
        narrowProperty.setName(NARROW_RESOURCE_ACCESS);
        narrowProperty.setLabel("Narrow down resource access array?");
        narrowProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE);
        narrowProperty.setHelpText("Narrow down resource access claim to contain only the requested context entry");
        CONFIG_PROPERTIES.add(narrowProperty);
    }

    @Override
    public String getDisplayCategory() {
        return TOKEN_MAPPER_CATEGORY;
    }

    @Override
    public int getPriority() {
        return PRIORITY;
    }

    @Override
    public String getDisplayType() {
        return DISPLAY_TYPE;
    }

    @Override
    public String getHelpText() {
        return "Maps the D4Science context audience by reading the configured header's value and sets it as the configured token claim, if it is in scope";
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return CONFIG_PROPERTIES;
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }

    @Override
    protected void setClaim(final IDToken token,
            final ProtocolMapperModel mappingModel,
            final UserSessionModel userSession,
            final KeycloakSession keycloakSession,
            final ClientSessionContext clientSessionCtx) {

        // Since only the OIDCAccessTokenMapper interface is implemented, we are almost sure that
        // the token object is an AccessToken but adding a specific check anyway
        if (token instanceof AccessToken) {
            AccessToken accessToken = ((AccessToken) token);
            String headerName = mappingModel.getConfig().get(HTTP_REQUEST_HEADER_NAME);
            if (headerName == null || "".equals(headerName)) {
                headerName = DEFAULT_HEADER_NAME;
            }
            logger.debugf("Looking for the '%s' header", headerName);
            String requestedD4SContext = keycloakSession.getContext().getRequestHeaders().getHeaderString(headerName);

            if (requestedD4SContext != null && !"".equals(requestedD4SContext)) {
                logger.debugf("Checking resource access for the requested context: %s", requestedD4SContext);
                Access contextAccessInResourceAccess = accessToken.getResourceAccess(requestedD4SContext);
                if (contextAccessInResourceAccess != null) {
                    logger.debugf("Mapping it as the configured claim: %s",
                            mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME));

                    OIDCAttributeMapperHelper.mapClaim(token, mappingModel, requestedD4SContext);

                    if (Boolean.parseBoolean(mappingModel.getConfig().get(NARROW_RESOURCE_ACCESS))) {
                        // Removing all access details but the requested context
                        accessToken.getResourceAccess().clear();
                        accessToken.getResourceAccess().put(requestedD4SContext, contextAccessInResourceAccess);
                    }
                } else {
                    logger.warnf("Requested context '%s' is not accessible to the client: %s", requestedD4SContext,
                            clientSessionCtx.getClientSession().getClient().getName());
                }
            }
        }
    }

}