package org.apache.archiva.metadata.repository;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.archiva.metadata.model.ArtifactMetadata;
import org.apache.archiva.metadata.model.ProjectMetadata;
import org.apache.archiva.metadata.model.ProjectVersionMetadata;
import org.apache.archiva.metadata.model.ProjectVersionReference;
import org.apache.archiva.metadata.repository.filter.ExcludesFilter;
import org.apache.archiva.metadata.repository.storage.ReadMetadataRequest;
import org.apache.archiva.metadata.repository.storage.RepositoryStorage;
import org.apache.archiva.metadata.repository.storage.RepositoryStorageMetadataInvalidException;
import org.apache.archiva.metadata.repository.storage.RepositoryStorageMetadataNotFoundException;
import org.apache.archiva.metadata.repository.storage.RepositoryStorageRuntimeException;
import org.apache.archiva.redback.components.cache.Cache;
import org.apache.archiva.repository.events.RepositoryListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.inject.Inject;
import javax.inject.Named;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * <p>
 * Default implementation of the metadata resolver API. At present it will handle updating the content repository
 * from new or changed information in the model and artifacts from the repository storage.
 * </p>
 * <p>
 * This is a singleton component to allow an alternate implementation to be provided. It is intended to be the same
 * system-wide for the whole content repository instead of on a per-managed-repository basis. Therefore, the session is
 * passed in as an argument to obtain any necessary resources, rather than the class being instantiated within the
 * session in the context of a single managed repository's resolution needs.
 * </p>
 * <p>
 * Note that the caller is responsible for the session, such as closing and saving (which is implied by the resolver
 * being obtained from within the session). The {@link RepositorySession#markDirty()} method is used as a hint to ensure
 * that the session knows we've made changes at close. We cannot ensure the changes will be persisted if the caller
 * chooses to revert first. This is preferable to storing the metadata immediately - a separate session would require
 * having a bi-directional link with the session factory, and saving the existing session might save other changes
 * unknowingly by the caller.
 * </p>
 */
@Service("metadataResolver#default")
public class DefaultMetadataResolver
    implements MetadataResolver
{

    private Logger log = LoggerFactory.getLogger( DefaultMetadataResolver.class );

    /**
     * <p>
     * FIXME: this needs to be configurable based on storage type - and could also be instantiated per repo. Change to a
     * factory, and perhaps retrieve from the session. We should avoid creating one per request, however.
     * </p>
     * <p>
     * TODO: Also need to accommodate availability of proxy module
     * ... could be a different type since we need methods to modify the storage metadata, which would also allow more
     * appropriate methods to pass in the already determined repository configuration, for example, instead of the ID
     * </p>
     */
    @Inject
    @Named(value = "repositoryStorage#maven2")
    private RepositoryStorage repositoryStorage;

    @Inject
    @Autowired(required = false)
    private List<RepositoryListener> listeners = new ArrayList<>();

    /**
     * Cache used for namespaces
     */
    @Inject
    @Named( value = "cache#namespaces" )
    private Cache<String, Collection<String>> namespacesCache;

    @Override
    public ProjectVersionMetadata resolveProjectVersion( RepositorySession session, String repoId, String namespace,
                                                         String projectId, String projectVersion )
        throws MetadataResolutionException
    {
        MetadataRepository metadataRepository = session.getRepository();

        ProjectVersionMetadata metadata =
            metadataRepository.getProjectVersion( repoId, namespace, projectId, projectVersion );
        // TODO: do we want to detect changes as well by comparing timestamps? isProjectVersionNewerThan(updated)
        //       in such cases we might also remove/update stale metadata, including adjusting plugin-based facets
        //       This would also be better than checking for completeness - we can then refresh only when fixed (though
        //       sometimes this has an additional dependency - such as a parent - requesting the user to force an update
        //       may then work here and be more efficient than always trying again)
        if ( metadata == null || metadata.isIncomplete() )
        {
            try
            {
                ReadMetadataRequest readMetadataRequest =
                    new ReadMetadataRequest().repositoryId( repoId ).namespace( namespace ).projectId(
                        projectId ).projectVersion( projectVersion ).browsingRequest( true );
                metadata = repositoryStorage.readProjectVersionMetadata( readMetadataRequest );

                log.debug( "Resolved project version metadata from storage: {}", metadata );

                // FIXME: make this a more generic post-processing that plugins can take advantage of
                //       eg. maven projects should be able to process parent here
                if ( !metadata.getDependencies().isEmpty() )
                {
                    ProjectVersionReference ref = new ProjectVersionReference();
                    ref.setNamespace( namespace );
                    ref.setProjectId( projectId );
                    ref.setProjectVersion( projectVersion );
                    ref.setReferenceType( ProjectVersionReference.ReferenceType.DEPENDENCY );
                }
                try
                {
                    for ( RepositoryListener listener : listeners )
                    {
                        listener.addArtifact( session, repoId, namespace, projectId, metadata );
                    }
                    metadataRepository.updateProjectVersion( repoId, namespace, projectId, metadata );
                }
                catch ( MetadataRepositoryException e )
                {
                    log.warn( "Unable to persist resolved information: {}", e.getMessage(), e );
                }

                session.markDirty();
            }
            catch ( RepositoryStorageMetadataInvalidException e )
            {
                for ( RepositoryListener listener : listeners )
                {
                    listener.addArtifactProblem( session, repoId, namespace, projectId, projectVersion, e );
                }
                throw new MetadataResolutionException( e.getMessage(), e );
            }
            catch ( RepositoryStorageMetadataNotFoundException e )
            {
                for ( RepositoryListener listener : listeners )
                {
                    listener.addArtifactProblem( session, repoId, namespace, projectId, projectVersion, e );
                }
                // no need to rethrow - return null
            }
            catch ( RepositoryStorageRuntimeException e )
            {
                for ( RepositoryListener listener : listeners )
                {
                    listener.addArtifactProblem( session, repoId, namespace, projectId, projectVersion, e );
                }
                throw new MetadataResolutionException( e.getMessage(), e );
            }

        }
        return metadata;
    }

    @Override
    public Collection<ProjectVersionReference> resolveProjectReferences( RepositorySession session, String repoId,
                                                                         String namespace, String projectId,
                                                                         String projectVersion )
        throws MetadataResolutionException
    {
        // TODO: is this assumption correct? could a storage mech. actually know all references in a non-Maven scenario?
        // not passed to the storage mechanism as resolving references would require iterating all artifacts
        MetadataRepository metadataRepository = session.getRepository();
        return metadataRepository.getProjectReferences( repoId, namespace, projectId, projectVersion );
    }

    @Override
    public Collection<String> resolveRootNamespaces( RepositorySession session, String repoId )
        throws MetadataResolutionException
    {
        try
        {

            Collection<String> namespaces = namespacesCache.get( repoId );
            if ( namespaces != null )
            {
                return namespaces;
            }

            MetadataRepository metadataRepository = session.getRepository();
            namespaces = metadataRepository.getRootNamespaces( repoId );
            Collection<String> storageNamespaces =
                repositoryStorage.listRootNamespaces( repoId, new ExcludesFilter<String>( namespaces ) );
            if ( storageNamespaces != null && !storageNamespaces.isEmpty() )
            {

                log.debug( "Resolved root namespaces from storage: {}", storageNamespaces );

                for ( String n : storageNamespaces )
                {
                    try
                    {
                        metadataRepository.updateNamespace( repoId, n );
                        // just invalidate cache entry
                        String cacheKey = repoId + "-" + n;
                        namespacesCache.remove( cacheKey );
                    }
                    catch ( MetadataRepositoryException e )
                    {
                        log.warn( "Unable to persist resolved information: {}", e.getMessage(), e );
                    }
                }
                session.markDirty();

                namespaces = new ArrayList<>( namespaces );
                namespaces.addAll( storageNamespaces );
            }

            namespacesCache.put( repoId, namespaces );

            return namespaces;
        }
        catch ( RepositoryStorageRuntimeException e )
        {
            throw new MetadataResolutionException( e.getMessage(), e );
        }
    }

    @Override
    public Collection<String> resolveNamespaces( RepositorySession session, String repoId, String namespace )
        throws MetadataResolutionException
    {
        try
        {
            MetadataRepository metadataRepository = session.getRepository();
            String cacheKey = repoId + "-" + namespace;
            Collection<String> namespaces = namespacesCache.get( cacheKey );
            if ( namespaces == null )
            {
                namespaces = metadataRepository.getNamespaces( repoId, namespace );
                namespacesCache.put( cacheKey, namespaces );
            }
            Collection<String> exclusions = new ArrayList<>( namespaces );
            exclusions.addAll( metadataRepository.getProjects( repoId, namespace ) );
            Collection<String> storageNamespaces =
                repositoryStorage.listNamespaces( repoId, namespace, new ExcludesFilter<String>( exclusions ) );
            if ( storageNamespaces != null && !storageNamespaces.isEmpty() )
            {

                log.debug( "Resolved namespaces from storage: {}", storageNamespaces );

                for ( String n : storageNamespaces )
                {
                    try
                    {
                        metadataRepository.updateNamespace( repoId, namespace + "." + n );
                        // just invalidate cache entry
                        cacheKey = repoId + "-" + namespace + "." + n;
                        namespacesCache.remove( cacheKey );
                    }
                    catch ( MetadataRepositoryException e )
                    {
                        log.warn( "Unable to persist resolved information: {}", e.getMessage(), e );
                    }
                }
                session.markDirty();

                namespaces = new ArrayList<>( namespaces );
                namespaces.addAll( storageNamespaces );
            }
            return namespaces;
        }
        catch ( RepositoryStorageRuntimeException e )
        {
            throw new MetadataResolutionException( e.getMessage(), e );
        }
    }

    @Override
    public Collection<String> resolveProjects( RepositorySession session, String repoId, String namespace )
        throws MetadataResolutionException
    {
        try
        {
            MetadataRepository metadataRepository = session.getRepository();
            Collection<String> projects = metadataRepository.getProjects( repoId, namespace );
            Collection<String> exclusions = new ArrayList<>( projects );

            String cacheKey = repoId + "-" + namespace;
            Collection<String> namespaces = namespacesCache.get( cacheKey );
            if ( namespaces == null )
            {
                namespaces = metadataRepository.getNamespaces( repoId, namespace );
                namespacesCache.put( cacheKey, namespaces );
            }

            exclusions.addAll( namespaces );

            Collection<String> storageProjects =
                repositoryStorage.listProjects( repoId, namespace, new ExcludesFilter<>( exclusions ) );
            if ( storageProjects != null && !storageProjects.isEmpty() )
            {

                log.debug( "Resolved projects from storage: {}", storageProjects );
                for ( String projectId : storageProjects )
                {
                    ProjectMetadata projectMetadata =
                        repositoryStorage.readProjectMetadata( repoId, namespace, projectId );
                    if ( projectMetadata != null )
                    {
                        try
                        {
                            metadataRepository.updateProject( repoId, projectMetadata );
                        }
                        catch ( MetadataRepositoryException e )
                        {
                            log.warn( "Unable to persist resolved information: {}", e.getMessage(), e );
                        }
                    }
                }
                session.markDirty();

                projects = new ArrayList<>( projects );
                projects.addAll( storageProjects );
            }
            return projects;
        }
        catch ( RepositoryStorageRuntimeException e )
        {
            throw new MetadataResolutionException( e.getMessage(), e );
        }
    }

    @Override
    public Collection<String> resolveProjectVersions( RepositorySession session, String repoId, String namespace,
                                                      String projectId )
        throws MetadataResolutionException
    {
        try
        {
            MetadataRepository metadataRepository = session.getRepository();

            Collection<String> projectVersions = metadataRepository.getProjectVersions( repoId, namespace, projectId );
            Collection<String> storageProjectVersions =
                repositoryStorage.listProjectVersions( repoId, namespace, projectId,
                                                       new ExcludesFilter<String>( projectVersions ) );
            if ( storageProjectVersions != null && !storageProjectVersions.isEmpty() )
            {
                log.debug( "Resolved project versions from storage: {}", storageProjectVersions );

                for ( String projectVersion : storageProjectVersions )
                {
                    try
                    {
                        ReadMetadataRequest readMetadataRequest =
                            new ReadMetadataRequest().repositoryId( repoId ).namespace( namespace ).projectId(
                                projectId ).projectVersion( projectVersion );
                        ProjectVersionMetadata versionMetadata =
                            repositoryStorage.readProjectVersionMetadata( readMetadataRequest );
                        for ( RepositoryListener listener : listeners )
                        {
                            listener.addArtifact( session, repoId, namespace, projectId, versionMetadata );
                        }

                        metadataRepository.updateProjectVersion( repoId, namespace, projectId, versionMetadata );
                    }
                    catch ( MetadataRepositoryException e )
                    {
                        log.warn( "Unable to persist resolved information: {}", e.getMessage(), e );
                    }
                    catch ( RepositoryStorageMetadataInvalidException e )
                    {
                        log.warn(
                            "Not update project in metadata repository due to an error resolving it from storage: {}",
                            e.getMessage() );

                        for ( RepositoryListener listener : listeners )
                        {
                            listener.addArtifactProblem( session, repoId, namespace, projectId, projectVersion, e );
                        }
                    }
                    catch ( RepositoryStorageMetadataNotFoundException e )
                    {
                        for ( RepositoryListener listener : listeners )
                        {
                            listener.addArtifactProblem( session, repoId, namespace, projectId, projectVersion, e );
                        }
                    }
                }
                session.markDirty();

                projectVersions = new ArrayList<>( projectVersions );
                projectVersions.addAll( storageProjectVersions );
            }
            return projectVersions;
        }
        catch ( RepositoryStorageRuntimeException e )
        {
            throw new MetadataResolutionException( e.getMessage(), e );
        }
    }

    @Override
    public Collection<ArtifactMetadata> resolveArtifacts( RepositorySession session, String repoId, String namespace,
                                                          String projectId, String projectVersion )
        throws MetadataResolutionException
    {
        try
        {
            MetadataRepository metadataRepository = session.getRepository();
            Collection<ArtifactMetadata> artifacts =
                metadataRepository.getArtifacts( repoId, namespace, projectId, projectVersion );
            ExcludesFilter<String> filter = new ExcludesFilter<String>( createArtifactIdList( artifacts ) );

            ReadMetadataRequest readMetadataRequest =
                new ReadMetadataRequest().repositoryId( repoId ).namespace( namespace ).projectId(
                    projectId ).projectVersion( projectVersion ).filter( filter );

            Collection<ArtifactMetadata> storageArtifacts =
                repositoryStorage.readArtifactsMetadata( readMetadataRequest );
            if ( storageArtifacts != null && !storageArtifacts.isEmpty() )
            {

                log.debug( "Resolved artifacts from storage: {}", storageArtifacts );

                for ( ArtifactMetadata artifact : storageArtifacts )
                {
                    try
                    {
                        metadataRepository.updateArtifact( repoId, namespace, projectId, projectVersion, artifact );
                    }
                    catch ( MetadataRepositoryException e )
                    {
                        log.warn( "Unable to persist resolved information: {}", e.getMessage(), e );
                    }
                }
                session.markDirty();

                artifacts = new ArrayList<>( artifacts );
                artifacts.addAll( storageArtifacts );
            }
            return artifacts;
        }
        catch ( RepositoryStorageRuntimeException e )
        {
            for ( RepositoryListener listener : listeners )
            {
                listener.addArtifactProblem( session, repoId, namespace, projectId, projectVersion, e );
            }
            throw new MetadataResolutionException( e.getMessage(), e );
        }
    }

    private Collection<String> createArtifactIdList( Collection<ArtifactMetadata> artifacts )
    {
        Collection<String> artifactIds = new ArrayList<>();
        for ( ArtifactMetadata artifact : artifacts )
        {
            artifactIds.add( artifact.getId() );
        }
        return artifactIds;
    }
}
