#!/usr/bin/env python3
"""Release helper: git commit/push + docker build/push.

Flow:
1) Prompt for version
2) git add -A
3) git commit -m "Version <version>" (skips if nothing to commit)
4) git push -u origin main (ensures origin points to target repo)
5) docker build + push tags: <version> and latest
"""

from __future__ import annotations

import argparse
import re
import shutil
import subprocess
from dataclasses import dataclass


@dataclass(frozen=True)
class Config:
    target_git_url: str = "https://git.alphen.cloud/bramval/PsalmbordOnlineCE.git"
    docker_image: str = "git.alphen.cloud/bramval/psalmbordonlinece"
    branch: str = "main"


VERSION_RE = re.compile(r"^[0-9A-Za-z][0-9A-Za-z._-]{0,127}$")


def run(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
    """Run command and stream output; return CompletedProcess."""
    print(f"\n$ {' '.join(cmd)}")
    return subprocess.run(cmd, text=True, check=check)


def capture(cmd: list[str]) -> str:
    """Run command and capture stdout."""
    return subprocess.check_output(cmd, text=True).strip()


def ensure_tools() -> None:
    for tool in ("git", "docker"):
        if shutil.which(tool) is None:
            raise SystemExit(f"Error: '{tool}' is not available in PATH")


def ensure_git_repo() -> None:
    try:
        out = subprocess.check_output(["git", "rev-parse", "--is-inside-work-tree"], text=True).strip()
    except subprocess.CalledProcessError:
        raise SystemExit("Error: not a git repository (run from within the repo)")
    if out.lower() != "true":
        raise SystemExit("Error: not a git work tree")


def prompt_version() -> str:
    version = input("Enter version (e.g. 1.2.3): ").strip()
    if not version:
        raise SystemExit("Error: version cannot be empty")
    if not VERSION_RE.match(version):
        raise SystemExit(
            "Error: invalid version. Use only letters/numbers and . _ - (max 128 chars)"
        )
    return version


def prompt_yes_no(question: str, default_no: bool = True) -> bool:
    prompt = "(y/N)" if default_no else "(Y/n)"
    ans = input(f"{question} {prompt}: ").strip().lower()
    if not ans:
        return not default_no
    return ans in ("y", "yes")


def ensure_origin(config: Config) -> None:
    try:
        origin_url = capture(["git", "remote", "get-url", "origin"])
    except subprocess.CalledProcessError:
        origin_url = ""

    if not origin_url:
        print(f"Remote 'origin' not found; adding {config.target_git_url}")
        run(["git", "remote", "add", "origin", config.target_git_url])
        return

    if origin_url != config.target_git_url:
        print(
            f"Remote 'origin' is '{origin_url}' -> updating to '{config.target_git_url}'"
        )
        run(["git", "remote", "set-url", "origin", config.target_git_url])


def parse_args(config: Config) -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description=(
            "Commit+push to PsalmbordOnlineCE and build+push docker image (version + latest)."
        )
    )
    parser.add_argument(
        "--version",
        dest="version",
        help="Version string used for commit message (Version <version>) and Docker tag",
    )
    parser.add_argument(
        "--yes",
        action="store_true",
        help="Skip confirmation prompts",
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Print what would happen without committing/pushing/building/pushing docker",
    )
    parser.add_argument(
        "--branch",
        default=config.branch,
        help=f"Branch to push (default: {config.branch})",
    )
    parser.add_argument(
        "--remote-url",
        default=config.target_git_url,
        help=f"Remote URL to set for origin (default: {config.target_git_url})",
    )
    parser.add_argument(
        "--docker-image",
        default=config.docker_image,
        help=f"Docker image name (default: {config.docker_image})",
    )
    return parser.parse_args()


def main() -> int:
    config = Config()
    args = parse_args(config)

    ensure_tools()
    ensure_git_repo()

    version = (args.version or "").strip() or prompt_version()
    if not VERSION_RE.match(version):
        raise SystemExit(
            "Error: invalid version. Use only letters/numbers and . _ - (max 128 chars)"
        )

    config = Config(
        target_git_url=args.remote_url,
        docker_image=args.docker_image,
        branch=args.branch,
    )

    commit_message = f"Version {version}"

    # Branch warning
    try:
        current_branch = capture(["git", "branch", "--show-current"])
    except subprocess.CalledProcessError:
        current_branch = ""
    if current_branch and current_branch != config.branch:
        print(
            f"Warning: you are on branch '{current_branch}' (expected '{config.branch}')."
        )
        if not args.yes and not prompt_yes_no(
            f"Continue and push '{config.branch}' anyway?", default_no=True
        ):
            return 1

    ensure_origin(config)

    print("\n== Git: status ==")
    run(["git", "status", "-sb"], check=False)

    if args.dry_run:
        print("\n== Dry run ==")
        print("Would run:")
        for c in (
            ["git", "add", "-A"],
            ["git", "commit", "-m", commit_message],
            ["git", "push", "-u", "origin", config.branch],
            [
                "docker",
                "build",
                "-t",
                f"{config.docker_image}:{version}",
                "-t",
                f"{config.docker_image}:latest",
                ".",
            ],
            ["docker", "push", f"{config.docker_image}:{version}"],
            ["docker", "push", f"{config.docker_image}:latest"],
        ):
            print(f"  - {' '.join(c)}")
        return 0

    if not args.yes and not prompt_yes_no(
        "Proceed with commit+push and docker push?", default_no=True
    ):
        print("Aborted.")
        return 1

    print("\n== Git: add/commit/push ==")
    run(["git", "add", "-A"])

    # commit may fail if nothing to commit
    try:
        run(["git", "commit", "-m", commit_message])
        print(f"Committed: {commit_message}")
    except subprocess.CalledProcessError:
        print("No changes to commit (or commit failed). Continuing...")

    try:
        run(["git", "push", "-u", "origin", config.branch])
    except subprocess.CalledProcessError as e:
        print(
            "\nGit push failed. If you're using Gitea over HTTPS, you likely need to authenticate with a Personal Access Token (PAT) instead of your password.\n"
            "- Ensure you can push: git remote -v\n"
            "- Configure credentials (Windows): Credential Manager / Git Credential Manager\n"
            "- Or switch to SSH remote and ensure your key is added on the server\n"
        )
        return int(getattr(e, "returncode", 1) or 1)

    print("\n== Docker: build/tag/push ==")
    print(f"Docker image: {config.docker_image}")
    print(f"Tags: {version}, latest")

    run(
        [
            "docker",
            "build",
            "-t",
            f"{config.docker_image}:{version}",
            "-t",
            f"{config.docker_image}:latest",
            ".",
        ]
    )
    try:
        run(["docker", "push", f"{config.docker_image}:{version}"])
        run(["docker", "push", f"{config.docker_image}:latest"])
    except subprocess.CalledProcessError as e:
        print(
            "\nDocker push failed. Make sure you're logged in:\n"
            "  docker login git.alphen.cloud\n"
        )
        return int(getattr(e, "returncode", 1) or 1)

    print("\nDone.")
    print("If docker push failed due to auth, run: docker login git.alphen.cloud")
    return 0


if __name__ == "__main__":
    # Make Ctrl+C exit nicely
    try:
        raise SystemExit(main())
    except KeyboardInterrupt:
        print("\nAborted (Ctrl+C).")
        raise SystemExit(130)
