//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.start.fileinits;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.xml.parsers.ParserConfigurationException;

import org.eclipse.jetty.start.BaseHome;
import org.eclipse.jetty.start.FS;
import org.eclipse.jetty.start.StartLog;
import org.eclipse.jetty.start.Utils;
import org.xml.sax.SAXException;

/**
 * Attempt to download a <code>maven://</code> URI, by first attempting to find
 * the resource in the maven repository system (starting with local, then
 * central)
 * <p>
 * Valid URI Formats:
 * <dl>
 * <dt>{@code maven://<groupId>/<artifactId>/<version>}</dt>
 * <dd>minimum requirement (type defaults to <code>jar</code>, with no classifier)</dd>
 * <dt>{@code maven://<groupId>/<artifactId>/<version>/<type>}</dt>
 * <dd>optional type requirement</dd>
 * <dt>{@code maven://<groupId>/<artifactId>/<version>/<type>/<classifier>}</dt>
 * <dd>optional type and classifier requirement</dd>
 * </dl>
 */
public class MavenLocalRepoFileInitializer extends DownloadFileInitializer
{
    public static class Coordinates
    {
        public String groupId;
        public String artifactId;
        public String version;
        public String type;
        public String classifier;
        private String mavenRepoUri = DEFAULT_REMOTE_REPO;

        public String toPath()
        {
            return toActualPath(version);
        }

        private String toActualPath(String actualVersion)
        {
            StringBuilder pathlike = new StringBuilder();
            pathlike.append(groupId.replace('.', '/'));
            pathlike.append('/').append(artifactId);
            pathlike.append('/').append(version);
            pathlike.append('/').append(artifactId);
            pathlike.append('-').append(actualVersion);
            if (classifier != null)
            {
                pathlike.append('-').append(classifier);
            }
            pathlike.append('.').append(type);
            return pathlike.toString();
        }

        public String toMetadataPath()
        {
            StringBuilder pathLike = new StringBuilder();
            pathLike.append(groupId.replace('.', '/'));
            pathLike.append('/').append(artifactId);
            pathLike.append('/').append(version);
            pathLike.append("/maven-metadata.xml");

            return pathLike.toString();
        }

        public URI toCentralURI()
        {
            return URI.create(mavenRepoUri + toPath());
        }

        public URI toCentralURI(String actualVersion)
        {
            return URI.create(mavenRepoUri + toActualPath(actualVersion));
        }

        public URI toSnapshotMetadataXmlURI()
        {
            return URI.create(mavenRepoUri + toMetadataPath());
        }

        @Override
        public String toString()
        {
            StringBuilder str = new StringBuilder();
            str.append(groupId);
            str.append(':').append(artifactId);
            str.append(':').append(version);
            str.append(':').append(type);
            if (classifier != null)
                str.append(':').append(classifier);
            return str.toString();
        }
    }

    private final Path localRepositoryDir;

    private static final String DEFAULT_REMOTE_REPO = "https://repo1.maven.org/maven2/";

    private final boolean readonly;
    private final String mavenRepoUri;
    private boolean offline;

    public MavenLocalRepoFileInitializer(BaseHome baseHome)
    {
        this(baseHome, null, true);
    }

    public MavenLocalRepoFileInitializer(BaseHome baseHome, Path localRepoDir, boolean readonly)
    {
        this(baseHome, localRepoDir, readonly, null);
    }

    public MavenLocalRepoFileInitializer(BaseHome baseHome, Path localRepoDir, boolean readonly, String mavenRepoUri)
    {
        super(baseHome, "maven");
        this.localRepositoryDir = localRepoDir != null ? localRepoDir : newTempRepo();
        this.readonly = readonly;
        this.mavenRepoUri = mavenRepoUri;
    }

    @Override
    protected boolean allowInsecureHttpDownloads()
    {
        // Always allow insecure http downloads in this file initializer.

        // The user is either using the DEFAULT_REMOTE_REPO, or has redeclared it to a new URI.
        // If the `maven.repo.uri` property has been changed from default, this indicates a change
        // to a different maven uri, overwhelmingly pointing to a maven repository manager
        // like artifactory or nexus.   This is viewed as an intentional decision by the
        // user and as such we should not put additional hurdles in their way.
        return true;
    }

    private static Path newTempRepo()
    {
        Path javaTempDir = Paths.get(System.getProperty("java.io.tmpdir"));
        // Simple return here, don't create the directory, unless it's being used.
        return javaTempDir.resolve("jetty-start-downloads");
    }

    @Override
    public boolean create(URI uri, String location) throws IOException
    {
        Coordinates coords = getCoordinates(uri);
        if (coords == null)
        {
            // Skip, not a maven:// URI
            return false;
        }

        URI destURI = URI.create(location);
        if (destURI.isAbsolute() && destURI.getScheme().equals("extract"))
        {
            // Extract Flow

            // Download to local repo.
            Path localFile = localRepositoryDir.resolve(coords.toPath());
            if (!FS.canReadFile(localFile))
            {
                if (offline)
                {
                    StartLog.warn("Maven is offline, but Local Maven Repo does not contain: %s%n", coords);
                    throw new IllegalStateException("Maven is offline, but Local Maven Repo does not contain: %s%n".formatted(coords));
                }
                else
                {
                    if (FS.ensureDirectoryExists(localFile.getParent()))
                        StartLog.info("mkdir %s", _basehome.toShortForm(localFile.getParent()));
                    download(coords, localFile);
                    if (!FS.canReadFile(localFile))
                    {
                        throw new IOException("Unable to establish temp copy of file to extract: " + localFile);
                    }
                }
            }

            // Destination Directory
            Path destination;
            String extractLocation = destURI.getSchemeSpecificPart();
            if (extractLocation.equals("/"))
            {
                destination = _basehome.getBasePath();
            }
            else
            {
                extractLocation = extractLocation.replaceFirst("^[/\\\\]*", "");
                if (!extractLocation.endsWith("/"))
                    throw new IOException("Extract mode can only unpack to a directory, end your URL with a slash: " + location);
                destination = _basehome.getBasePath().resolve(extractLocation);

                if (Files.exists(destination) && !Files.isDirectory(destination))
                    throw new IOException("Destination already exists, and is not a directory: " + destination);

                if (!destination.startsWith(_basehome.getBasePath()))
                    throw new IOException("For security reasons, Jetty start is unable to extract outside of the ${jetty.base} - " + location);
            }

            FS.extract(localFile, destination);
        }
        else
        {
            // Copy Flow
            Path destination = getDestination(uri, location);
            if (isFilePresent(destination))
                return false;

            // Grab copy from local repository (download if needed to local repository)
            Path localRepoFile = getLocalRepoFile(coords);

            if (localRepoFile != null)
            {
                if (FS.ensureDirectoryExists(destination.getParent()))
                    StartLog.info("mkdir %s", _basehome.toShortForm(destination.getParent()));
                StartLog.info("copy %s to %s", localRepoFile, _basehome.toShortForm(destination));
                Files.copy(localRepoFile, destination);
                return true;
            }

            if (offline)
            {
                StartLog.warn("Maven is offline, but Local Maven Repo does not contain: %s%n", coords);
                throw new IllegalStateException("Maven is offline, but Local Maven Repo does not contain: %s%n".formatted(coords));
            }
            else
            {
                // normal non-local repo version
                download(coords, destination);
            }
        }

        return true;
    }

    private Path getLocalRepoFile(Coordinates coords) throws IOException
    {
        Path localFile = localRepositoryDir.resolve(coords.toPath());
        if (FS.canReadFile(localFile))
            return localFile;

        // Download, if needed
        if (!readonly)
        {
            if (offline)
            {
                StartLog.warn("Maven is offline, but Local Maven Repo does not contain: %s%n", coords);
                throw new IllegalStateException("Maven is offline, but Local Maven Repo does not contain: %s%n".formatted(coords));
            }
            download(coords, localFile);
            return localFile;
        }

        return null;
    }

    public String getRemoteUri()
    {
        if (this.mavenRepoUri != null)
        {
            return this.mavenRepoUri;
        }
        else
        {
            return System.getProperty("maven.repo.uri", DEFAULT_REMOTE_REPO);
        }
    }

    public Coordinates getCoordinates(URI uri)
    {
        if (!"maven".equalsIgnoreCase(uri.getScheme()))
        {
            return null;
        }

        String ssp = uri.getSchemeSpecificPart();

        if (ssp.startsWith("//"))
        {
            ssp = ssp.substring(2);
        }

        String[] parts = ssp.split("/");

        if (StartLog.isDebugEnabled())
        {
            StartLog.debug("ssp = %s", ssp);
            StartLog.debug("parts = %d", parts.length);
            for (int i = 0; i < parts.length; i++)
            {
                StartLog.debug("  part[%2d]: [%s]", i, parts[i]);
            }
        }

        if (parts.length < 3)
        {
            throw new RuntimeException("Not a valid maven:// uri - " + uri);
        }

        Coordinates coords = new Coordinates();
        coords.groupId = parts[0];
        coords.artifactId = parts[1];
        coords.version = parts[2];
        coords.type = "jar";
        coords.classifier = null;
        coords.mavenRepoUri = getRemoteUri();

        if (parts.length >= 4)
        {
            if (Utils.isNotBlank(parts[3]))
            {
                coords.type = parts[3];
            }

            if ((parts.length == 5) && (Utils.isNotBlank(parts[4])))
            {
                coords.classifier = parts[4];
            }
        }

        return coords;
    }

    protected void download(Coordinates coords, Path destination)
        throws IOException
    {
        if (offline)
        {
            StartLog.warn("Maven configuration is set to offline, unable to download: %s", coords);
            return;
        }

        if (coords.version.endsWith("-SNAPSHOT"))
        {
            Path localRepoMetadataPath = localRepositoryDir.resolve(coords.toMetadataPath());
            if (isMetadataStale(localRepoMetadataPath))
            {
                // Grab a fresh copy of the metadata xml
                URI mavenMetadataURI = coords.toSnapshotMetadataXmlURI();
                super.download(mavenMetadataURI, localRepoMetadataPath);
            }

            if (Files.exists(localRepoMetadataPath))
            {
                // parse metadata to get actual SNAPSHOT version
                String actualVersion = getMetadataVersion(localRepoMetadataPath, coords);
                super.download(coords.toCentralURI(actualVersion), destination);
            }
        }
        else
        {
            super.download(coords.toCentralURI(), destination);
        }
    }

    private boolean isMetadataStale(Path localRepoMetadataPath)
    {
        if (!Files.exists(localRepoMetadataPath))
        {
            // doesn't exist, it's stale.
            return true;
        }

        try
        {
            MavenMetadata mavenMetadata = new MavenMetadata(localRepoMetadataPath);
            return MavenMetadata.isExpiredTimestamp(mavenMetadata.getLastUpdated());
        }
        catch (IOException | ParserConfigurationException | SAXException e)
        {
            return true;
        }
    }

    private String getMetadataVersion(Path localRepoMetadataPath, Coordinates coords) throws IOException
    {
        try
        {
            MavenMetadata mavenMetadata = new MavenMetadata(localRepoMetadataPath);
            MavenMetadata.Snapshot snapshot = mavenMetadata.getSnapshot(coords.classifier, coords.type);
            return snapshot.value;
        }
        catch (IOException | ParserConfigurationException | SAXException e)
        {
            throw new IOException("Unable to parse " + localRepoMetadataPath, e);
        }
    }

    /**
     * protected only for testing purpose
     *
     * @param uri the uri to download
     * @param destination the destination File
     */
    @Override
    protected void download(URI uri, Path destination)
        throws IOException
    {
        super.download(uri, destination);
    }

    public MavenLocalRepoFileInitializer offline(boolean offline)
    {
        this.offline = offline;
        return this;
    }
}
