#!/usr/bin/env python3
"""Link your Launchpad user to GitHub, proposing branches to LP and GitHub"""

from argparse import ArgumentParser
from subprocess import Popen, PIPE
import os
import sys

try:
    from launchpadlib.launchpad import Launchpad
except ImportError:
    print(
        "Missing python launchpadlib dependency to create branches for you."
        "Install with: sudo apt-get install python3-launchpadlib"
    )
    sys.exit(1)

if "avoid-pep8-E402-import-not-top-of-file":
    _tdir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
    sys.path.insert(0, _tdir)
    from cloudinit import util


DRYRUN = False
LP_TO_GIT_USER_FILE = ".lp-to-git-user"
MIGRATE_BRANCH_NAME = "migrate-lp-to-github"
GITHUB_PULL_URL = "https://github.com/canonical/cloud-init/compare/main...{github_user}:{branch}"
GH_UPSTREAM_URL = "https://github.com/canonical/cloud-init"


def error(message):
    if isinstance(message, bytes):
        message = message.decode("utf-8")
    log("ERROR: {error}".format(error=message))
    sys.exit(1)


def log(message):
    print(message)


def subp(cmd, skip=False):
    prefix = "SKIPPED: " if skip else "$ "
    log("{prefix}{command}".format(prefix=prefix, command=" ".join(cmd)))
    if skip:
        return
    proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
    out, err = proc.communicate()
    if proc.returncode:
        error(err if err else out)
    return out.decode("utf-8")


LP_GIT_PATH_TMPL = "git+ssh://{launchpad_user}@git.launchpad.net/"
LP_UPSTREAM_PATH_TMPL = LP_GIT_PATH_TMPL + "cloud-init"
LP_REMOTE_PATH_TMPL = LP_GIT_PATH_TMPL + "~{launchpad_user}/cloud-init"
GITHUB_REMOTE_PATH_TMPL = "git@github.com:{github_user}/cloud-init.git"


# Comment templates
COMMIT_MSG_TMPL = """\
lp-to-git-users: adding {gh_username}

Mapped from {lp_username}
"""
PUBLISH_DIR = "/tmp/cloud-init-lp-to-github-migration"


def get_parser():
    parser = ArgumentParser(description=__doc__)
    parser.add_argument(
        "--dryrun",
        required=False,
        default=False,
        action="store_true",
        help=(
            "Run commands and review operation in dryrun mode, "
            "making not changes."
        ),
    )
    parser.add_argument("launchpad_user", help="Your launchpad username.")
    parser.add_argument("github_user", help="Your github username.")
    parser.add_argument(
        "--local-repo-dir",
        required=False,
        dest="repo_dir",
        help=(
            "The name of the local directory into which we clone."
            " Default: {}".format(PUBLISH_DIR)
        ),
    )
    parser.add_argument(
        "--upstream-branch",
        required=False,
        dest="upstream",
        default="origin/main",
        help=(
            "The name of remote branch target into which we will merge."
            " Default: origin/main"
        ),
    )
    parser.add_argument(
        "-v",
        "--verbose",
        required=False,
        default=False,
        action="store_true",
        help=("Print all actions."),
    )
    return parser


def create_publish_branch(upstream, publish_branch):
    """Create clean publish branch target in the current git repo."""
    branches = subp(["git", "branch"])
    upstream_remote, upstream_branch = upstream.split("/", 1)
    subp(["git", "checkout", upstream_branch])
    subp(["git", "pull"])
    if publish_branch in branches:
        subp(["git", "branch", "-D", publish_branch])
    subp(["git", "checkout", upstream, "-b", publish_branch])


def add_lp_and_github_remotes(lp_user, gh_user):
    """Add lp and github remotes if not present.

    @return Tuple with (lp_remote_name, gh_remote_name)
    """
    lp_remote = LP_REMOTE_PATH_TMPL.format(launchpad_user=lp_user)
    gh_remote = GITHUB_REMOTE_PATH_TMPL.format(github_user=gh_user)
    remotes = subp(["git", "remote", "-v"])
    lp_remote_name = gh_remote_name = None
    for remote in remotes.splitlines():
        if not remote:
            continue
        remote_name, remote_url, _operation = remote.split()
        if lp_remote == remote_url:
            lp_remote_name = remote_name
        elif gh_remote == remote_url:
            gh_remote_name = remote_name
    if not lp_remote_name:
        log(
            "launchpad: Creating git remote launchpad-{} to point at your"
            " LP repo".format(lp_user)
        )
        lp_remote_name = "launchpad-{}".format(lp_user)
        subp(["git", "remote", "add", lp_remote_name, lp_remote])
    try:
        subp(["git", "fetch", lp_remote_name])
    except:
        log("launchpad: Pushing to ensure LP repo exists")
        subp(["git", "push", lp_remote_name, "main:main"])
        subp(["git", "fetch", lp_remote_name])
    if not gh_remote_name:
        log(
            "github: Creating git remote github-{} to point at your"
            " GH repo".format(gh_user)
        )
        gh_remote_name = "github-{}".format(gh_user)
        subp(["git", "remote", "add", gh_remote_name, gh_remote])
    try:
        subp(["git", "fetch", gh_remote_name])
    except:
        log(
            "ERROR: [github] Could not fetch remote '{remote}'."
            "Please create a fork for your github user by clicking 'Fork'"
            " from {gh_upstream}".format(
                remote=gh_remote, gh_upstream=GH_UPSTREAM_URL
            )
        )
        sys.exit(1)
    return (lp_remote_name, gh_remote_name)


def create_migration_branch(
    branch_name, upstream, lp_user, gh_user, commit_msg
):
    """Create an LP to GitHub migration branch and add lp_user->gh_user."""
    log(
        "Creating a migration branch: {} adding your users".format(
            MIGRATE_BRANCH_NAME
        )
    )
    create_publish_branch(upstream, MIGRATE_BRANCH_NAME)
    lp_to_git_map = {}
    lp_to_git_file = os.path.join(os.getcwd(), "tools", LP_TO_GIT_USER_FILE)
    if os.path.exists(lp_to_git_file):
        with open(lp_to_git_file) as stream:
            lp_to_git_map = util.load_json(stream.read())
    if gh_user in lp_to_git_map.values():
        raise RuntimeError(
            "github user '{}' already in {}".format(gh_user, lp_to_git_file)
        )
    if lp_user in lp_to_git_map:
        raise RuntimeError(
            "launchpad user '{}' already in {}".format(lp_user, lp_to_git_file)
        )
    lp_to_git_map[lp_user] = gh_user
    with open(lp_to_git_file, "w") as stream:
        stream.write(util.json_dumps(lp_to_git_map))
    subp(["git", "add", lp_to_git_file])
    commit_file = os.path.join(os.path.dirname(os.getcwd()), "commit.msg")
    with open(commit_file, "wb") as stream:
        stream.write(commit_msg.encode("utf-8"))
    subp(["git", "commit", "--all", "-F", commit_file])


def main():
    global DRYRUN
    global VERBOSITY
    parser = get_parser()
    args = parser.parse_args()
    DRYRUN = args.dryrun
    VERBOSITY = 1 if args.verbose else 0
    repo_dir = args.repo_dir or PUBLISH_DIR
    if not os.path.exists(repo_dir):
        cleanup_repo_dir = True
        subp(
            [
                "git",
                "clone",
                LP_UPSTREAM_PATH_TMPL.format(
                    launchpad_user=args.launchpad_user
                ),
                repo_dir,
            ]
        )
    else:
        cleanup_repo_dir = False
    cwd = os.getcwd()
    os.chdir(repo_dir)
    log("Syncing main branch with upstream")
    subp(["git", "checkout", "main"])
    subp(["git", "pull"])
    try:
        lp_remote_name, gh_remote_name = add_lp_and_github_remotes(
            args.launchpad_user, args.github_user
        )
        commit_msg = COMMIT_MSG_TMPL.format(
            gh_username=args.github_user, lp_username=args.launchpad_user
        )
        create_migration_branch(
            MIGRATE_BRANCH_NAME,
            args.upstream,
            args.launchpad_user,
            args.github_user,
            commit_msg,
        )

        for push_remote in (lp_remote_name, gh_remote_name):
            subp(["git", "push", push_remote, MIGRATE_BRANCH_NAME, "--force"])
    except Exception as e:
        error("Failed setting up migration branches: {0}".format(e))
    finally:
        os.chdir(cwd)
        if cleanup_repo_dir and os.path.exists(repo_dir):
            util.del_dir(repo_dir)
    # Make merge request on LP
    log("[launchpad] Automatically creating merge proposal using launchpadlib")
    lp = Launchpad.login_with(
        "server-team github-migration tool", "production", version="devel"
    )
    main = lp.git_repositories.getByPath(path="cloud-init").getRefByPath(
        path="main"
    )
    LP_BRANCH_PATH = "~{launchpad_user}/cloud-init/+git/cloud-init"
    lp_git_repo = lp.git_repositories.getByPath(
        path=LP_BRANCH_PATH.format(launchpad_user=args.launchpad_user)
    )
    lp_user_migrate_branch = lp_git_repo.getRefByPath(
        path="refs/heads/migrate-lp-to-github"
    )
    lp_merge_url = (
        "https://code.launchpad.net/"
        + LP_BRANCH_PATH.format(launchpad_user=args.launchpad_user)
        + "/+ref/"
        + MIGRATE_BRANCH_NAME
    )
    try:
        lp_user_migrate_branch.createMergeProposal(
            commit_message=commit_msg, merge_target=main, needs_review=True
        )
    except Exception:
        log(
            "[launchpad] active merge proposal already exists at:\n"
            "{url}\n".format(url=lp_merge_url)
        )
    else:
        log(
            "[launchpad] Merge proposal created at:\n{url}.\n".format(
                url=lp_merge_url
            )
        )
    log(
        "To link your account to github open your browser and"
        " click 'Create pull request' at the following URL:\n"
        "{url}".format(
            url=GITHUB_PULL_URL.format(
                github_user=args.github_user, branch=MIGRATE_BRANCH_NAME
            )
        )
    )
    if os.path.exists(repo_dir):
        util.del_dir(repo_dir)
    return 0


if __name__ == "__main__":
    sys.exit(main())
