/*
 * 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.
 */
package org.apache.maven.plugins.site.deploy;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.manager.WagonManager;
import org.apache.maven.doxia.site.inheritance.URIPathDescriptor;
import org.apache.maven.doxia.tools.SiteTool;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.DistributionManagement;
import org.apache.maven.model.Site;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.site.AbstractSiteMojo;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
import org.apache.maven.settings.crypto.SettingsDecrypter;
import org.apache.maven.settings.crypto.SettingsDecryptionResult;
import org.apache.maven.wagon.CommandExecutionException;
import org.apache.maven.wagon.CommandExecutor;
import org.apache.maven.wagon.ConnectionException;
import org.apache.maven.wagon.ResourceDoesNotExistException;
import org.apache.maven.wagon.TransferFailedException;
import org.apache.maven.wagon.UnsupportedProtocolException;
import org.apache.maven.wagon.Wagon;
import org.apache.maven.wagon.authentication.AuthenticationException;
import org.apache.maven.wagon.authentication.AuthenticationInfo;
import org.apache.maven.wagon.authorization.AuthorizationException;
import org.apache.maven.wagon.observers.Debug;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.repository.Repository;
import org.codehaus.plexus.PlexusConstants;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.context.Context;
import org.codehaus.plexus.context.ContextException;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;

/**
 * Abstract base class for deploy mojos.
 * Since 2.3 this includes {@link SiteStageMojo} and {@link SiteStageDeployMojo}.
 *
 * @author ltheussl
 * @since 2.3
 */
public abstract class AbstractDeployMojo extends AbstractSiteMojo implements Contextualizable {
    /**
     * Directory containing the generated project sites and report distributions.
     *
     * @since 2.3
     */
    @Parameter(alias = "outputDirectory", defaultValue = "${project.reporting.outputDirectory}", required = true)
    private File inputDirectory;

    /**
     * Whether to run the "chmod" command on the remote site after the deploy.
     * Defaults to "true".
     *
     * @since 2.1
     */
    @Parameter(property = "maven.site.chmod", defaultValue = "true")
    private boolean chmod;

    /**
     * The mode used by the "chmod" command. Only used if chmod = true.
     * Defaults to "g+w,a+rX".
     *
     * @since 2.1
     */
    @Parameter(property = "maven.site.chmod.mode", defaultValue = "g+w,a+rX")
    private String chmodMode;

    /**
     * The options used by the "chmod" command. Only used if chmod = true.
     * Defaults to "-Rf".
     *
     * @since 2.1
     */
    @Parameter(property = "maven.site.chmod.options", defaultValue = "-Rf")
    private String chmodOptions;

    /**
     * Set this to 'true' to skip site deployment.
     *
     * @since 3.0
     */
    @Parameter(property = "maven.site.deploy.skip", defaultValue = "false")
    private boolean skipDeploy;

    /**
     */
    @Component
    private WagonManager wagonManager; // maven-compat

    /**
     * The current user system settings for use in Maven.
     */
    @Parameter(defaultValue = "${settings}", readonly = true)
    private Settings settings;

    /**
     * @since 3.0-beta-2
     */
    @Parameter(defaultValue = "${session}", readonly = true)
    protected MavenSession mavenSession;

    private String topDistributionManagementSiteUrl;

    private Site deploySite;

    private PlexusContainer container;

    /**
     * {@inheritDoc}
     */
    public void execute() throws MojoExecutionException {
        if (skip && isDeploy()) {
            getLog().info("maven.site.skip = true: Skipping site deployment");
            return;
        }

        if (skipDeploy && isDeploy()) {
            getLog().info("maven.site.deploy.skip = true: Skipping site deployment");
            return;
        }

        deployTo(new Repository(getDeploySite().getId(), getDeploySite().getUrl()));
    }

    /**
     * Make sure the given URL ends with a slash.
     *
     * @param url a String
     * @return if url already ends with '/' it is returned unchanged.
     *         Otherwise a '/' character is appended.
     */
    protected static String appendSlash(final String url) {
        if (url.endsWith("/")) {
            return url;
        } else {
            return url + "/";
        }
    }

    /**
     * Detect if the mojo is staging or deploying.
     *
     * @return <code>true</code> if the mojo is for deploy and not staging (local or deploy)
     */
    protected abstract boolean isDeploy();

    /**
     * Get the top distribution management site url, used for module relative path calculations.
     * This should be a top-level URL, ie above modules and locale sub-directories. Each deploy mojo
     * can tweak algorithm to determine this top site by implementing determineTopDistributionManagementSiteUrl().
     *
     * @return the site for deployment
     * @throws MojoExecutionException in case of issue
     * @see #determineTopDistributionManagementSiteUrl()
     */
    protected String getTopDistributionManagementSiteUrl() throws MojoExecutionException {
        if (topDistributionManagementSiteUrl == null) {
            topDistributionManagementSiteUrl = determineTopDistributionManagementSiteUrl();

            if (!isDeploy()) {
                getLog().debug("distributionManagement.site.url relative path: " + getDeployModuleDirectory());
            }
        }
        return topDistributionManagementSiteUrl;
    }

    protected abstract String determineTopDistributionManagementSiteUrl() throws MojoExecutionException;

    /**
     * Get the site used for deployment, with its id to look up credential settings and the target URL for the deploy.
     * This should be a top-level URL, ie above modules and locale sub-directories. Each deploy mojo
     * can tweak algorithm to determine this deploy site by implementing determineDeploySite().
     *
     * @return the site for deployment
     * @throws MojoExecutionException in case of issue
     * @see #determineDeploySite()
     */
    protected Site getDeploySite() throws MojoExecutionException {
        if (deploySite == null) {
            deploySite = determineDeploySite();
        }
        return deploySite;
    }

    protected abstract Site determineDeploySite() throws MojoExecutionException;

    /**
     * Find the relative path between the distribution URLs of the top site and the current project.
     *
     * @return the relative path or "./" if the two URLs are the same.
     * @throws MojoExecutionException in case of issue
     */
    protected String getDeployModuleDirectory() throws MojoExecutionException {
        String to = getSite(project).getUrl();

        getLog().debug("Mapping url source calculation: ");
        String from = getTopDistributionManagementSiteUrl();

        String relative = siteTool.getRelativePath(to, from);

        // SiteTool.getRelativePath() uses File.separatorChar,
        // so we need to convert '\' to '/' in order for the URL to be valid for Windows users
        relative = relative.replace('\\', '/');

        return ("".equals(relative)) ? "./" : relative;
    }

    /**
     * Use wagon to deploy the generated site to a given repository.
     *
     * @param repository the repository to deploy to.
     *                   This needs to contain a valid, non-null {@link Repository#getId() id}
     *                   to look up credentials for the deploy, and a valid, non-null
     *                   {@link Repository#getUrl() scm url} to deploy to.
     * @throws MojoExecutionException if the deploy fails.
     */
    private void deployTo(final Repository repository) throws MojoExecutionException {
        if (!inputDirectory.exists()) {
            throw new MojoExecutionException("The site does not exist, please run site:site first");
        }

        if (getLog().isDebugEnabled()) {
            getLog().debug("Deploying to '" + repository.getUrl() + "',\n    Using credentials from server id '"
                    + repository.getId() + "'");
        }

        deploy(inputDirectory, repository);
    }

    private void deploy(final File directory, final Repository repository) throws MojoExecutionException {
        // TODO: work on moving this into the deployer like the other deploy methods
        final Wagon wagon = getWagon(repository, wagonManager);

        try {
            SettingsDecrypter settingsDecrypter = container.lookup(SettingsDecrypter.class);

            ProxyInfo proxyInfo = getProxy(repository, settingsDecrypter);

            push(directory, repository, wagon, proxyInfo, getLocales(), getDeployModuleDirectory());

            if (chmod) {
                chmod(wagon, repository, chmodOptions, chmodMode);
            }
        } catch (ComponentLookupException cle) {
            throw new MojoExecutionException("Unable to lookup SettingsDecrypter: " + cle.getMessage(), cle);
        } finally {
            try {
                wagon.disconnect();
            } catch (ConnectionException e) {
                getLog().error("Error disconnecting wagon - ignored", e);
            }
        }
    }

    private Wagon getWagon(final Repository repository, final WagonManager manager) throws MojoExecutionException {
        final Wagon wagon;

        try {
            wagon = manager.getWagon(repository);
        } catch (UnsupportedProtocolException e) {
            String shortMessage = "Unsupported protocol: '" + repository.getProtocol() + "' for site deployment to "
                    + "distributionManagement.site.url=" + repository.getUrl() + ".";
            String longMessage =
                    "\n" + shortMessage + "\n" + "Currently supported protocols are: " + getSupportedProtocols() + ".\n"
                            + "    Protocols may be added through wagon providers.\n" + "    For more information, see "
                            + "https://maven.apache.org/plugins/maven-site-plugin/examples/adding-deploy-protocol.html";

            getLog().error(longMessage);

            throw new MojoExecutionException(shortMessage);
        } catch (TransferFailedException e) {
            throw new MojoExecutionException("Unable to configure Wagon: '" + repository.getProtocol() + "'", e);
        }

        if (!wagon.supportsDirectoryCopy()) {
            throw new MojoExecutionException(
                    "Wagon protocol '" + repository.getProtocol() + "' doesn't support directory copying");
        }

        return wagon;
    }

    private String getSupportedProtocols() {
        try {
            Set<String> protocols = container.lookupMap(Wagon.class).keySet();

            return StringUtils.join(protocols.iterator(), ", ");
        } catch (ComponentLookupException e) {
            // in the unexpected case there is a problem when instantiating a wagon provider
            getLog().error(e);
        }
        return "";
    }

    private void push(
            final File inputDirectory,
            final Repository repository,
            final Wagon wagon,
            final ProxyInfo proxyInfo,
            final List<Locale> localesList,
            final String relativeDir)
            throws MojoExecutionException {
        AuthenticationInfo authenticationInfo = wagonManager.getAuthenticationInfo(repository.getId());
        getLog().debug("authenticationInfo with id '" + repository.getId() + "': "
                + ((authenticationInfo == null) ? "-" : authenticationInfo.getUserName()));

        try {
            if (getLog().isDebugEnabled()) {
                Debug debug = new Debug();

                wagon.addSessionListener(debug);

                wagon.addTransferListener(debug);
            }

            if (proxyInfo != null) {
                getLog().debug("connect with proxyInfo");
                wagon.connect(repository, authenticationInfo, proxyInfo);
            } else if (proxyInfo == null && authenticationInfo != null) {
                getLog().debug("connect with authenticationInfo and without proxyInfo");
                wagon.connect(repository, authenticationInfo);
            } else {
                getLog().debug("connect without authenticationInfo and without proxyInfo");
                wagon.connect(repository);
            }

            getLog().info("Pushing " + inputDirectory);

            for (Locale locale : localesList) {
                if (!locale.equals(SiteTool.DEFAULT_LOCALE)) {
                    getLog().info("   >>> to " + appendSlash(repository.getUrl()) + locale + "/" + relativeDir);

                    wagon.putDirectory(new File(inputDirectory, locale.toString()), locale + "/" + relativeDir);
                } else {
                    // TODO: this also uploads the non-default locales,
                    // is there a way to exclude directories in wagon?
                    getLog().info("   >>> to " + appendSlash(repository.getUrl()) + relativeDir);

                    wagon.putDirectory(inputDirectory, relativeDir);
                }
            }
        } catch (ResourceDoesNotExistException
                | TransferFailedException
                | AuthorizationException
                | ConnectionException
                | AuthenticationException e) {
            throw new MojoExecutionException("Error uploading site", e);
        }
    }

    private static void chmod(
            final Wagon wagon, final Repository repository, final String chmodOptions, final String chmodMode)
            throws MojoExecutionException {
        try {
            if (wagon instanceof CommandExecutor) {
                CommandExecutor exec = (CommandExecutor) wagon;
                exec.executeCommand("chmod " + chmodOptions + " " + chmodMode + " " + repository.getBasedir());
            }
            // else ? silently ignore, FileWagon is not a CommandExecutor!
        } catch (CommandExecutionException e) {
            throw new MojoExecutionException("Error uploading site", e);
        }
    }

    /**
     * Get proxy information.
     * <p>
     * Get the <code>ProxyInfo</code> of the proxy associated with the <code>host</code>
     * and the <code>protocol</code> of the given <code>repository</code>.
     * </p>
     * <p>
     * Extract from <a href="https://docs.oracle.com/javase/1.5.0/docs/guide/net/properties.html">
     * J2SE Doc : Networking Properties - nonProxyHosts</a> : "The value can be a list of hosts,
     * each separated by a |, and in addition a wildcard character (*) can be used for matching"
     * </p>
     * <p>
     * Defensively support comma (",") and semi colon (";") in addition to pipe ("|") as separator.
     * </p>
     *
     * @param repository   the Repository to extract the ProxyInfo from
     * @param wagonManager the WagonManager used to connect to the Repository
     * @return a ProxyInfo object instantiated or <code>null</code> if no matching proxy is found
     */
    public static ProxyInfo getProxyInfo(Repository repository, WagonManager wagonManager) {
        ProxyInfo proxyInfo = wagonManager.getProxy(repository.getProtocol());

        if (proxyInfo == null) {
            return null;
        }

        String host = repository.getHost();
        String nonProxyHostsAsString = proxyInfo.getNonProxyHosts();
        for (String nonProxyHost : StringUtils.split(nonProxyHostsAsString, ",;|")) {
            if (StringUtils.contains(nonProxyHost, "*")) {
                // Handle wildcard at the end, beginning or middle of the nonProxyHost
                final int pos = nonProxyHost.indexOf('*');
                String nonProxyHostPrefix = nonProxyHost.substring(0, pos);
                String nonProxyHostSuffix = nonProxyHost.substring(pos + 1);
                // prefix*
                if (StringUtils.isNotEmpty(nonProxyHostPrefix)
                        && host.startsWith(nonProxyHostPrefix)
                        && StringUtils.isEmpty(nonProxyHostSuffix)) {
                    return null;
                }
                // *suffix
                if (StringUtils.isEmpty(nonProxyHostPrefix)
                        && StringUtils.isNotEmpty(nonProxyHostSuffix)
                        && host.endsWith(nonProxyHostSuffix)) {
                    return null;
                }
                // prefix*suffix
                if (StringUtils.isNotEmpty(nonProxyHostPrefix)
                        && host.startsWith(nonProxyHostPrefix)
                        && StringUtils.isNotEmpty(nonProxyHostSuffix)
                        && host.endsWith(nonProxyHostSuffix)) {
                    return null;
                }
            } else if (host.equals(nonProxyHost)) {
                return null;
            }
        }
        return proxyInfo;
    }

    /**
     * Get proxy information.
     *
     * @param repository        the Repository to extract the ProxyInfo from
     * @param settingsDecrypter settings password decrypter
     * @return a ProxyInfo object instantiated or <code>null</code> if no matching proxy is found.
     */
    private ProxyInfo getProxy(Repository repository, SettingsDecrypter settingsDecrypter) {
        String protocol = repository.getProtocol();
        String url = repository.getUrl();

        getLog().debug("repository protocol " + protocol);

        String originalProtocol = protocol;
        // olamy: hackish here protocol (wagon hint in fact !) is dav
        // but the real protocol (transport layer) is http(s)
        // and it's the one use in wagon to find the proxy arghhh
        // so we will check both
        if (StringUtils.equalsIgnoreCase("dav", protocol) && url.startsWith("dav:")) {
            url = url.substring(4);
            if (url.startsWith("http")) {
                try {
                    URL urlSite = new URL(url);
                    protocol = urlSite.getProtocol();
                    getLog().debug("found dav protocol so transform to real transport protocol " + protocol);
                } catch (MalformedURLException e) {
                    getLog().warn("fail to build URL with " + url);
                }
            }
        } else {
            getLog().debug("getProxy 'protocol': " + protocol);
        }

        if (mavenSession != null && protocol != null) {
            MavenExecutionRequest request = mavenSession.getRequest();

            if (request != null) {
                List<Proxy> proxies = request.getProxies();

                if (proxies != null) {
                    for (Proxy proxy : proxies) {
                        if (proxy.isActive()
                                && (protocol.equalsIgnoreCase(proxy.getProtocol())
                                        || originalProtocol.equalsIgnoreCase(proxy.getProtocol()))) {
                            SettingsDecryptionResult result =
                                    settingsDecrypter.decrypt(new DefaultSettingsDecryptionRequest(proxy));
                            proxy = result.getProxy();

                            ProxyInfo proxyInfo = new ProxyInfo();
                            proxyInfo.setHost(proxy.getHost());
                            // so hackish for wagon the protocol is https for site dav:
                            // dav:https://dav.codehaus.org/mojo/
                            proxyInfo.setType(protocol); // proxy.getProtocol() );
                            proxyInfo.setPort(proxy.getPort());
                            proxyInfo.setNonProxyHosts(proxy.getNonProxyHosts());
                            proxyInfo.setUserName(proxy.getUsername());
                            proxyInfo.setPassword(proxy.getPassword());

                            getLog().debug("found proxyInfo "
                                    + ("host:port " + proxyInfo.getHost() + ":" + proxyInfo.getPort() + ", "
                                            + proxyInfo.getUserName()));

                            return proxyInfo;
                        }
                    }
                }
            }
        }
        getLog().debug("getProxy 'protocol': " + protocol + " no ProxyInfo found");
        return null;
    }

    /**
     * {@inheritDoc}
     */
    public void contextualize(Context context) throws ContextException {
        container = (PlexusContainer) context.get(PlexusConstants.PLEXUS_KEY);
    }

    /**
     * Extract the distributionManagement site from the given MavenProject.
     *
     * @param project the MavenProject. Not null.
     * @return the project site. Not null.
     *         Also site.getUrl() and site.getId() are guaranteed to be not null.
     * @throws MojoExecutionException if any of the site info is missing.
     */
    protected static Site getSite(final MavenProject project) throws MojoExecutionException {
        final DistributionManagement distributionManagement = project.getDistributionManagement();

        if (distributionManagement == null) {
            throw new MojoExecutionException("Missing distribution management in project " + getFullName(project));
        }

        final Site site = distributionManagement.getSite();

        if (site == null) {
            throw new MojoExecutionException(
                    "Missing site information in the distribution management of the project " + getFullName(project));
        }

        if (site.getUrl() == null || site.getId() == null) {
            throw new MojoExecutionException(
                    "Missing site data: specify url and id for project " + getFullName(project));
        }

        return site;
    }

    private static String getFullName(MavenProject project) {
        return project.getName() + " (" + project.getGroupId() + ':' + project.getArtifactId() + ':'
                + project.getVersion() + ')';
    }

    /**
     * Extract the distributionManagement site of the top level parent of the given MavenProject.
     * This climbs up the project hierarchy and returns the site of the last project
     * for which {@link #getSite(org.apache.maven.project.MavenProject)} returns a site that resides in the
     * same site. Notice that it doesn't take into account if the parent is in the reactor or not.
     *
     * @param project the MavenProject. Not <code>null</code>.
     * @return the top level site. Not <code>null</code>.
     *         Also site.getUrl() and site.getId() are guaranteed to be not <code>null</code>.
     * @throws MojoExecutionException if no site info is found in the tree.
     * @see URIPathDescriptor#sameSite(java.net.URI)
     */
    protected MavenProject getTopLevelProject(MavenProject project) throws MojoExecutionException {
        Site site = getSite(project);

        MavenProject parent = project;

        while (parent.getParent() != null) {
            MavenProject oldProject = parent;
            // MSITE-585, MNG-1943
            parent = parent.getParent();

            Site oldSite = site;

            try {
                site = getSite(parent);
            } catch (MojoExecutionException e) {
                return oldProject;
            }

            // MSITE-600
            URIPathDescriptor siteURI = new URIPathDescriptor(URIEncoder.encodeURI(site.getUrl()), "");
            URIPathDescriptor oldSiteURI = new URIPathDescriptor(URIEncoder.encodeURI(oldSite.getUrl()), "");

            if (!siteURI.sameSite(oldSiteURI.getBaseURI())) {
                return oldProject;
            }
        }

        return parent;
    }

    private static class URIEncoder {
        private static final String MARK = "-_.!~*'()";
        private static final String RESERVED = ";/?:@&=+$,";

        private static String encodeURI(final String uriString) {
            final char[] chars = uriString.toCharArray();
            final StringBuilder uri = new StringBuilder(chars.length);

            // MSITE-750: wagon dav: pseudo-protocol
            if (uriString.startsWith("dav:http")) {
                // transform dav:http to dav-http
                chars[3] = '-';
            }

            for (char c : chars) {
                if ((c >= '0' && c <= '9')
                        || (c >= 'a' && c <= 'z')
                        || (c >= 'A' && c <= 'Z')
                        || MARK.indexOf(c) != -1
                        || RESERVED.indexOf(c) != -1) {
                    uri.append(c);
                } else {
                    uri.append('%');
                    uri.append(Integer.toHexString((int) c));
                }
            }
            return uri.toString();
        }
    }
}
