package com.acunetix;

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractProject;
import hudson.model.Item;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.security.ACL;
import hudson.tasks.BuildStepDescriptor;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import jenkins.model.Jenkins;
import jenkins.tasks.SimpleBuildStep;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.verb.POST;

import javax.annotation.Nonnull;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;

import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials;


public class BuildScanner extends hudson.tasks.Builder implements SimpleBuildStep {
    private final String profile;
    private final String target;
    private String targetName;
    private final String repTemp;
    private String reportTemplateName;
    private final String threat;
    private final Boolean stopScan;
    private Boolean incScan;
    private String incScanId;
    public final Boolean svRep;


    @DataBoundConstructor
    public BuildScanner(String profile, String target, String repTemp, String threat, Boolean stopScan, Boolean svRep, Boolean incScan, String incScanId) {
        this.profile = profile;
        this.target = target;
        this.repTemp = repTemp;
        this.threat = threat;
        this.stopScan = stopScan;
        this.svRep = svRep;
        this.incScan = incScan;
        try {
            Engine aac = new Engine(getDescriptor().getgApiUrl(), getDescriptor().getgApiKey());
            if (aac.getVersion() > 12) {
                if (incScan) {
                    if (aac.checkScanExist(incScanId)) {
                        if ((!aac.getScanProfile(incScanId).equals(profile)) || (!aac.getScanTarget(incScanId).equals(target))) {
                            this.incScanId = aac.createIncScan(profile, target);
                            aac.deleteScan(incScanId);
                        }
                        else {
                            this.incScanId = incScanId;
                        }
                    }
                    else {
                        this.incScanId = aac.createIncScan(profile, target);
                    }
                }
                else{
                    this.incScanId = incScanId;
                }
            }
            else {
                this.incScan = false;
            }
            this.targetName = aac.getTargetName(this.target);
            this.reportTemplateName = aac.getReportTemplateName(this.repTemp);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public String getProfile() {
        return profile;
    }


    public String getTarget() {
        return target;
    }

    public String getRepTemp() {
        return repTemp;
    }

    public String getThreat() {
        return threat;
    }

    public Boolean getStopScan() {
        return stopScan;
    }

    private String getTargetName() {
        return targetName;
    }

    private String getReportTemplateName() {
        return reportTemplateName;
    }

    public Boolean getIncScan() {
        return incScan;
    }

    public String getIncScanId() {
        return incScanId;
    }


    @Override
    public void perform(@Nonnull Run<?, ?> build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws hudson.AbortException, InterruptedException {
        final String PROCESSING = "processing";
        final String COMPLETED = "completed";
        final String ABORTED = "aborted";
        final String SCHEDULED = "scheduled";
        final String QUEUED = "queued";
        final String NOREPORT = "no_report";
        final PrintStream listenerLogger = listener.getLogger();

        Engine engine = new Engine(getDescriptor().getgApiUrl(), getDescriptor().getgApiKey());
        String scanId = null;
        Boolean scanAbortedExternally = false;
        Boolean scanAbortedByUser = false;
        String scanStatus = "";
        String scanThreat;
        Boolean started = false;
        Boolean bThreat = false;
        Boolean bScheduled = false;

        try {
            Engine testengine = new Engine(getDescriptor().getgApiUrl(), getDescriptor().getgApiKey());
            int rc = testengine.doTestConnection(getDescriptor().getgApiUrl() + "/me");
            if (rc != 200) {
                listenerLogger.println(SR.getString("aborting.the.build"));
                throw new hudson.AbortException(SR.getString("cannot.connect.to.application"));
                }
            if ((engine.getTargetName(target) == null)) {
                listenerLogger.println(SR.getString("aborting.the.build"));
                throw new hudson.AbortException(SR.getString("invalid.target"));
                }
            else if (!engine.checkScanProfileExists(profile)) {
                listenerLogger.println(SR.getString("aborting.the.build"));
                throw new hudson.AbortException(SR.getString("invalid.scan_type"));
                }
            listenerLogger.println(SR.getString("starting.scan.on.target.0", getTargetName()));
            if (this.incScan) {
                if (!engine.checkScanExist(incScanId)) {
                    throw new hudson.AbortException(SR.getString("could.not.find.scan.with.scanid.0.create.new", this.incScanId));
                }
                engine.triggerIncScan(this.incScanId, false);
                scanId = this.incScanId;
            }
            else {
                scanId = engine.startScan(profile, target, false);
                if (scanId == null) {
                    listenerLogger.println(SR.getString("aborting.the.build"));
                    throw new hudson.AbortException(SR.getString("cannot.connect.to.application"));
                }
            }
            while (!scanStatus.equals(COMPLETED)) {
                if (scanStatus.equals(PROCESSING) && !started) {
                    started = true;
                    listenerLogger.println(SR.getString("scan.started"));
                    listenerLogger.println(SR.getString("view.scan.status") + getDescriptor().getgApiUrl().replace("api/v1", "#") + "/scans/" + scanId + "/info");
                }
                if (scanStatus.equals(SCHEDULED) && !bScheduled) {
                    bScheduled = true;
                    listenerLogger.println(SR.getString("the.scan.is.in.scheduled.state"));
                }
                if (scanStatus.equals(ABORTED)) {
                    scanAbortedExternally = true;
                    listenerLogger.println(SR.getString("aborting.the.build"));
                    throw new hudson.AbortException(SR.getString("scan.aborted.outside"));
                }

                scanThreat = engine.getScanThreat(scanId);
                if (engine.checkThreat(threat, scanThreat)) {
                    bThreat = true;
                    //listenerLogger.println(SR.getString("scan.threat.0.1", Engine.getThreatName(scanThreat), this.getThreat()));
                    listenerLogger.println(SR.getString("scan.threat.0", Engine.getThreatName(scanThreat)));
                    listenerLogger.println(SR.getString("check.vulnerabilities.found") + getDescriptor().getgApiUrl().replace("api/v1", "#") + "/scans/" + scanId + "/vulnerabilities");
                    listenerLogger.println(SR.getString("aborting.the.build"));
                    throw new hudson.AbortException(SR.getString("scan.threat"));
                }
                Thread.sleep(10000);
                scanStatus = engine.getScanStatus(scanId);
            }
            listenerLogger.println(SR.getString("scan.completed"));
        } catch (InterruptedException e) {
            scanAbortedByUser = true;
            listenerLogger.println(SR.getString("aborting.the.build"));
            throw new hudson.AbortException(SR.getString("build.aborted"));
        } catch (hudson.AbortException e) {
            throw e;
        } catch (SSLHandshakeException e) {
            e.printStackTrace();
            throw new hudson.AbortException(SR.getString("certificate.to.the.java.ca.store"));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            if (!engine.checkScanExist(scanId)) {
                listenerLogger.println(SR.getString("aborting.the.build"));
                scanAbortedExternally = true;
                throw new hudson.AbortException(SR.getString("could.not.find.scan.with.scanid.0", scanId));
            }
        } catch (java.net.ConnectException e) {
            e.printStackTrace();
            listenerLogger.println(SR.getString("aborting.the.build"));
            scanAbortedExternally = true;
            throw new hudson.AbortException(SR.getString("could.not.connect.to.application.connection.refused"));
            } catch (ConnectionException e) {
            listenerLogger.println(SR.getString("aborting.the.build"));
            throw new hudson.AbortException(SR.getString("cannot.connect.to.application"));
        } catch (Exception e) {
            e.printStackTrace();
            listenerLogger.println(e.getMessage());
        } finally {
            try {
                if (stopScan && scanId != null && !scanAbortedExternally && (bThreat || scanAbortedByUser) && !bScheduled) {
                    engine.stopScan(scanId);
                    try {
                        String status = "";
                        while (!status.equals(ABORTED) && !status.equals(COMPLETED)) {
                            Thread.sleep(10000);
                            status = engine.getScanStatus(scanId);
                        }
                        listenerLogger.println(SR.getString("the.scan.was.stopped"));
                    } catch (InterruptedException | IOException e) {
                        e.printStackTrace();
                        listenerLogger.println(e.getMessage());
                    }
                }
                if (!repTemp.equals(NOREPORT) && scanId != null && !scanAbortedByUser && !scanAbortedExternally) {
                    listenerLogger.println(SR.getString("generating.0.report", getReportTemplateName()));
                    Thread.sleep(10000);
                    String downloadLink = engine.generateReport(scanId, repTemp, "scans");
                    listenerLogger.print("Scan report download link: " + engine.getUrl(getDescriptor().getgApiUrl(), downloadLink) + "\n");
                    String dfName = engine.doDownload(engine.getUrl(getDescriptor().getgApiUrl(), downloadLink), workspace.getRemote());
                    if (svRep) {
                        listenerLogger.print(SR.getString("report.saved.in.workspace") + dfName + "\n");
                    }
                }
            } catch (InterruptedException | IOException e) {
                e.printStackTrace();
                listenerLogger.println(e.getMessage());
            }
        }
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<hudson.tasks.Builder> {
        private String gApiUrl;
        private String gApiKeyID;

        public DescriptorImpl() {
            load();
        }
        @POST
        public FormValidation doTestConnection(@QueryParameter("gApiUrl") final String ApiUrl,
                                               @QueryParameter("gApiKey") final String apiKey) {
            Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
            try {
                if (ApiUrl.length() == 0)
                    return FormValidation.error(SR.getString("please.set.the.api.url"));
                Engine apio = new Engine(ApiUrl, getgApiKey());
                int respCode = apio.doTestConnection(ApiUrl + "/me");
                if (respCode == 200) {
                    return FormValidation.ok(SR.getString("connected.successfully"));
                }
            } catch (SSLHandshakeException e) {
                e.printStackTrace();
                return FormValidation.error(SR.getString("certificate.to.the.java.ca.store"));
            } catch (IOException e) {
                e.printStackTrace();
                return FormValidation.error(e.getMessage());
            }
            return FormValidation.error(SR.getString("cannot.connect.to.application"));
        }

        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            return true;
        }

        /**
         * This human readable name is used in the configuration screen.
         */
        public String getDisplayName() {
            return "Acunetix";
        }

        @Override
        public boolean configure(StaplerRequest req, JSONObject formData) throws FormException {
            gApiUrl = formData.getString("gApiUrl");
            gApiKeyID = formData.getString("gApiKeyID");
            save();
            return super.configure(req, formData);
        }

        public String getgApiUrl() {
            return gApiUrl;
        }

        private String getgApiKeyID() {return gApiKeyID;}

        private String getgApiKey() {
            StandardCredentials credentials = null;
            try {
                credentials = CredentialsMatchers.firstOrNull(
                        lookupCredentials(StandardCredentials.class, (Item) null, ACL.SYSTEM, new ArrayList<DomainRequirement>()),
                        CredentialsMatchers.withId(gApiKeyID));
            }
            catch (NullPointerException e) {
                throw new ConnectionException(SR.getString("api.key.not.set"));
            }
            if (credentials != null) {
                if (credentials instanceof StringCredentials) {
                    return ((StringCredentials) credentials).getSecret().getPlainText();
                }
            }
            throw new IllegalStateException("Could not find Acunetix API Key ID: " + gApiKeyID);
        }



        public ListBoxModel doFillProfileItems() throws IOException {
            ListBoxModel items = new ListBoxModel();
            Engine apio = new Engine(gApiUrl, getgApiKey());
            JSONArray jsa = apio.getScanningProfiles();
            for (int i = 0; i < jsa.size(); i++) {
                JSONObject item = jsa.getJSONObject(i);
                String profile_name = item.getString("name");
                String profile_id = item.getString("profile_id");
                items.add(profile_name, profile_id);
            }
            return items;
        }

        public ListBoxModel doFillTargetItems() throws IOException {
            ListBoxModel items = new ListBoxModel();
            Engine apio = new Engine(gApiUrl, getgApiKey());
            JSONArray jsa = apio.getTargets();
            for (int i = 0; i < jsa.size(); i++) {
                JSONObject item = jsa.getJSONObject(i);
                String mi = item.getString("manual_intervention");
                if (mi.equals("null") || mi.equals("false")) {
                    String address = item.getString("address");
                    String target_id = item.getString("target_id");
                    String description = item.getString("description");
                    String target_name = address;
                    if (description.length() > 0) {
                        if (description.length() > 100) {
                            description = description.substring(0, 100);
                        }
                        target_name += "  (" + description + ")";
                    }
                    items.add(target_name, target_id);
                }
            }
            return items;
        }

        public ListBoxModel doFillRepTempItems() throws IOException {
            ListBoxModel items = new ListBoxModel();
            Engine apio = new Engine(gApiUrl, getgApiKey());
            JSONArray jsa = apio.getReportTemplates();
            items.add("Do not generate a report", "no_report");
            for (int i = 0; i < jsa.size(); i++) {
                JSONObject item = jsa.getJSONObject(i);
                String group = item.getString("group");
                String reportTemplate_name = item.getString("name");
                String template_id = item.getString("template_id");
                if (!reportTemplate_name.equals("Scan Comparison")) {
                    items.add(reportTemplate_name, template_id);
                    }
            }
            return items;
        }

        public ListBoxModel doFillThreatItems() throws IOException {
            ListBoxModel items = new ListBoxModel();
            items.add("Do not fail the build", "DoNotFail");
            items.add("High", "High");
            items.add("Medium or High", "Medium");
            items.add("Low, Medium or High", "Low");
            return items;
        }

        public ListBoxModel doFillGApiKeyIDItems(
                @AncestorInPath Item item) {
            StandardListBoxModel result = new StandardListBoxModel();
            if (item == null) {
                if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
                    return result.includeCurrentValue(gApiKeyID);
                }
            } else {
                if (!item.hasPermission(Item.EXTENDED_READ)
                        && !item.hasPermission(CredentialsProvider.USE_ITEM)) {
                    return result.includeCurrentValue(gApiKeyID);
                }
            }
            if (gApiKeyID != null) {
                result.includeMatchingAs(ACL.SYSTEM, Jenkins.getInstance(), StringCredentials.class,
                        Collections.<DomainRequirement> emptyList(), CredentialsMatchers.allOf(CredentialsMatchers.withId(gApiKeyID)));
            }
            return result
                    .includeMatchingAs(ACL.SYSTEM, Jenkins.getInstance(), StringCredentials.class,
                            Collections.<DomainRequirement> emptyList(), CredentialsMatchers.allOf(CredentialsMatchers.instanceOf(StringCredentials.class)));
        }

        public Boolean testVersion() {
            Boolean res = false;
            try {
                Engine aac = new Engine(getgApiUrl(), getgApiKey());
                res = aac.getVersion() > 12;
            } catch (IOException e) {
                e.printStackTrace();
            }
            return res;
        }
    }
}