package org.apereo.cas.support.saml.mdui;

import com.google.common.base.Throwables;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import org.apereo.cas.support.saml.OpenSamlConfigBean;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver;
import org.opensaml.saml.metadata.resolver.MetadataResolver;
import org.opensaml.saml.metadata.resolver.filter.MetadataFilter;
import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain;
import org.opensaml.saml.metadata.resolver.impl.DOMMetadataResolver;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * This is {@link AbstractMetadataResolverAdapter} that encapsulates
 * commons between static and dynamic resolvers.
 *
 * @author Misagh Moayyed
 * @since 4.1.0
 */
public abstract class AbstractMetadataResolverAdapter implements MetadataResolverAdapter {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMetadataResolverAdapter.class);

    /**
     * Metadata resources along with filters to perform validation.
     */
    protected Map<Resource, MetadataFilterChain> metadataResources;

    /**
     * Whether the metadata resolver should require valid metadata. Default is true.
     */
    protected boolean requireValidMetadata = true;

    /**
     * The openSAML config bean.
     **/
    protected OpenSamlConfigBean configBean;

    private ChainingMetadataResolver metadataResolver;

    private final Object lock = new Object();

    /**
     * Instantiates a new abstract metadata resolver adapter.
     */
    public AbstractMetadataResolverAdapter() {
        this.metadataResources = new HashMap<>();
    }

    /**
     * Instantiates a new static metadata resolver adapter.
     *
     * @param metadataResources the metadata resources
     */
    public AbstractMetadataResolverAdapter(final Map<Resource, MetadataFilterChain> metadataResources) {
        this.metadataResources = metadataResources;
    }

    public void setRequireValidMetadata(final boolean requireValidMetadata) {
        this.requireValidMetadata = requireValidMetadata;
    }

    /**
     * Retrieve the remote source's input stream to parse data.
     *
     * @param resource the resource
     * @param entityId the entity id
     * @return the input stream
     * @throws IOException if stream cannot be read
     */
    protected InputStream getResourceInputStream(final Resource resource, final String entityId) throws IOException {
        LOGGER.debug("Locating metadata resource from input stream.");
        if (!resource.exists() || !resource.isReadable()) {
            throw new FileNotFoundException("Resource does not exist or is unreadable");
        }
        return resource.getInputStream();
    }

    @Override
    public EntityDescriptor getEntityDescriptorForEntityId(final String entityId) {
        try {
            final CriteriaSet criterions = new CriteriaSet(new EntityIdCriterion(entityId));
            if (this.metadataResolver != null) {
                return this.metadataResolver.resolveSingle(criterions);
            }
        } catch (final Exception ex) {
            throw Throwables.propagate(ex);
        }
        return null;

    }

    /**
     * Build metadata resolver aggregate.
     */
    public void buildMetadataResolverAggregate() {
        buildMetadataResolverAggregate(null);
    }

    /**
     * Build metadata resolver aggregate. Loops through metadata resources
     * and attempts to resolve the metadata.
     *
     * @param entityId the entity id
     */
    public void buildMetadataResolverAggregate(final String entityId) {
        try {
            LOGGER.debug("Building metadata resolver aggregate");

            this.metadataResolver = new ChainingMetadataResolver();
            final List<MetadataResolver> resolvers = new ArrayList<>();

            final Set<Map.Entry<Resource, MetadataFilterChain>> entries = this.metadataResources.entrySet();
            entries.forEach(entry -> {
                final Resource resource = entry.getKey();
                LOGGER.debug("Loading [{}]", resource.getFilename());
                resolvers.addAll(loadMetadataFromResource(entry.getValue(), resource, entityId));
            });

            synchronized (this.lock) {
                this.metadataResolver.setId(ChainingMetadataResolver.class.getCanonicalName());
                this.metadataResolver.setResolvers(resolvers);
                LOGGER.info("Collected metadata from [{}] resolvers(s). Initializing aggregate resolver...", resolvers.size());
                this.metadataResolver.initialize();
                LOGGER.info("Metadata aggregate initialized successfully.");
            }

        } catch (final Exception ex) {
            throw Throwables.propagate(ex);
        }
    }

    /**
     * Load metadata from resource.
     *
     * @param metadataFilter the metadata filter
     * @param resource       the resource
     * @param entityId       the entity id
     * @return the list
     */
    private List<MetadataResolver> loadMetadataFromResource(final MetadataFilter metadataFilter,
                                                            final Resource resource, final String entityId) {
        LOGGER.debug("Evaluating metadata resource [{}]", resource.getFilename());
        try (InputStream in = getResourceInputStream(resource, entityId)) {
            if (in.available() > 0 || in.markSupported()) {
                LOGGER.debug("Parsing [{}]", resource.getFilename());
                final Document document = this.configBean.getParserPool().parse(in);
                return buildSingleMetadataResolver(metadataFilter, resource, document);
            }
            LOGGER.warn("Input stream from resource [{}] appears empty. Moving on...", resource.getFilename());
        } catch (final Exception e) {
            LOGGER.warn("Could not retrieve input stream from resource. Moving on...", e);
        }
        return new ArrayList<>();
    }

    /**
     * Build single metadata resolver.
     *
     * @param metadataFilterChain the metadata filters chained together
     * @param resource            the resource
     * @param document            the xml document to parse
     * @return list of resolved metadata from resources.
     * @throws IOException the iO exception
     */
    private List<MetadataResolver> buildSingleMetadataResolver(final MetadataFilter metadataFilterChain,
                                                               final Resource resource, final Document document) throws IOException {
        try {
            final Element metadataRoot = document.getDocumentElement();
            final DOMMetadataResolver metadataProvider = new DOMMetadataResolver(metadataRoot);

            metadataProvider.setParserPool(this.configBean.getParserPool());
            metadataProvider.setFailFastInitialization(true);
            metadataProvider.setRequireValidMetadata(this.requireValidMetadata);
            metadataProvider.setId(metadataProvider.getClass().getCanonicalName());
            if (metadataFilterChain != null) {
                metadataProvider.setMetadataFilter(metadataFilterChain);
            }
            LOGGER.debug("Initializing metadata resolver for [{}]", resource);
            metadataProvider.initialize();

            final List<MetadataResolver> resolvers = new ArrayList<>();
            resolvers.add(metadataProvider);
            return resolvers;
        } catch (final Exception ex) {
            LOGGER.warn("Could not initialize metadata resolver. Resource will be ignored", ex);
        }
        return new ArrayList<>();
    }

    public void setMetadataResources(final Map<Resource, MetadataFilterChain> metadataResources) {
        this.metadataResources = metadataResources;
    }

    public void setConfigBean(final OpenSamlConfigBean configBean) {
        this.configBean = configBean;
    }
}
