package org.apereo.cas.util;

import com.google.common.base.Throwables;
import org.apache.commons.lang3.StringUtils;
import org.apereo.cas.configuration.model.support.saml.sps.AbstractSamlSPProperties;
import org.apereo.cas.configuration.support.Beans;
import org.apereo.cas.services.ChainingAttributeReleasePolicy;
import org.apereo.cas.services.PrincipalAttributeRegisteredServiceUsernameProvider;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.ReturnMappedAttributeReleasePolicy;
import org.apereo.cas.services.ServicesManager;
import org.apereo.cas.support.saml.services.SamlRegisteredService;
import org.apereo.cas.support.saml.services.idp.metadata.cache.SamlRegisteredServiceCachingMetadataResolver;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver;
import org.opensaml.saml.metadata.resolver.filter.impl.PredicateFilter;
import org.opensaml.saml.metadata.resolver.impl.AbstractBatchMetadataResolver;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.StreamSupport;

/**
 * This is {@link SamlSPUtils}.
 *
 * @author Misagh Moayyed
 * @since 5.0.0
 */
public final class SamlSPUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(SamlSPUtils.class);

    private SamlSPUtils() {
    }

    /**
     * New saml service provider registration.
     *
     * @param sp       the properties
     * @param resolver the resolver
     * @return the saml registered service
     */
    public static SamlRegisteredService newSamlServiceProviderService(final AbstractSamlSPProperties sp,
                                                                      final SamlRegisteredServiceCachingMetadataResolver resolver) {
        if (StringUtils.isBlank(sp.getMetadata())) {
            LOGGER.debug("Skipped registration of [{}] since no metadata location is found", sp.getName());
            return null;
        }

        try {
            final SamlRegisteredService service = new SamlRegisteredService();
            service.setName(sp.getName());
            service.setDescription(sp.getDescription());
            service.setEvaluationOrder(Integer.MIN_VALUE);
            service.setMetadataLocation(sp.getMetadata());

            final List<String> attributesToRelease = new ArrayList<>(sp.getAttributes());
            if (StringUtils.isNotBlank(sp.getNameIdAttribute())) {
                attributesToRelease.add(sp.getNameIdAttribute());
                service.setUsernameAttributeProvider(new PrincipalAttributeRegisteredServiceUsernameProvider(sp.getNameIdAttribute()));
            }
            if (StringUtils.isNotBlank(sp.getNameIdFormat())) {
                service.setRequiredNameIdFormat(sp.getNameIdFormat());
            }

            final Map<String, String> attributes = Beans.transformPrincipalAttributesListIntoMap(attributesToRelease);
            final ChainingAttributeReleasePolicy policy = new ChainingAttributeReleasePolicy();
            policy.addPolicy(new ReturnMappedAttributeReleasePolicy(attributes));
            service.setAttributeReleasePolicy(policy);
            
            service.setMetadataCriteriaRoles(SPSSODescriptor.DEFAULT_ELEMENT_NAME.getLocalPart());
            service.setMetadataCriteriaRemoveEmptyEntitiesDescriptors(true);
            service.setMetadataCriteriaRemoveRolelessEntityDescriptors(true);

            if (StringUtils.isNotBlank(sp.getSignatureLocation())) {
                service.setMetadataSignatureLocation(sp.getSignatureLocation());
            }

            final List<String> entityIDList = sp.getEntityIds();
            if (entityIDList.isEmpty()) {
                final ChainingMetadataResolver chainingResolver = resolver.resolve(service);
                if (chainingResolver.getResolvers().isEmpty()) {
                    LOGGER.warn("Skipped registration of [{}] since no metadata resolver could be constructed", sp.getName());
                    return null;
                }

                chainingResolver.getResolvers().forEach(r -> {
                    if (r instanceof AbstractBatchMetadataResolver) {
                        final Iterator<EntityDescriptor> it = ((AbstractBatchMetadataResolver) r).iterator();
                        final Optional<EntityDescriptor> descriptor =
                                StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.ORDERED), false)
                                        .filter(e -> e.getSPSSODescriptor(SAMLConstants.SAML20P_NS) != null)
                                        .findFirst();
                        if (descriptor.isPresent()) {
                            entityIDList.add(descriptor.get().getEntityID());
                        } else {
                            LOGGER.warn("Skipped registration of [{}] since no entity id could be found", sp.getName());
                        }
                    }
                });
            }

            if (entityIDList.isEmpty()) {
                LOGGER.warn("Skipped registration of [{}] since no metadata entity ids could be found", sp.getName());
                return null;
            }
            final String entityIds = org.springframework.util.StringUtils.collectionToDelimitedString(entityIDList, "|");
            service.setMetadataCriteriaDirection(PredicateFilter.Direction.INCLUDE.name());
            service.setMetadataCriteriaPattern(entityIds);

            LOGGER.debug("Registering saml service [{}] by entity id [{}]", sp.getName(), entityIds);
            service.setServiceId(entityIds);

            service.setSignAssertions(sp.isSignAssertions());
            service.setSignResponses(sp.isSignResponses());
            
            return service;
        } catch (final Exception e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Save service only if it's not already found in the registry.
     *
     * @param service         the service
     * @param servicesManager the services manager
     */
    public static void saveService(final RegisteredService service, final ServicesManager servicesManager) {
        servicesManager.load();

        if (servicesManager.findServiceBy(registeredService -> registeredService instanceof SamlRegisteredService
                && registeredService.getServiceId().equals(service.getServiceId())) != null) {
            LOGGER.info("Service [{}] does not exist in the registry and will be added.", service.getServiceId());
            servicesManager.save(service);
            servicesManager.load();
        } else {
            LOGGER.info("Service [{}] exists in the registry and will not be added again.", service.getServiceId());
        }
    }
}
