#!/usr/bin/env python3
# pylint: disable=invalid-name
"""Manage my Git infrastructure."""

import argparse
import os
import os.path
import sys

import github3.exceptions  # pylint: disable=import-error
import gitlab.exceptions  # pylint: disable=import-error
import passhole.passhole  # pylint: disable=import-error
import sh  # pylint: disable=import-error
from sh import tf as terraform  # pylint: disable=import-error

sys.path.append(os.path.expanduser("~/Documents/bin"))

import rcfiles.git  # noqa: E402 pylint: disable=wrong-import-position
import rcfiles.github  # noqa: E402 pylint: disable=wrong-import-position
import rcfiles.gitlab  # noqa: E402 pylint: disable=wrong-import-position

GH_MIRROR_PREFIX = "https://*****@github.com/"


def error(message):
    """Print an error message with the current subparser's usage and exit."""
    # pylint: disable=protected-access,possibly-used-before-assignment
    sub_parser = arg_parser._subparsers._actions[1].choices[_args.command]
    sub_parser.error(message)


def github_mirrors(project, gh_conn):
    """Return a list of GitHub repositories that are mirrored from the GitLab
    project."""
    repos = []
    for mirror in project.remote_mirrors.list(all=True):
        if mirror.url.startswith(GH_MIRROR_PREFIX):
            name, owner = (
                mirror.url.removeprefix(GH_MIRROR_PREFIX)
                .removesuffix("/")
                .removesuffix(".git")
                .split("/")
            )
            try:
                repos.append(gh_conn.repository(name, owner))
            except github3.exceptions.NotFoundError:
                pass
    return repos


def mirror_project(project, gh_conn, token):
    """Mirror a GitLab project to GitHub."""
    gh_me = rcfiles.github.me(gh_conn)
    mirror_url = f"https://{token}@github.com/{gh_me}/{project.name}.git"

    try:
        gh_repo = gh_conn.repository(gh_me, project.name)
        print(
            f"Using existing GitHub repository {gh_repo.html_url}.",
            file=sys.stderr,
        )
    except github3.exceptions.NotFoundError:
        gh_repo = gh_conn.create_repository(project.name)
        print(
            f"Created a new GitHub repository {gh_repo.html_url}.",
            file=sys.stderr,
        )

    gh_repo.edit(
        project.name,
        homepage=project.web_url,
        description=f"Mirror of {project.web_url}",
        has_issues=False,
        has_wiki=False,
        has_projects=False,
    )

    for mirror in project.remote_mirrors.list(all=True):
        if mirror.url.startswith(f"{GH_MIRROR_PREFIX}/{gh_me}/{project.name}"):
            mirror.url = mirror_url
            break
    else:
        mirror = project.remote_mirrors.create({"url": mirror_url})

    mirror.enabled = True
    mirror.only_protected_branches = False
    mirror.keep_divergent_refs = False
    mirror.save()

    return gh_repo


def get_mirror_token():
    """Get the GitHub token for mirroring a GitLab repository.

    Reads it from a Keepass password database using Passhole.
    """
    ENTRY_PATH = "Web Sites/GitHub".split("/")  # noqa
    TOKEN_FIELD = "GitLab mirroring token"  # nosec # noqa

    # The following line requires an interactive session for getting the
    # password.
    db = passhole.passhole.open_database()
    entry = db.find_entries(path=ENTRY_PATH, first=True)
    return entry.get_custom_property(TOKEN_FIELD)


def guess_remote(remote_type="gitlab", gl_conn=None):
    # pylint: disable=too-many-return-statements
    """Try to guess the right remote of the Git repo we're in.

    return None if failed, or the repo dictionary from get_all_remotes().
    """
    if remote_type not in ["gitlab", "github"]:
        return None

    if remote_type == "gitlab" and gl_conn is None:
        return None

    remotes = rcfiles.git.get_all_remotes()
    if remotes is None:
        return None

    if "origin" in remotes:
        remote = remotes["origin"]
        if remote_type == "gitlab" and rcfiles.gitlab.is_gitlab_url(
            gl_conn, remote["url"]
        ):
            return remote
        if remote_type == "github" and rcfiles.github.is_github_url(
            remote["url"]
        ):
            return remote

    for remote in remotes.values():
        if remote_type == "gitlab" and rcfiles.gitlab.is_gitlab_url(
            gl_conn, remote["url"]
        ):
            return remote
        if remote_type == "github" and rcfiles.github.is_github_url(
            remote["url"]
        ):
            return remote

    return None


def guess_name(args, gh_conn=None, gl_conn=None):
    """Try to guess the name from the arguments and the repository remotes."""
    if "name" not in args or args.name is None:
        if not rcfiles.git.in_repo():
            error("Name not provided and not in a Git repo.")

        remote = guess_remote(
            remote_type=(
                "github" if "github" in args and args.github else "gitlab"
            ),
            gl_conn=gl_conn,
        )
        if remote is None:
            if "github" in args and args.github:
                error("Name not provided and could not find a GitHub remote.")
            else:
                error("Name not provided and could not find a GitLab remote.")
        if "github" in args and args.github:
            name = rcfiles.github.url_to_name(remote["url"])
        else:
            name = rcfiles.gitlab.url_to_name(gl_conn, remote["url"])
        print(
            f"""Name not provided, using {name} from the {remote["name"]} remote.""",  # noqa: E501
            file=sys.stderr,
        )
    else:
        if "github" in args and args.github:
            if "/" in args.name:
                name = args.name
            else:
                print(
                    "Name does not include project, defaulting to the GitHub user.",  # noqa: E501
                    file=sys.stderr,
                )
                name = f"{rcfiles.github.me(gh_conn)}/{args.name}"
        else:
            if "/" in args.name:
                name = args.name
            else:
                print(
                    "Name does not include project, defaulting to the GitLab user.",  # noqa: E501
                    file=sys.stderr,
                )
                name = f"{rcfiles.gitlab.me(gl_conn)}/{args.name}"
    return name


def create_github_repo(args):
    """Create a new GitHub repository.

    Does the following:
    - Creates the mirror.
    - Clones the repository.
    - Commits an initial empty commit.
    """
    if "/" in args.name:
        error("Can't specify an organization.")
    if args.internal or args.private:
        error("Can't create internal or private GitHub repositories.")
    try:
        conn = rcfiles.github.connect()
    except Exception as e:  # pylint: disable=broad-except
        error(f"Failed to connect to GitHub: {e}")

    repo = conn.create_repository(
        args.name,
        description=args.description,
    )
    print(
        f"Created a new GitHub repository {repo.html_url}.",
        file=sys.stderr,
    )

    rcfiles.git.git.clone(repo.ssh_url)
    print("Cloned repository.", file=sys.stderr)

    rcfiles.git.empty_commit(repo.name)
    print(
        "Committed an initial empty commit.",
        file=sys.stderr,
    )


def create_gitlab_repo(args):
    """Create a new GitLab repository.

    Does the following:
    - Creates the repository.
    - Creates the mirror.
    - Commits an initial empty commit.
    - Clones the  repository.
    - Adds the mirror remote.
    """
    if args.private and args.internal:
        error("Repository can be internal or private, not both.")
    try:
        conn = rcfiles.gitlab.connect()
    except Exception as e:  # pylint: disable=broad-except
        error(f"Failed to connect to GitLab: {e}")

    if args.private:
        visibility = "private"
    elif args.internal:
        visibility = "internal"
    else:
        visibility = "public"

    name_with_namespace = guess_name(args, gl_conn=conn, gh_conn=None)
    namespace, name = name_with_namespace.split("/")
    if namespace == rcfiles.gitlab.me(conn):
        project = conn.projects.create(
            {
                "name": name,
                "description": args.description,
                "visibility": visibility,
            }
        )
    else:
        group = conn.groups.get(namespace)
        project = conn.projects.create(
            {
                "name": name,
                "description": args.description,
                "visibility": visibility,
                "namespace_id": group.id,
            }
        )
    print(
        f"Created a new {visibility} GitLab repository {project.web_url}.",
        file=sys.stderr,
    )

    if args.mirror:
        gh_repo = mirror_repo(args)

    rcfiles.gitlab.empty_commit(project)
    print(
        "Committed an initial empty commit.",
        file=sys.stderr,
    )

    rcfiles.git.git.clone(project.ssh_url_to_repo)
    print("Cloned repository.", file=sys.stderr)

    if args.mirror:
        rcfiles.git.add_remote(project.name, "github", gh_repo.ssh_url)
        print("Added a remote for the mirror repository.", file=sys.stderr)


def create_repo(args):
    """Create a new repository."""
    if args.mirror and args.github:
        error("Can't mirror from GitHub to GitLab.")
    if args.github:
        create_github_repo(args)
    else:
        create_gitlab_repo(args)


def mirror_repo(args):
    """Mirror a GitLab repository to GitHub.

    Does the following:
    - Creates the mirror.
    - Adds the mirror remote.
    """
    try:
        gh_conn = rcfiles.github.connect()
    except Exception as e:  # pylint: disable=broad-except
        error(f"Failed to connect to GitHub: {e}")
    try:
        gl_conn = rcfiles.gitlab.connect()
    except Exception as e:  # pylint: disable=broad-except
        error(f"Failed to connect to GitLab: {e}")

    name = guess_name(args, gh_conn, gl_conn)

    try:
        project = gl_conn.projects.get(name)
    except gitlab.exceptions.GitlabGetError:
        error(f"Could not find GitLab project {name}.")

    gh_repo = mirror_project(project, gh_conn, get_mirror_token())
    print(
        f"Setup mirror for {project.web_url} to {gh_repo.html_url}.",
        file=sys.stderr,
    )

    if args.name is None and rcfiles.git.in_repo():
        rcfiles.git.add_remote(".", "github", gh_repo.ssh_url)
        print("Added a remote for the mirror repository.", file=sys.stderr)

    return gh_repo


def archive_repo(args):
    """Archive a repository.

    Does the following:
    - Archives the repository (sets it to read-only).
    - Archives all GitHub mirrors.
    """
    try:
        gh_conn = rcfiles.github.connect()
    except Exception as e:  # pylint: disable=broad-except
        error(f"Failed to connect to GitHub: {e}")
    if not args.github:
        try:
            gl_conn = rcfiles.gitlab.connect()
        except Exception as e:  # pylint: disable=broad-except
            error(f"Failed to connect to GitLab: {e}")

    if args.github:
        owner, name = guess_name(args, gh_conn=gh_conn, gl_conn=None).split(
            "/"
        )
    else:
        name = guess_name(args, gh_conn=None, gl_conn=gl_conn)

    if args.github:
        repo = gh_conn.repository(owner, name)
        repo.edit(name, archived=True)
        print(f"Archived repository {repo.html_url}.", file=sys.stderr)
    else:
        project = gl_conn.projects.get(name)
        project.archive()
        print(f"Archived repository {project.web_url}.", file=sys.stderr)
        for mirror in github_mirrors(project, gh_conn):
            mirror.edit(mirror.name, archived=True)
            print(
                f"Archived GitHub mirror {mirror.html_url}.", file=sys.stderr
            )


def fork_repo(args):
    """Fork a GitHub repository.

    Does the following:
    - Forks the GitHub repository.
    - Clones the repository.
    - Adds the upstream remote.
    """
    try:
        conn = rcfiles.github.connect()
    except Exception as e:  # pylint: disable=broad-except
        error(f"Failed to connect to GitHub: {e}")

    if "/" not in args.name:
        error("Must provide a full repository name.")

    org, name = args.name.split("/")
    upstream = conn.repository(org, name)
    fork = upstream.create_fork()
    print(
        f"Forked GitHub repo {upstream.full_name} to {fork.full_name}.",
        file=sys.stderr,
    )

    with sh.pushd(os.path.expanduser("~/Repositories/GitHub")):
        rcfiles.git.git.clone(fork.ssh_url)
        print("Cloned repository.", file=sys.stderr)

        rcfiles.git.add_remote(upstream.name, "upstream", upstream.ssh_url)
        print("Added an upstream remote.", file=sys.stderr)


def terraform_init(args):
    """Initialize a GitLab-managed Terraform state."""
    if not rcfiles.git.in_repo():
        error("This command must be run from inside a Git repository.")
    try:
        conn = rcfiles.gitlab.connect()
    except Exception as e:  # pylint: disable=broad-except
        error(f"Failed to connect to GitLab: {e}")
    remotes = rcfiles.git.get_all_remotes()
    for remote in remotes.values():
        if rcfiles.gitlab.is_gitlab_url(conn, remote["url"]):
            break
    else:
        error(
            "This command must be run from inside a Git repository with a GitLab remote."  # noqa: E501
        )
    username = rcfiles.gitlab.me(conn)
    url = rcfiles.gitlab.http_url(conn)
    name = guess_name(args, gl_conn=conn)
    project = conn.projects.get(name)
    address = (
        f"{url}/api/v4/projects/{project.id}/terraform/state/{args.state}"
    )
    # fmt: off
    terraform.init(
        "-backend-config", f"address={address}",
        "-backend-config", f"lock_address={address}/lock",
        "-backend-config", f"unlock_address={address}/lock",
        "-backend-config", f"username={username}",
        "-backend-config", f"password={conn.private_token}",
        "-backend-config", "lock_method=POST",
        "-backend-config", "unlock_method=DELETE",
        "-reconfigure",
    )
    # fmt: on


def build_arg_parser():
    """Builds the argument parser."""
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(
        title="Commands", required=True, dest="command"
    )

    parser_create = subparsers.add_parser(
        "new", help=create_repo.__doc__.splitlines()[0]
    )
    parser_create.set_defaults(func=create_repo)
    parser_create.add_argument("name", help="Name of the repository.")
    parser_create.add_argument(
        "-m", "--mirror", help="Setup a mirror in GitHub.", action="store_true"
    )
    parser_create.add_argument(
        "--github",
        help="Create the repository in GitHub.",
        action="store_true",
    )
    parser_create.add_argument(
        "--internal",
        help="Create an internal GitLab repository.",
        action="store_true",
    )
    parser_create.add_argument(
        "--private",
        help="Create a private GitLab repository.",
        action="store_true",
    )
    parser_create.add_argument(
        "--description",
        help="Repository description.",
    )

    parser_mirror = subparsers.add_parser(
        "mirror", help=mirror_repo.__doc__.splitlines()[0]
    )
    parser_mirror.set_defaults(func=mirror_repo)
    parser_mirror.add_argument(
        "name", help="Name of the GitLab repository.", nargs="?"
    )

    parser_archive = subparsers.add_parser(
        "archive", help=archive_repo.__doc__.splitlines()[0]
    )
    parser_archive.set_defaults(func=archive_repo)
    parser_archive.add_argument(
        "name", help="Name of the repository.", nargs="?"
    )
    parser_archive.add_argument(
        "--github", help="The repository is in GitHub.", action="store_true"
    )

    parser_fork = subparsers.add_parser(
        "fork", help="Forks a GitHub repository."
    )
    parser_fork.set_defaults(func=fork_repo)
    parser_fork.add_argument("name", help="Name of the repository.")

    parser_tfinit = subparsers.add_parser(
        "tfinit", help=terraform_init.__doc__.splitlines()[0]
    )
    parser_tfinit.set_defaults(func=terraform_init)
    parser_tfinit.add_argument(
        "state",
        help="The Terraform state name",
        default="default",
        nargs="?",
    )

    return parser


if __name__ == "__main__":
    arg_parser = build_arg_parser()
    _args = arg_parser.parse_args()
    _args.func(_args)
