package org.apache.archiva.consumers.core.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.common.utils.VersionUtil;
import org.apache.archiva.metadata.model.ArtifactMetadata;
import org.apache.archiva.metadata.model.facets.AuditEvent;
import org.apache.archiva.metadata.model.maven2.MavenArtifactFacet;
import org.apache.archiva.metadata.repository.MetadataRepository;
import org.apache.archiva.metadata.repository.MetadataRepositoryException;
import org.apache.archiva.metadata.repository.MetadataResolutionException;
import org.apache.archiva.metadata.repository.RepositorySession;
import org.apache.archiva.model.ArtifactReference;
import org.apache.archiva.repository.ContentNotFoundException;
import org.apache.archiva.repository.ManagedRepositoryContent;
import org.apache.archiva.repository.events.RepositoryListener;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Base class for all repository purge tasks.
 */
public abstract class AbstractRepositoryPurge
    implements RepositoryPurge
{
    protected Logger log = LoggerFactory.getLogger( getClass( ) );

    protected final ManagedRepositoryContent repository;

    protected final RepositorySession repositorySession;

    protected final List<RepositoryListener> listeners;

    private Logger logger = LoggerFactory.getLogger( "org.apache.archiva.AuditLog" );

    private static final char DELIM = ' ';

    public AbstractRepositoryPurge( ManagedRepositoryContent repository, RepositorySession repositorySession,
                                    List<RepositoryListener> listeners )
    {
        this.repository = repository;
        this.repositorySession = repositorySession;
        this.listeners = listeners;
    }

    /*
     * We have to track namespace, project, project version, artifact version and classifier
     * There is no metadata class that contains all these properties.
     */
    class ArtifactInfo
    {
        final String namespace;
        final String name;
        final String projectVersion;
        String version;
        String classifier;

        ArtifactInfo( String namespace, String name, String projectVersion, String version )
        {
            this.namespace = namespace;
            this.name = name;
            this.projectVersion = projectVersion;
            this.version = version;
        }

        ArtifactInfo( String namespace, String name, String projectVersion )
        {
            this.namespace = namespace;
            this.name = name;
            this.projectVersion = projectVersion;
        }

        /*
         * Creates a info object without version and classifier
         */
        ArtifactInfo projectVersionLevel( )
        {
            return new ArtifactInfo( this.namespace, this.name, this.projectVersion );
        }

        public void setClassifier( String classifier )
        {
            this.classifier = classifier;
        }

        public String getNamespace( )
        {
            return namespace;
        }

        public String getName( )
        {
            return name;
        }

        public String getProjectVersion( )
        {
            return projectVersion;
        }

        public String getVersion( )
        {
            return version;
        }

        public String getClassifier( )
        {
            return classifier;
        }

        public boolean hasClassifier( )
        {
            return classifier != null && !"".equals( classifier );
        }

        @Override
        public boolean equals( Object o )
        {
            if ( this == o ) return true;
            if ( o == null || getClass( ) != o.getClass( ) ) return false;

            ArtifactInfo that = (ArtifactInfo) o;

            if ( !namespace.equals( that.namespace ) ) return false;
            if ( !name.equals( that.name ) ) return false;
            if ( !projectVersion.equals( that.projectVersion ) ) return false;
            if ( !( version != null ? version.equals( that.version ) : that.version == null ) ) return false;
            return classifier != null ? classifier.equals( that.classifier ) : that.classifier == null;
        }

        @Override
        public int hashCode( )
        {
            int result = namespace.hashCode( );
            result = 31 * result + name.hashCode( );
            result = 31 * result + projectVersion.hashCode( );
            result = 31 * result + ( version != null ? version.hashCode( ) : 0 );
            result = 31 * result + ( classifier != null ? classifier.hashCode( ) : 0 );
            return result;
        }

        @Override
        public String toString( )
        {
            final StringBuilder sb = new StringBuilder( "ArtifactInfo{" );
            sb.append( "namespace='" ).append( namespace ).append( '\'' );
            sb.append( ", name='" ).append( name ).append( '\'' );
            sb.append( ", projectVersion='" ).append( projectVersion ).append( '\'' );
            sb.append( ", version='" ).append( version ).append( '\'' );
            sb.append( ", classifier='" ).append( classifier ).append( '\'' );
            sb.append( '}' );
            return sb.toString( );
        }
    }

    /**
     * Purge the repo. Update db and index of removed artifacts.
     *
     * @param references
     */
    protected void purge( Set<ArtifactReference> references )
    {
        if ( references != null && !references.isEmpty( ) )
        {
            MetadataRepository metadataRepository = repositorySession.getRepository( );
            Map<ArtifactInfo, ArtifactMetadata> metaRemovalList = new HashMap<>( );
            Map<String, Collection<ArtifactMetadata>> metaResolved = new HashMap<>( );
            for ( ArtifactReference reference : references )
            {
                String baseVersion = VersionUtil.getBaseVersion( reference.getVersion( ) );
                // Needed for tracking in the hashmap
                String metaBaseId = reference.getGroupId( ) + "/" + reference.getArtifactId( ) + "/" + baseVersion;

                if ( !metaResolved.containsKey( metaBaseId ) )
                {
                    try
                    {
                        metaResolved.put( metaBaseId, metadataRepository.getArtifacts( repository.getId( ), reference.getGroupId( ),
                            reference.getArtifactId( ), baseVersion ) );
                    }
                    catch ( MetadataResolutionException e )
                    {
                        log.error( "Error during metadata retrieval {}: {}", metaBaseId, e.getMessage( ) );
                    }
                }
                Path artifactFile = repository.toFile( reference ).toPath( );

                for ( RepositoryListener listener : listeners )
                {
                    listener.deleteArtifact( metadataRepository, repository.getId( ), reference.getGroupId( ),
                        reference.getArtifactId( ), reference.getVersion( ),
                        artifactFile.getFileName( ).toString( ) );
                }
                try
                {
                    Files.delete( artifactFile );
                    log.debug( "File deleted: {}", artifactFile.toAbsolutePath( ) );
                }
                catch ( IOException e )
                {
                    log.error( "Could not delete file {}: {}", artifactFile.toAbsolutePath( ), e.getMessage( ), e );
                    continue;
                }
                try
                {
                    repository.deleteArtifact( reference );
                }
                catch ( ContentNotFoundException e )
                {
                    log.warn( "skip error deleting artifact {}: {}", reference, e.getMessage( ) );
                }

                boolean snapshotVersion = VersionUtil.isSnapshot( reference.getVersion( ) );


                // If this is a snapshot we have to search for artifacts with the same version. And remove all of them.
                if ( snapshotVersion )
                {
                    Collection<ArtifactMetadata> artifacts =
                        metaResolved.get( metaBaseId );
                    if ( artifacts != null )
                    {
                        // cleanup snapshots metadata
                        for ( ArtifactMetadata artifactMetadata : artifacts )
                        {
                            // Artifact metadata and reference version should match.
                            if ( artifactMetadata.getVersion( ).equals( reference.getVersion( ) ) )
                            {
                                ArtifactInfo info = new ArtifactInfo( artifactMetadata.getNamespace( ), artifactMetadata.getProject( ), artifactMetadata.getProjectVersion( ), artifactMetadata.getVersion( ) );
                                if ( StringUtils.isNotBlank( reference.getClassifier( ) ) )
                                {
                                    info.setClassifier( reference.getClassifier( ) );
                                    metaRemovalList.put( info, artifactMetadata );
                                }
                                else
                                {
                                    // metadataRepository.removeArtifact( artifactMetadata, baseVersion );
                                    metaRemovalList.put( info, artifactMetadata );
                                }
                            }
                        }
                    }
                }
                else // otherwise we delete the artifact version
                {
                    ArtifactInfo info = new ArtifactInfo( reference.getGroupId( ), reference.getArtifactId( ), baseVersion, reference.getVersion( ) );
                    for ( ArtifactMetadata metadata : metaResolved.get( metaBaseId ) )
                    {
                        metaRemovalList.put( info, metadata );
                    }
                }
                triggerAuditEvent( repository.getRepository( ).getId( ), ArtifactReference.toKey( reference ),
                    AuditEvent.PURGE_ARTIFACT );
                purgeSupportFiles( artifactFile );
            }
            purgeMetadata( metadataRepository, metaRemovalList );
            repositorySession.save( );

        }
    }

    /*
     * Purges the metadata. First removes the artifacts. After that empty versions will be removed.
     */
    private void purgeMetadata( MetadataRepository metadataRepository, Map<ArtifactInfo, ArtifactMetadata> dataList )
    {
        Set<ArtifactInfo> projectLevelMetadata = new HashSet<>( );
        for ( Map.Entry<ArtifactInfo, ArtifactMetadata> infoEntry : dataList.entrySet( ) )
        {
            ArtifactInfo info = infoEntry.getKey( );
            try
            {
                removeArtifact( metadataRepository, info, infoEntry.getValue( ) );
                log.debug( "Removed artifact from MetadataRepository {}", info );
            }
            catch ( MetadataRepositoryException e )
            {
                log.error( "Could not remove artifact from MetadataRepository {}: {}", info, e.getMessage( ), e );
            }
            projectLevelMetadata.add( info.projectVersionLevel( ) );
        }
        metadataRepository.save( );
        Collection<ArtifactMetadata> artifacts = null;
        // Get remaining artifacts and remove project if empty
        for ( ArtifactInfo info : projectLevelMetadata )
        {
            try
            {
                artifacts = metadataRepository.getArtifacts( repository.getId( ), info.getNamespace( ), info.getName( ),
                    info.getProjectVersion( ) );
                if ( artifacts.size( ) == 0 )
                {
                    metadataRepository.removeProjectVersion( repository.getId( ), info.getNamespace( ),
                        info.getName( ), info.getProjectVersion( ) );
                    log.debug( "Removed project version from MetadataRepository {}", info );
                }
            }
            catch ( MetadataResolutionException | MetadataRepositoryException e )
            {
                log.error( "Could not remove project version from MetadataRepository {}: {}", info, e.getMessage( ), e );
            }
        }
        metadataRepository.save( );

    }

    /*
     * Removes the artifact from the metadataRepository. If a classifier is set, the facet will be removed.
     */
    private void removeArtifact( MetadataRepository metadataRepository, ArtifactInfo artifactInfo, ArtifactMetadata artifactMetadata ) throws MetadataRepositoryException
    {
        if ( artifactInfo.hasClassifier( ) )
        {
            // cleanup facet which contains classifier information
            MavenArtifactFacet mavenArtifactFacet =
                (MavenArtifactFacet) artifactMetadata.getFacet(
                    MavenArtifactFacet.FACET_ID );

            if ( StringUtils.equals( artifactInfo.classifier,
                mavenArtifactFacet.getClassifier( ) ) )
            {
                artifactMetadata.removeFacet( MavenArtifactFacet.FACET_ID );
                String groupId = artifactInfo.getNamespace( ), artifactId =
                    artifactInfo.getName( ),
                    version = artifactInfo.getProjectVersion( );
                MavenArtifactFacet mavenArtifactFacetToCompare = new MavenArtifactFacet( );
                mavenArtifactFacetToCompare.setClassifier( artifactInfo.getClassifier( ) );
                metadataRepository.removeArtifact( repository.getId( ), groupId, artifactId,
                    version, mavenArtifactFacetToCompare );
                metadataRepository.save( );
            }
        }
        else
        {
            metadataRepository.removeArtifact( artifactMetadata, artifactInfo.getProjectVersion( ) );
        }
    }

    private void deleteSilently( Path path )
    {
        try
        {
            Files.deleteIfExists( path );
            triggerAuditEvent( repository.getRepository( ).getId( ), path.toString( ), AuditEvent.PURGE_FILE );
        }
        catch ( IOException e )
        {
            log.error( "Error occured during file deletion {}: {} ", path, e.getMessage( ), e );
        }
    }

    /**
     * <p>
     * This find support files for the artifactFile and deletes them.
     * </p>
     * <p>
     * Support Files are things like ".sha1", ".md5", ".asc", etc.
     * </p>
     *
     * @param artifactFile the file to base off of.
     */
    private void purgeSupportFiles( Path artifactFile )
    {
        Path parentDir = artifactFile.getParent( );

        if ( !Files.exists( parentDir ) )
        {
            return;
        }

        final String artifactName = artifactFile.getFileName( ).toString( );

        try
        {
            deleteArtifactFiles( parentDir, 3, artifactName );
        }
        catch ( IOException e )
        {
            log.error( "Purge of support files failed {}: {}", artifactFile, e.getMessage( ), e );
        }

    }

    public static void deleteArtifactFiles(final Path directory, final int maxDepth, final String artifactName) throws IOException
    {
            Files.walkFileTree(directory, new HashSet<FileVisitOption>(  ), maxDepth, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile( Path file, BasicFileAttributes attrs) throws IOException {
                    if (file.getFileName().toString().startsWith(artifactName)) {
                        try {
                            Files.delete( file );
                        } catch (IOException e) {
                            // We do not stop, if an error occurs.
                        }
                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    return FileVisitResult.CONTINUE;
                }

            });
    }

    private void triggerAuditEvent( String repoId, String resource, String action )
    {
        String msg =
            repoId + DELIM + "<system-purge>" + DELIM + "<system>" + DELIM + '\"' + resource + '\"' + DELIM + '\"' +
                action + '\"';

        logger.info( msg );
    }
}
