package com.github.kostyasha.github.integration.branch.trigger;

import com.github.kostyasha.github.integration.branch.GitHubBranchBadgeAction;
import com.github.kostyasha.github.integration.branch.GitHubBranchCause;
import com.github.kostyasha.github.integration.branch.GitHubBranchTrigger;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import hudson.model.Action;
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Job;
import hudson.model.ParameterValue;
import hudson.model.ParametersAction;
import hudson.model.Queue;
import hudson.model.queue.QueueTaskFuture;
import jenkins.model.ParameterizedJobMixIn;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.kohsuke.github.GHCommitState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static com.cloudbees.jenkins.GitHubWebHook.getJenkinsInstance;
import static com.google.common.base.Predicates.instanceOf;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.jenkinsci.plugins.github.pullrequest.utils.JobHelper.getDefaultParametersValues;
import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from;
import static org.jenkinsci.plugins.github.util.JobInfoHelpers.asParameterizedJobMixIn;

/**
 * @author Kanstantsin Shautsou
 */
public class JobRunnerForBranchCause implements Predicate<GitHubBranchCause> {

    private static final Logger LOGGER = LoggerFactory.getLogger(JobRunnerForBranchCause.class);

    private Job job;
    private GitHubBranchTrigger trigger;

    public JobRunnerForBranchCause(Job job, GitHubBranchTrigger trigger) {
        this.job = job;
        this.trigger = trigger;
    }

    @Override
    public boolean apply(GitHubBranchCause cause) {
        try {
            cause.setPollingLogFile(trigger.getPollingLogAction().getPollingLogFile());

            StringBuilder sb = new StringBuilder();
            sb.append("Jenkins queued the run (").append(cause.getReason()).append(")");

            if (trigger.isCancelQueued() && cancelQueuedBuildByBranchName(cause.getBranchName())) {
                sb.append(". Queued builds aborted");
            }

            QueueTaskFuture<?> queueTaskFuture = startJob(cause);
            if (isNull(queueTaskFuture)) {
                LOGGER.error("{} job didn't start", job.getFullName());
            }

            LOGGER.info(sb.toString());

            // remote connection
            if (trigger.isPreStatus()) {
                trigger.getRemoteRepository()
                        .createCommitStatus(cause.getCommitSha(),
                                GHCommitState.PENDING,
                                null,
                                sb.toString(),
                                job.getFullName());
            }
        } catch (IOException e) {
            LOGGER.error("Can't trigger build ({})", e.getMessage(), e);
            return false;
        }
        return true;
    }

    /**
     * Cancel previous builds for specified PR id.
     */
    private static boolean cancelQueuedBuildByBranchName(final String branch) {
        Queue queue = getJenkinsInstance().getQueue();

        for (Queue.Item item : queue.getItems()) {
            Optional<Cause> cause = from(item.getAllActions())
                    .filter(instanceOf(CauseAction.class))
                    .transformAndConcat(new CausesFromAction())
                    .filter(instanceOf(GitHubBranchCause.class))
                    .firstMatch(new CauseHasBranch(branch));

            if (cause.isPresent()) {
                queue.cancel(item);
                return true;
            }
        }

        return false;
    }

    public QueueTaskFuture<?> startJob(GitHubBranchCause cause) {
        return startJob(cause, null);
    }

    public QueueTaskFuture<?> startJob(GitHubBranchCause cause, Cause additionalCause) {
        ParametersAction parametersAction;
        List<ParameterValue> parameters = getDefaultParametersValues(job);
        cause.fillParameters(parameters);

        try {
            Constructor<ParametersAction> constructor = ParametersAction.class.getConstructor(List.class, Collection.class);
            Set<String> names = new HashSet<>();
            for (ParameterValue param : parameters) {
                names.add(param.getName());
            }
            parametersAction = constructor.newInstance(parameters, names);
        } catch (NoSuchMethodException | IllegalAccessException | InstantiationException
                | InvocationTargetException ex) {
            parametersAction = new ParametersAction(parameters);
        }

        GitHubBranchBadgeAction gitHubBadgeAction = new GitHubBranchBadgeAction(cause);

        ParameterizedJobMixIn parameterizedJobMixIn = asParameterizedJobMixIn(job);

        // remove after switch to newer core
        int quietPeriod = 0;
        try {
            Object mixinJob = FieldUtils.readField(parameterizedJobMixIn, "val$job", true);
            ParameterizedJobMixIn.ParameterizedJob parameterizedJob = (ParameterizedJobMixIn.ParameterizedJob) mixinJob;
            quietPeriod = parameterizedJob.getQuietPeriod();
        } catch (IllegalAccessException e) {
            LOGGER.error("Couldn't extract quiet period, falling back to {}", quietPeriod, e);
        }

        CauseAction causeAction;
        if (nonNull(additionalCause)) {
            causeAction = new CauseAction(cause, additionalCause);
        } else {
            causeAction = new CauseAction(cause);
        }

        return asParameterizedJobMixIn(job).scheduleBuild2(quietPeriod,
                causeAction,
                parametersAction,
                gitHubBadgeAction
        );
    }

    private static class CausesFromAction implements Function<Action, Iterable<Cause>> {
        @Override
        public Iterable<Cause> apply(Action input) {
            return ((CauseAction) input).getCauses();
        }
    }

    private static class CauseHasBranch implements Predicate<Cause> {
        private final String branch;

        CauseHasBranch(String branch) {
            this.branch = branch;
        }

        @Override
        public boolean apply(Cause cause) {
            return ((GitHubBranchCause) cause).getBranchName().equals(branch);
        }
    }
}