"""BuildService — build images from git repositories.

Handles: git archive, docker build, push/load.
"""

from __future__ import annotations

import contextlib
import os
import shlex
import subprocess
import tempfile
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING

from kuberoku import branding
from kuberoku.exceptions import BuildError

if TYPE_CHECKING:
    from kuberoku.factory import KuberokuFactory


@dataclass(frozen=True)
class BuildResult:
    """Result of a build operation."""

    image: str
    commit_sha: str
    ref_name: str


class BuildService:
    """Business logic for building images from git."""

    def __init__(self, factory: KuberokuFactory) -> None:
        self._factory = factory

    def build(
        self,
        app: str,
        ref: str = "HEAD",
        on_step: Callable[[str], None] | None = None,
    ) -> BuildResult:
        """Build an image from a git ref."""
        self._check_prerequisites()

        if on_step:
            on_step("Resolving git ref...")
        full_sha, short_sha = self._resolve_ref(ref)

        uncommitted = self._check_uncommitted()
        if uncommitted > 0 and on_step:
            on_step(f"Warning: {uncommitted} uncommitted changes (not included in build)")

        # Determine registry
        registry = self._factory.registry.get_or_detect(on_step=on_step)
        is_local = self._factory.registry.is_local_cluster()

        image = f"{registry}/{app}:{short_sha}" if registry else f"{app}:{short_sha}"

        # Git archive to temp dir
        if on_step:
            on_step("Creating build context from git archive...")
        with tempfile.TemporaryDirectory() as tmpdir:
            archive_path = Path(tmpdir) / "context.tar"
            self._git_archive(full_sha, archive_path)

            # Docker build
            if on_step:
                on_step(f"Building image {image}...")
            self._docker_build(archive_path, image)

        # Push or load
        if registry:
            if on_step:
                on_step(f"Pushing {image}...")
            self._docker_push(image)
        elif is_local:
            if on_step:
                on_step("Loading image into local cluster...")
            self._local_load(image)
        else:
            raise BuildError(
                "No container registry configured for remote cluster. "
                f"Set {branding.ENV_PREFIX}REGISTRY to a registry you can push to "
                f"(e.g. {branding.ENV_PREFIX}REGISTRY=my-registry.example.com)."
            )

        return BuildResult(image=image, commit_sha=full_sha, ref_name=ref)

    def _check_prerequisites(self) -> None:
        """Check that git and docker are available."""
        try:
            subprocess.run(
                ["git", "rev-parse", "--git-dir"],
                capture_output=True,
                check=True,
            )
        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            raise BuildError("Not a git repository or git not installed.") from e

        try:
            subprocess.run(
                ["docker", "info"],
                capture_output=True,
                check=True,
            )
        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            raise BuildError("Docker not installed or not running.") from e

        if not Path("Dockerfile").is_file():
            raise BuildError("No Dockerfile found in the current directory.")

    def _resolve_ref(self, ref: str) -> tuple[str, str]:
        """Resolve a git ref to (full_sha, short_sha)."""
        try:
            result = subprocess.run(
                ["git", "rev-parse", ref],
                capture_output=True,
                text=True,
                check=True,
            )
            full_sha = result.stdout.strip()
            short_sha = full_sha[:7]
            return full_sha, short_sha
        except subprocess.CalledProcessError as e:
            raise BuildError(f"Cannot resolve git ref '{ref}'.") from e

    def _check_uncommitted(self) -> int:
        """Count uncommitted changes."""
        try:
            result = subprocess.run(
                ["git", "status", "--porcelain"],
                capture_output=True,
                text=True,
                check=True,
            )
            lines = [line for line in result.stdout.strip().split("\n") if line.strip()]
            return len(lines)
        except subprocess.CalledProcessError:
            return 0

    def _git_archive(self, sha: str, dest: Path) -> None:
        """Create a tar archive of the git tree at a given SHA."""
        try:
            subprocess.run(
                ["git", "archive", "-o", str(dest), sha],
                capture_output=True,
                check=True,
            )
        except subprocess.CalledProcessError as e:
            raise BuildError(f"git archive failed: {e.stderr}") from e

    def _docker_build(self, context: Path, tag: str) -> None:
        """Build a Docker image from an archive context."""
        try:
            subprocess.run(
                ["docker", "build", "-t", tag, "-"],
                input=context.read_bytes(),
                capture_output=True,
                check=True,
            )
        except subprocess.CalledProcessError as e:
            raise BuildError(f"Docker build failed: {e.stderr}") from e

    def _docker_push(self, image: str) -> None:
        """Push an image to a remote registry."""
        try:
            subprocess.run(
                ["docker", "push", image],
                capture_output=True,
                check=True,
            )
        except subprocess.CalledProcessError as e:
            raise BuildError(f"Docker push failed: {e.stderr}") from e

    def _local_load(self, image: str) -> None:
        """Load an image into a local cluster (k3d/kind/minikube)."""
        cmd = self._factory.registry.local_load_command(image)
        if cmd:
            with contextlib.suppress(subprocess.CalledProcessError):
                subprocess.run(cmd, capture_output=True, check=True)
        # Also check KUBEROKU_LOCAL_LOAD env var
        env_cmd = os.environ.get("KUBEROKU_LOCAL_LOAD")
        if env_cmd:
            with contextlib.suppress(subprocess.CalledProcessError):
                subprocess.run(
                    shlex.split(env_cmd) + [image],
                    capture_output=True,
                    check=True,
                )
