package io.jenkins.plugins.analysis.warnings.axivion;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.auth.UsernamePasswordCredentials;

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.google.gson.JsonObject;

import edu.hm.hafner.analysis.ParsingCanceledException;
import edu.hm.hafner.analysis.ParsingException;
import edu.hm.hafner.analysis.Report;
import edu.hm.hafner.util.VisibleForTesting;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.jenkinsci.Symbol;
import hudson.Extension;
import hudson.FilePath;
import hudson.model.Item;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.security.ACL;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import hudson.util.Secret;
import jenkins.model.Jenkins;

import io.jenkins.plugins.analysis.core.model.Tool;
import io.jenkins.plugins.analysis.core.util.LogHandler;
import io.jenkins.plugins.util.EnvironmentResolver;
import io.jenkins.plugins.util.JenkinsFacade;

/** Provides a parser and customized messages for the Axivion Suite. */
@SuppressWarnings({"PMD.ExcessiveImports", "PMD.DataClass"})
public final class AxivionSuite extends Tool {
    private static final long serialVersionUID = 967222727302169818L;
    private static final String ID = "axivion-suite";

    private String projectUrl = StringUtils.EMPTY;
    private String credentialsId = StringUtils.EMPTY;
    private String basedir = "$";

    @VisibleForTesting
    AxivionSuite(final String projectUrl, final String credentialsId, final String basedir) {
        super();
        setBasedir(basedir);
        setCredentialsId(credentialsId);
        setProjectUrl(projectUrl);
    }

    /** Creates a new instance of {@link AxivionSuite}. */
    @DataBoundConstructor
    public AxivionSuite() {
        super();
        // empty constructor required for stapler
    }

    public String getBasedir() {
        return basedir;
    }

    @DataBoundSetter
    public void setBasedir(final String basedir) {
        this.basedir = basedir;
    }

    public String getProjectUrl() {
        return projectUrl;
    }

    @DataBoundSetter
    public void setProjectUrl(final String projectUrl) {
        this.projectUrl = projectUrl;
    }

    public String getCredentialsId() {
        return credentialsId;
    }

    @DataBoundSetter
    public void setCredentialsId(final String credentialsId) {
        this.credentialsId = credentialsId;
    }

    @Override
    public Report scan(final Run<?, ?> run, final FilePath workspace, final Charset sourceCodeEncoding,
            final LogHandler logger) throws ParsingException, ParsingCanceledException {

        AxivionDashboard dashboard = new RemoteAxivionDashboard(projectUrl, withValidCredentials());
        AxivionParser parser = new AxivionParser(projectUrl, expandBaseDir(run, basedir));

        Report report = new Report();
        report.logInfo("Axivion webservice: " + projectUrl);
        report.logInfo("Local basedir: " + basedir);

        for (AxIssueKind kind : AxIssueKind.values()) {
            final JsonObject payload = dashboard.getIssues(kind);
            parser.parse(report, kind, payload);
        }

        return report;
    }

    private UsernamePasswordCredentials withValidCredentials() {
        final List<StandardUsernamePasswordCredentials> all =
                CredentialsProvider.lookupCredentials(
                        StandardUsernamePasswordCredentials.class,
                        (Item) null,
                        ACL.SYSTEM,
                        Collections.emptyList());

        StandardUsernamePasswordCredentials jenkinsCredentials =
                CredentialsMatchers.firstOrNull(all,
                        CredentialsMatchers.withId(credentialsId));

        if (jenkinsCredentials == null) {
            throw new ParsingException("Could not find the credentials for " + credentialsId);
        }

        return new UsernamePasswordCredentials(
                jenkinsCredentials.getUsername(),
                Secret.toString(jenkinsCredentials.getPassword()));
    }

    private static String expandBaseDir(final Run<?, ?> run, final String baseDir) {
        String expandedBasedir;
        try {
            EnvironmentResolver environmentResolver = new EnvironmentResolver();

            expandedBasedir =
                    environmentResolver.expandEnvironmentVariables(
                            run.getEnvironment(TaskListener.NULL), baseDir);
        }
        catch (IOException | InterruptedException ignore) {
            expandedBasedir = baseDir;
        }
        return expandedBasedir;
    }

    /** Descriptor for {@link AxivionSuite}. * */
    @Symbol({"axivionSuite", "axivion"})
    @Extension
    public static class AxivionSuiteToolDescriptor extends ToolDescriptor {

        /** Creates the descriptor instance. */
        public AxivionSuiteToolDescriptor() {
            super(ID);
        }

        @NonNull
        @Override
        public String getDisplayName() {
            return "Axivion Suite";
        }

        @Override
        public String getHelp() {
            return "For using Axivion Suite, set up your analysis project and the web service. Provide the URL and credentials.";
        }

        /**
         * Dashboard project url must be a valid url.
         *
         * @param projectUrl
         *         url to a project inside an Axivion dashboard
         *
         * @return {@link FormValidation#ok()} is a valid url
         */
        public FormValidation doCheckProjectUrl(@QueryParameter final String projectUrl) {
            try {
                new URL(projectUrl).toURI();
            }
            catch (URISyntaxException | MalformedURLException ex) {
                return FormValidation.error("This is not a valid URL.");
            }
            return FormValidation.ok();
        }

        /**
         * Checks whether the given path is a correct os path.
         *
         * @param basedir
         *         path to check
         *
         * @return {@link FormValidation#ok()} is a valid url
         */
        @SuppressFBWarnings("PATH_TRAVERSAL_IN")
        public FormValidation doCheckBasedir(@QueryParameter final String basedir) {
            try {
                if (!basedir.contains("$")) {
                    // path with a variable cannot be checked at this point
                    Paths.get(basedir);
                }
            }
            catch (InvalidPathException e) {
                return FormValidation.error("You have to provide a valid path.");
            }
            return FormValidation.ok();
        }

        /**
         * Checks whether valid credentials are given.
         *
         * @param item
         *         jenkins configuration
         * @param credentialsId
         *         id of the stored credentials pair
         *
         * @return {@link FormValidation#ok()} if credentials exist and are valid
         */
        public FormValidation doCheckCredentialsId(
                @AncestorInPath final Item item, @QueryParameter final String credentialsId) {
            if (StringUtils.isBlank(credentialsId)) {
                return FormValidation.error("You have to provide credentials.");
            }
            if (item == null) {
                if (!new JenkinsFacade().hasPermission(Jenkins.ADMINISTER)) {
                    return FormValidation.ok();
                }
            }
            else {
                if (!item.hasPermission(Item.EXTENDED_READ)
                        && !item.hasPermission(CredentialsProvider.USE_ITEM)) {
                    return FormValidation.ok();
                }
            }

            if (CredentialsMatchers.firstOrNull(
                    CredentialsProvider.lookupCredentials(
                            StandardUsernamePasswordCredentials.class,
                            item,
                            ACL.SYSTEM,
                            Collections.emptyList()),
                    CredentialsMatchers.withId(credentialsId))
                    == null) {
                return FormValidation.error("Cannot find currently selected credentials.");
            }
            return FormValidation.ok();
        }

        /**
         * Shows the user all available credential id items.
         *
         * @param item
         *         jenkins configuration
         * @param credentialsId
         *         current used credentials
         *
         * @return a list view of all credential ids
         */
        public ListBoxModel doFillCredentialsIdItems(
                @AncestorInPath final Item item, @QueryParameter final String credentialsId) {
            StandardListBoxModel result = new StandardListBoxModel();
            if (item == null) {
                if (!new JenkinsFacade().hasPermission(Jenkins.ADMINISTER)) {
                    return result.includeCurrentValue(credentialsId);
                }
            }
            else {
                if (!item.hasPermission(Item.EXTENDED_READ)
                        && !item.hasPermission(CredentialsProvider.USE_ITEM)) {
                    return result.includeCurrentValue(credentialsId);
                }
            }
            return result.includeAs(
                    ACL.SYSTEM,
                    item,
                    StandardUsernamePasswordCredentials.class,
                    Collections.emptyList())
                    .includeCurrentValue(credentialsId);
        }
    }
}