#!/usr/bin/env python3
from __future__ import annotations

import argparse
import hashlib
import os
import re
import shlex
import subprocess
import sys
from pathlib import Path


class ComposeBranchCommand:
    max_project_length = 63

    def __init__(self, working_directory: Path) -> None:
        self.working_directory = working_directory

    def _git(self, *arguments: str) -> str:
        result = subprocess.run(
            ["git", *arguments],
            cwd=self.working_directory,
            check=False,
            capture_output=True,
            text=True,
        )
        if result.returncode != 0:
            raise RuntimeError(result.stderr.strip() or "Git command failed")
        return result.stdout.strip()

    @classmethod
    def project_name(cls, repository: str, branch: str) -> str:
        raw_name = f"{repository}-{branch}".lower()
        safe_name = re.sub(r"[^a-z0-9_-]+", "-", raw_name).strip("-_")
        safe_name = re.sub(r"-{2,}", "-", safe_name)
        if not safe_name:
            raise RuntimeError("Git repository and branch names are empty")
        if safe_name == raw_name and len(safe_name) <= cls.max_project_length:
            return safe_name

        digest = hashlib.sha256(raw_name.encode()).hexdigest()[:8]
        prefix = safe_name[: cls.max_project_length - len(digest) - 1].rstrip("-_")
        return f"{prefix}-{digest}"

    def run(
        self,
        compose_arguments: list[str],
        dry_run: bool,
        port: str | None,
    ) -> int:
        repository_root = Path(self._git("rev-parse", "--show-toplevel"))
        branch = self._git("branch", "--show-current")
        if not branch:
            raise RuntimeError("Check out a Git branch before running this command")

        try:
            origin = self._git("config", "--get", "remote.origin.url")
        except RuntimeError:
            origin = ""
        repository = Path(origin.removesuffix(".git")).name or repository_root.name
        project = self.project_name(repository, branch)
        command = ["docker", "compose", "-p", project, *compose_arguments]
        rendered_command = shlex.join(command)
        print(f"PORT={port} {rendered_command}" if port is not None else rendered_command)
        if dry_run:
            return 0
        environment = None
        if port is not None:
            environment = os.environ.copy()
            environment["PORT"] = port
        return subprocess.run(
            command,
            cwd=self.working_directory,
            check=False,
            env=environment,
        ).returncode


def compose_port(value: str) -> str:
    if value == "auto":
        return "0"
    try:
        port = int(value)
    except ValueError as error:
        raise argparse.ArgumentTypeError("port must be auto or a number") from error
    if not 1 <= port <= 65535:
        raise argparse.ArgumentTypeError("port must be between 1 and 65535")
    return str(port)


def argument_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Run Docker Compose in a project isolated by Git branch."
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Print the Docker Compose command without running it.",
    )
    parser.add_argument(
        "--port",
        type=compose_port,
        metavar="PORT",
        help="Set PORT for Compose; use 'auto' to let Docker choose a port.",
    )
    parser.add_argument(
        "compose_arguments",
        nargs=argparse.REMAINDER,
        help="Arguments passed to docker compose.",
    )
    return parser


def main() -> int:
    parser = argument_parser()
    arguments = parser.parse_args()
    compose_arguments = arguments.compose_arguments
    if compose_arguments[:1] == ["--"]:
        compose_arguments = compose_arguments[1:]
    if not compose_arguments:
        parser.error("provide a Docker Compose command, for example: up -d")

    try:
        return ComposeBranchCommand(Path.cwd()).run(
            compose_arguments,
            arguments.dry_run,
            arguments.port,
        )
    except (OSError, RuntimeError) as error:
        print(f"compose_branch.py: {error}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
