/*
 * The MIT License
 *
 * Copyright 2016-2017 CloudBees, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.jenkinsci.plugins.github_branch_source;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMHeadMigration;
import jenkins.scm.api.SCMHeadOrigin;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy;
import jenkins.scm.api.mixin.ChangeRequestSCMHead2;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.github.GHPullRequest;
import org.kohsuke.github.GHRepository;

/**
 * Head corresponding to a pull request.
 * Named like {@code PR-123} or {@code PR-123-merge} or {@code PR-123-head}.
 */
public class PullRequestSCMHead extends SCMHead implements ChangeRequestSCMHead2 {

    private static final Logger LOGGER = Logger.getLogger(PullRequestSCMHead.class.getName());
    private static final AtomicBoolean UPGRADE_SKIPPED_2_0_X = new AtomicBoolean(false);

    private static final long serialVersionUID = 1;

    private Boolean merge;
    private final int number;
    private final BranchSCMHead target;
    private final String sourceOwner;
    private final String sourceRepo;
    private final String sourceBranch;
    private final SCMHeadOrigin origin;
    /**
     * Only populated if de-serializing instances.
     */
    private transient Metadata metadata;

    PullRequestSCMHead(PullRequestSCMHead copy) {
        super(copy.getName());
        this.merge = copy.merge;
        this.number = copy.number;
        this.target = copy.target;
        this.sourceOwner = copy.sourceOwner;
        this.sourceRepo = copy.sourceRepo;
        this.sourceBranch = copy.sourceBranch;
        this.origin = copy.origin;
        this.metadata = copy.metadata;
    }

    PullRequestSCMHead(GHPullRequest pr, String name, boolean merge) {
        super(name);
        // the merge flag is encoded into the name, so safe to store here
        this.merge = merge;
        this.number = pr.getNumber();
        this.target = new BranchSCMHead(pr.getBase().getRef());
        // the source stuff is immutable for a pull request on github, so safe to store here
        GHRepository repository = pr.getHead().getRepository(); // may be null for deleted forks JENKINS-41246
        this.sourceOwner = repository == null ? null : repository.getOwnerName();
        this.sourceRepo = repository == null ? null : repository.getName();
        this.sourceBranch = pr.getHead().getRef();

        if (pr.getRepository().getOwnerName().equalsIgnoreCase(sourceOwner)) {
            this.origin = SCMHeadOrigin.DEFAULT;
        } else {
            // if the forked repo name differs from the upstream repo name
            this.origin = pr.getBase().getRepository().getName().equalsIgnoreCase(sourceRepo)
                    ? new SCMHeadOrigin.Fork(this.sourceOwner)
                    : new SCMHeadOrigin.Fork(repository == null ? this.sourceOwner : repository.getFullName());
        }
    }

    public PullRequestSCMHead(@NonNull String name, String sourceOwner, String sourceRepo, String sourceBranch, int number,
                       BranchSCMHead target, SCMHeadOrigin origin, ChangeRequestCheckoutStrategy strategy) {
        super(name);
        this.merge = ChangeRequestCheckoutStrategy.MERGE == strategy;
        this.number = number;
        this.target = target;
        this.sourceOwner = sourceOwner;
        this.sourceRepo = sourceRepo;
        this.sourceBranch = sourceBranch;
        this.origin = origin;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPronoun() {
        return Messages.PullRequestSCMHead_Pronoun();
    }

    public int getNumber() {
        return number;
    }

    /**
     * Default for old settings.
     *
     * @return the deserialized object.
     */
    @SuppressFBWarnings("SE_PRIVATE_READ_RESOLVE_NOT_INHERITED") // because JENKINS-41453
    private Object readResolve() {
        if (merge == null) {
            merge = true;
        }
        if (metadata != null) {
            // Upgrade from 1.x:
            if (UPGRADE_SKIPPED_2_0_X.compareAndSet(false, true)) {
                LOGGER.log(Level.WARNING, "GitHub Branch Source plugin was directly upgraded from 1.x to 2.2.0 "
                        + "or newer without completing a full fetch from all repositories. Consequently startup may be "
                        + "delayed while GitHub is queried for the missing information");
            }
            // we need the help of FixMetadataMigration
            return new FixMetadata(
                    getName(),
                    merge,
                    metadata.getNumber(),
                    new BranchSCMHead(metadata.getBaseRef())
            );
        }
        if (origin == null && !(this instanceof FixOrigin)) {
            // Upgrade from 2.0.x

            // we need the help of FixOriginMigration
            return new FixOrigin(this);
        }
        return this;
    }

    /**
     * Whether we intend to build the merge of the PR head with the base branch.
     *
     * @return {@code true} if this is a merge PR head.
     */
    public boolean isMerge() {
        return merge;
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public ChangeRequestCheckoutStrategy getCheckoutStrategy() {
        return merge ? ChangeRequestCheckoutStrategy.MERGE : ChangeRequestCheckoutStrategy.HEAD;
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public String getId() {
        return Integer.toString(number);
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public BranchSCMHead getTarget() {
        return target;
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public String getOriginName() {
        return sourceBranch;
    }

    public String getSourceOwner() {
        return sourceOwner;
    }

    public String getSourceBranch() {
        return sourceBranch;
    }

    public String getSourceRepo() {
        return sourceRepo;
    }

    @NonNull
    @Override
    public SCMHeadOrigin getOrigin() {
        return origin == null ? SCMHeadOrigin.DEFAULT : origin;
    }

    /**
     * Holds legacy data so we can recover the details.
     */
    private static class Metadata {
        private final int number;
        private final String url;
        private final String userLogin;
        private final String baseRef;

        public Metadata(int number, String url, String userLogin, String baseRef) {
            this.number = number;
            this.url = url;
            this.userLogin = userLogin;
            this.baseRef = baseRef;
        }

        public int getNumber() {
            return number;
        }

        public String getUrl() {
            return url;
        }

        public String getUserLogin() {
            return userLogin;
        }

        public String getBaseRef() {
            return baseRef;
        }
    }

    /**
     * Used to handle data migration.
     *
     * @see FixOriginMigration
     * @deprecated used for data migration.
     */
    @Deprecated
    @Restricted(NoExternalUse.class)
    public static class FixOrigin extends PullRequestSCMHead {

        FixOrigin(PullRequestSCMHead pullRequestSCMHead) {
            super(pullRequestSCMHead);
        }
    }

    /**
     * Used to handle data migration.
     *
     * @see FixOriginMigration
     * @deprecated used for data migration.
     */
    @Restricted(NoExternalUse.class)
    @Extension
    public static class FixOriginMigration extends
            SCMHeadMigration<GitHubSCMSource, FixOrigin, PullRequestSCMRevision> {
        public FixOriginMigration() {
            super(GitHubSCMSource.class, FixOrigin.class, PullRequestSCMRevision.class);
        }

        @Override
        public PullRequestSCMHead migrate(@NonNull GitHubSCMSource source, @NonNull FixOrigin head) {
            return new PullRequestSCMHead(head.getName(), head.getSourceOwner(), head.getSourceRepo(),
                    head.getSourceBranch(), head.getNumber(), head.getTarget(), source.getRepoOwner().equalsIgnoreCase(head.getSourceOwner())
                                        ? SCMHeadOrigin.DEFAULT
                                        : new SCMHeadOrigin.Fork(head.getSourceOwner()), head.getCheckoutStrategy());
        }

        @Override
        public SCMRevision migrate(@NonNull GitHubSCMSource source,
                                   @NonNull PullRequestSCMRevision revision) {
            PullRequestSCMHead head = migrate(source, (FixOrigin) revision.getHead());
            return head != null ? new PullRequestSCMRevision(
                    head,
                    revision.getBaseHash(),
                    revision.getPullHash()
            ) : null;
        }
    }

    /**
     * Used to handle data migration.
     *
     * @see FixMetadataMigration
     * @deprecated used for data migration.
     */
    @Deprecated
    @Restricted(NoExternalUse.class)
    public static class FixMetadata extends PullRequestSCMHead {
        FixMetadata(String name, Boolean merge, int number, BranchSCMHead branchSCMHead) {
            super(name, null, null, null, number, branchSCMHead, null, merge ? ChangeRequestCheckoutStrategy.MERGE : ChangeRequestCheckoutStrategy.HEAD);
        }

    }

    /**
     * Used to handle data migration.
     *
     * @see FixOriginMigration
     * @deprecated used for data migration.
     */
    @Restricted(NoExternalUse.class)
    @Extension
    public static class FixMetadataMigration extends
            SCMHeadMigration<GitHubSCMSource, FixMetadata, PullRequestSCMRevision> {
        public FixMetadataMigration() {
            super(GitHubSCMSource.class, FixMetadata.class, PullRequestSCMRevision.class);
        }

        @Override
        public PullRequestSCMHead migrate(@NonNull GitHubSCMSource source, @NonNull FixMetadata head) {
            PullRequestSource src = source.retrievePullRequestSource(head.getNumber());
            return new PullRequestSCMHead(head.getName(), src == null ? null : src.getSourceOwner(),
                    src == null ? null : src.getSourceRepo(), src == null ? null : src.getSourceBranch(),
                    head.getNumber(), head.getTarget(), src != null && source.getRepoOwner().equalsIgnoreCase(src.getSourceOwner())
                                        ? SCMHeadOrigin.DEFAULT
                                        : new SCMHeadOrigin.Fork(head.getSourceOwner()), head.getCheckoutStrategy());
        }

        @Override
        public SCMRevision migrate(@NonNull GitHubSCMSource source,
                                   @NonNull PullRequestSCMRevision revision) {
            PullRequestSCMHead head = migrate(source, (FixMetadata) revision.getHead());
            return head != null ? new PullRequestSCMRevision(
                    head,
                    revision.getBaseHash(),
                    revision.getPullHash()
            ) : null;
        }
    }

}