#!/usr/bin/env python3
import argparse
import re
import sys
from datetime import datetime
from pathlib import Path
from subprocess import check_output, run

from shared.common import REPO_DIR
from shared.versioning import get_branch_version

HEADER = "## [Unreleased](https://github.com/LostArtefacts/TR1X/compare/stable...develop) - ××××-××-××"
CHANGELOG_PATH = REPO_DIR / "CHANGELOG.md"


def update_changelog(
    changelog: str, old_version: str, new_version: str
) -> str:
    if f"[{new_version}]" in changelog:
        return changelog
    changelog = re.sub("Unreleased", new_version, changelog, count=1)
    changelog = re.sub("stable", old_version, changelog, count=1)
    changelog = re.sub("develop", new_version, changelog, count=1)
    changelog = re.sub(
        "××××-××-××", datetime.now().strftime("%Y-%m-%d"), changelog
    )
    changelog = HEADER + "\n\n" + changelog
    return changelog


class Git:
    def checkout_branch(self, branch_name: str) -> None:
        if check_output(["git", "diff", "--cached", "--name-only"]):
            raise RuntimeError("Staged files")
        check_output(["git", "checkout", branch_name])

    def reset(self, target: str, hard: bool = False) -> None:
        check_output(
            ["git", "reset", "develop", *(["--hard"] if hard else [])]
        )

    def delete_tag(self, tag_name: str) -> None:
        run(["git", "tag", "-d", tag_name])

    def create_tag(self, tag_name: str) -> None:
        check_output(["git", "tag", tag_name])

    def add(self, target: str) -> None:
        check_output(["git", "add", target])

    def commit(self, message: str) -> None:
        check_output(["git", "commit", "-m", message])

    def push(
        self, upstream: str, targets: list[str], force: bool = False
    ) -> None:
        check_output(
            [
                "git",
                "push",
                upstream,
                *targets,
                *(["--force-with-lease"] if force else []),
            ]
        )


class BaseCommand:
    name: str = NotImplemented
    help: str = NotImplemented

    def __init__(self, git: Git) -> None:
        self.git = git

    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:
        parser.add_argument("version")

    def run(self, args: argparse.Namespace) -> None:
        raise NotImplementedError("not implemented")


class CommitCommand(BaseCommand):
    name = "commit"
    help = "Create and tag a commit with the release information"

    def decorate_parser(self, parser: argparse.ArgumentParser) -> None:
        super().decorate_parser(parser)
        parser.add_argument(
            "-d",
            "--dry-run",
            action='store_true',
            help="only output the changelog to stdout, do not commit anything",
        )

    def run(self, args: argparse.Namespace) -> None:
        self.git.checkout_branch("develop")
        old_version = get_branch_version("origin/stable")
        new_version = args.version

        old_changelog = CHANGELOG_PATH.read_text()
        new_changelog = update_changelog(
            old_changelog, old_version, args.version
        )
        if old_changelog == new_changelog:
            return

        if args.dry_run:
            print(new_changelog)
            return

        CHANGELOG_PATH.write_text(new_changelog)
        self.git.add(CHANGELOG_PATH)
        self.git.commit(f"docs: release {args.version}")
        self.git.delete_tag(args.version)
        self.git.create_tag(args.version)


class BranchCommand(BaseCommand):
    name = "branch"
    help = "Merge branch to the specified tag"

    def run(self, args: argparse.Namespace) -> None:
        self.git.checkout_branch("stable")
        self.git.reset(args.version, hard=True)
        self.git.checkout_branch("develop")


class PushCommand(BaseCommand):
    name = "push"
    help = (
        "Push the develop and stable branches, and the version tag to GitHub"
    )

    def run(self, args) -> None:
        self.git.push(
            "origin", ["develop", "stable", args.version], force=True
        )


def parse_args(commands: list[BaseCommand]) -> None:
    parser = argparse.ArgumentParser(
        description="Argument parser with subcommands"
    )
    subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")
    for command in commands:
        subparser = subparsers.add_parser(command.name, help=command.help)
        command.decorate_parser(subparser)
        subparser.set_defaults(command=command)

    result = parser.parse_args()
    if not hasattr(result, "command"):
        parser.error("missing command")
    return result


def main() -> None:
    git = Git()
    commands = [
        command_cls(git) for command_cls in BaseCommand.__subclasses__()
    ]
    args = parse_args(commands)
    args.command.run(args)


main()
