package org.apereo.cas.config;

import org.apache.cxf.sts.STSPropertiesMBean;
import org.apache.cxf.sts.StaticSTSProperties;
import org.apache.cxf.sts.claims.ClaimsAttributeStatementProvider;
import org.apache.cxf.sts.claims.ClaimsManager;
import org.apache.cxf.sts.event.map.EventMapper;
import org.apache.cxf.sts.event.map.MapEventLogger;
import org.apache.cxf.sts.operation.TokenIssueOperation;
import org.apache.cxf.sts.operation.TokenValidateOperation;
import org.apache.cxf.sts.service.StaticService;
import org.apache.cxf.sts.token.delegation.SAMLDelegationHandler;
import org.apache.cxf.sts.token.delegation.TokenDelegationHandler;
import org.apache.cxf.sts.token.provider.DefaultConditionsProvider;
import org.apache.cxf.sts.token.provider.DefaultSubjectProvider;
import org.apache.cxf.sts.token.provider.SAMLTokenProvider;
import org.apache.cxf.sts.token.realm.RealmProperties;
import org.apache.cxf.sts.token.realm.Relationship;
import org.apache.cxf.sts.token.validator.SAMLTokenValidator;
import org.apache.cxf.sts.token.validator.TokenValidator;
import org.apache.cxf.sts.token.validator.X509TokenValidator;
import org.apache.cxf.transport.servlet.CXFServlet;
import org.apache.cxf.ws.security.sts.provider.SecurityTokenServiceProvider;
import org.apache.cxf.ws.security.sts.provider.operation.IssueOperation;
import org.apache.cxf.ws.security.sts.provider.operation.ValidateOperation;
import org.apache.wss4j.dom.validate.Validator;
import org.apereo.cas.CipherExecutor;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer;
import org.apereo.cas.authentication.AuthenticationMetaDataPopulator;
import org.apereo.cas.authentication.AuthenticationServiceSelectionStrategy;
import org.apereo.cas.authentication.SecurityTokenServiceAuthenticationMetaDataPopulator;
import org.apereo.cas.authentication.SecurityTokenServiceClientBuilder;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.model.support.wsfed.WsFederationProperties;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.support.claims.WrappingSecurityTokenServiceClaimsHandler;
import org.apereo.cas.support.realm.RealmPasswordVerificationCallbackHandler;
import org.apereo.cas.support.realm.UriRealmParser;
import org.apereo.cas.support.util.CryptoUtils;
import org.apereo.cas.support.validation.CipheredCredentialsValidator;
import org.apereo.cas.support.validation.SecurityTokenServiceCredentialCipherExecutor;
import org.apereo.cas.support.x509.X509TokenDelegationHandler;
import org.apereo.cas.ticket.DefaultSecurityTokenTicketFactory;
import org.apereo.cas.ticket.ExpirationPolicy;
import org.apereo.cas.ticket.SecurityTokenTicketFactory;
import org.apereo.cas.ticket.UniqueTicketIdGenerator;
import org.apereo.cas.util.DefaultUniqueTicketIdGenerator;
import org.apereo.cas.ws.idp.WSFederationConstants;
import org.opensaml.saml.saml2.core.NameID;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

import javax.xml.ws.Provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * This is {@link CoreWsSecuritySecurityTokenServiceConfiguration}.
 *
 * @author Misagh Moayyed
 * @since 5.1.0
 */
@Configuration("coreWsSecuritySecurityTokenServiceConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
@ImportResource(locations = {"classpath:jaxws-realms.xml", "classpath:META-INF/cxf/cxf.xml"})
public class CoreWsSecuritySecurityTokenServiceConfiguration implements AuthenticationEventExecutionPlanConfigurer {

    @Autowired
    @Qualifier("grantingTicketExpirationPolicy")
    private ExpirationPolicy grantingTicketExpirationPolicy;

    @Autowired
    @Qualifier("wsFederationAuthenticationServiceSelectionStrategy")
    private AuthenticationServiceSelectionStrategy wsFederationAuthenticationServiceSelectionStrategy;

    @Autowired
    @Qualifier("servicesManager")
    private ServicesManager servicesManager;

    @Autowired
    private CasConfigurationProperties casProperties;

    @Bean
    public ServletRegistrationBean cxfServlet() {
        final ServletRegistrationBean bean = new ServletRegistrationBean();
        bean.setEnabled(true);
        bean.setName("cxfServletSecurityTokenService");
        bean.setServlet(new CXFServlet());
        bean.setUrlMappings(Collections.singleton(WSFederationConstants.ENDPOINT_STS.concat("*")));
        bean.setAsyncSupported(true);
        return bean;
    }

    @Bean
    public EventMapper loggerListener() {
        return new EventMapper(new MapEventLogger());
    }

    @RefreshScope
    @Bean
    public List<TokenDelegationHandler> delegationHandlers() {
        final List<TokenDelegationHandler> handlers = new ArrayList<>();
        handlers.add(new SAMLDelegationHandler());
        handlers.add(new X509TokenDelegationHandler());
        return handlers;
    }

    @RefreshScope
    @Bean
    public Provider transportSTSProviderBean() {
        try {
            final SecurityTokenServiceProvider provider = new SecurityTokenServiceProvider();
            provider.setIssueOperation(transportIssueDelegate());
            provider.setValidateOperation(transportValidateDelegate());
            return provider;
        } catch (final Exception e) {
            throw new BeanCreationException(e.getMessage(), e);
        }
    }

    @RefreshScope
    @Bean
    public IssueOperation transportIssueDelegate() {
        final WsFederationProperties.SecurityTokenService wsfed = casProperties.getAuthn().getWsfedIdP().getSts();
        final WsFederationProperties.IdentityProvider idp = casProperties.getAuthn().getWsfedIdP().getIdp();

        final ClaimsManager claimsManager = new ClaimsManager();
        claimsManager.setClaimHandlers(Arrays.asList(new WrappingSecurityTokenServiceClaimsHandler(
                idp.getRealmName(),
                wsfed.getRealm().getIssuer())));

        final TokenIssueOperation op = new TokenIssueOperation();
        op.setTokenProviders(transportTokenProviders());
        op.setServices(Arrays.asList(transportService()));
        op.setStsProperties(transportSTSProperties());
        op.setClaimsManager(claimsManager);
        op.setTokenValidators(transportTokenValidators());
        op.setEventListener(loggerListener());
        op.setDelegationHandlers(delegationHandlers());
        op.setEncryptIssuedToken(wsfed.isEncryptTokens());

        return op;
    }

    @RefreshScope
    @Bean
    public ValidateOperation transportValidateDelegate() {
        final TokenValidateOperation op = new TokenValidateOperation();
        op.setTokenValidators(transportTokenValidators());
        op.setStsProperties(transportSTSProperties());
        op.setEventListener(loggerListener());
        return op;
    }

    @RefreshScope
    @Bean
    public List transportTokenValidators() {
        final List list = new ArrayList<>();
        list.add(transportSamlTokenValidator());
        list.add(new X509TokenValidator());
        return list;
    }

    @RefreshScope
    @Bean
    public List transportTokenProviders() {
        final List list = new ArrayList<>();
        list.add(transportSamlTokenProvider());
        return list;
    }

    @RefreshScope
    @Bean
    public RealmProperties casRealm() {
        final WsFederationProperties.SecurityTokenService wsfed = casProperties.getAuthn().getWsfedIdP().getSts();
        final WsFederationProperties.IdentityProvider idp = casProperties.getAuthn().getWsfedIdP().getIdp();

        final RealmProperties realm = new RealmProperties();
        realm.setIssuer(wsfed.getRealm().getIssuer());
        final Properties p = CryptoUtils.getSecurityProperties(wsfed.getRealm().getKeystoreFile(),
                wsfed.getRealm().getKeystorePassword(),
                wsfed.getRealm().getKeystoreAlias());
        realm.setSignatureCryptoProperties(p);
        realm.setCallbackHandler(new RealmPasswordVerificationCallbackHandler(wsfed.getRealm().getKeyPassword()));
        return realm;
    }


    @RefreshScope
    @Bean
    public Map<String, RealmProperties> realms() {
        final WsFederationProperties.IdentityProvider idp = casProperties.getAuthn().getWsfedIdP().getIdp();
        final Map<String, RealmProperties> realms = new HashMap<>();
        realms.put(idp.getRealmName(), casRealm());
        return realms;
    }

    @RefreshScope
    @Bean
    public SAMLTokenProvider transportSamlTokenProvider() {
        final WsFederationProperties.SecurityTokenService wsfed = casProperties.getAuthn().getWsfedIdP().getSts();

        final DefaultSubjectProvider s = new DefaultSubjectProvider();
        switch (wsfed.getSubjectNameIdFormat().trim().toLowerCase()) {
            case "email":
                s.setSubjectNameIDFormat(NameID.EMAIL);
                break;
            case "entity":
                s.setSubjectNameIDFormat(NameID.ENTITY);
                break;
            case "transient":
                s.setSubjectNameIDFormat(NameID.TRANSIENT);
                break;
            case "unspecified":
            default:
                s.setSubjectNameIDFormat(NameID.UNSPECIFIED);
                break;
        }

        final DefaultConditionsProvider c = new DefaultConditionsProvider();
        c.setAcceptClientLifetime(true);

        final SAMLTokenProvider provider = new SAMLTokenProvider();
        provider.setAttributeStatementProviders(Arrays.asList(new ClaimsAttributeStatementProvider()));
        provider.setRealmMap(realms());
        provider.setConditionsProvider(c);
        provider.setSubjectProvider(s);
        return provider;
    }

    @RefreshScope

    @Bean
    public TokenValidator transportSamlTokenValidator() {
        final WsFederationProperties.IdentityProvider idp = casProperties.getAuthn().getWsfedIdP().getIdp();
        final SAMLTokenValidator v = new SAMLTokenValidator();
        return v;
    }

    @RefreshScope
    @Bean
    public Validator transportUsernameTokenValidator() {
        return new CipheredCredentialsValidator(securityTokenServiceCredentialCipherExecutor());
    }

    @RefreshScope
    @Bean
    public StaticService transportService() {
        final StaticService s = new StaticService();
        s.setEndpoints(Arrays.asList(".*"));
        return s;
    }

    @RefreshScope
    @ConditionalOnMissingBean(name = "transportSTSProperties")
    @Bean
    public STSPropertiesMBean transportSTSProperties() {
        final WsFederationProperties.SecurityTokenService wsfed = casProperties.getAuthn().getWsfedIdP().getSts();
        final WsFederationProperties.IdentityProvider idp = casProperties.getAuthn().getWsfedIdP().getIdp();

        final StaticSTSProperties s = new StaticSTSProperties();
        s.setIssuer(getClass().getSimpleName());
        s.setRealmParser(new UriRealmParser(realms()));
        s.setSignatureCryptoProperties(CryptoUtils.getSecurityProperties(wsfed.getSigningKeystoreFile(), wsfed.getSigningKeystorePassword()));
        s.setEncryptionCryptoProperties(CryptoUtils.getSecurityProperties(wsfed.getEncryptionKeystoreFile(), wsfed.getEncryptionKeystorePassword()));

        final Relationship rel = new Relationship();
        rel.setType(Relationship.FED_TYPE_IDENTITY);
        rel.setSourceRealm(idp.getRealmName());
        rel.setTargetRealm(idp.getRealmName());

        s.setRelationships(Arrays.asList(rel));
        return s;
    }

    @Bean
    @RefreshScope
    public SecurityTokenServiceClientBuilder securityTokenServiceClientBuilder() {
        return new SecurityTokenServiceClientBuilder(casProperties.getAuthn().getWsfedIdP(),
                casProperties.getServer().getPrefix());
    }

    @ConditionalOnMissingBean(name = "securityTokenServiceAuthenticationMetaDataPopulator")
    @Bean
    @RefreshScope
    public AuthenticationMetaDataPopulator securityTokenServiceAuthenticationMetaDataPopulator() {
        return new SecurityTokenServiceAuthenticationMetaDataPopulator(servicesManager,
                wsFederationAuthenticationServiceSelectionStrategy, securityTokenServiceCredentialCipherExecutor(),
                securityTokenServiceClientBuilder());
    }

    @RefreshScope
    @Bean
    public CipherExecutor securityTokenServiceCredentialCipherExecutor() {
        final WsFederationProperties.SecurityTokenService wsfed = casProperties.getAuthn().getWsfedIdP().getSts();
        return new SecurityTokenServiceCredentialCipherExecutor(wsfed.getEncryptionKey(), wsfed.getSigningKey());
    }

    @Bean
    @RefreshScope
    public SecurityTokenTicketFactory securityTokenTicketFactory() {
        return new DefaultSecurityTokenTicketFactory(securityTokenTicketIdGenerator(), grantingTicketExpirationPolicy);
    }

    @ConditionalOnMissingBean(name = "securityTokenTicketIdGenerator")
    @Bean
    @RefreshScope
    public UniqueTicketIdGenerator securityTokenTicketIdGenerator() {
        return new DefaultUniqueTicketIdGenerator();
    }

    @Override
    public void configureAuthenticationExecutionPlan(final AuthenticationEventExecutionPlan plan) {
        plan.registerMetadataPopulator(securityTokenServiceAuthenticationMetaDataPopulator());
    }
}
