"""Helm CLI wrapper — thin layer over subprocess.run()."""

from __future__ import annotations

import json
import subprocess
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from ilum.constants import CHART_REPO_NAME, CHART_REPO_URL, DEFAULT_NAMESPACE, HELM_TIMEOUT
from ilum.errors import HelmError, HelmTimeoutError, PrerequisiteError


@dataclass
class HelmResult:
    """Captures the outcome of a single Helm invocation."""

    returncode: int
    stdout: str
    stderr: str
    json_data: Any = None
    command: list[str] = field(default_factory=list)
    duration_seconds: float = 0.0


class HelmClient:
    """Stateful wrapper around the ``helm`` binary.

    Every public method maps to one Helm sub-command and returns a
    :class:`HelmResult`.  Errors are surfaced as :class:`HelmError` (or
    :class:`HelmTimeoutError` for deadline exceeded).
    """

    def __init__(
        self,
        kubecontext: str = "",
        namespace: str = DEFAULT_NAMESPACE,
        dry_run: bool = False,
        timeout: str = HELM_TIMEOUT,
    ) -> None:
        self.kubecontext = kubecontext
        self.namespace = namespace
        self.dry_run = dry_run
        self.timeout = timeout

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _build_full_cmd(self, args: list[str]) -> list[str]:
        """Prepend ``helm`` and append global flags to *args*."""
        cmd: list[str] = ["helm"] + args
        if self.kubecontext:
            cmd += ["--kube-context", self.kubecontext]
        if self.namespace:
            cmd += ["--namespace", self.namespace]
        return cmd

    def _run(
        self,
        args: list[str],
        timeout_seconds: int = 600,
        capture_json: bool = False,
        read_only: bool = False,
    ) -> HelmResult:
        """Execute a Helm command and return the result.

        Parameters
        ----------
        args:
            Sub-command tokens **without** the leading ``helm``.
        timeout_seconds:
            Maximum wall-clock time before the process is killed.
        capture_json:
            When *True*, parse *stdout* as JSON into
            :pyattr:`HelmResult.json_data`.
        read_only:
            When *True*, execute the command even in dry-run mode.
            Use for read-only queries (list, get values, history, etc.)
            that are needed to plan write operations.
        """
        cmd = self._build_full_cmd(args)

        if self.dry_run and not read_only:
            return HelmResult(
                returncode=0,
                stdout=f"dry-run: would execute: {' '.join(cmd)}",
                stderr="",
                command=cmd,
            )

        start = time.monotonic()
        try:
            proc = subprocess.run(
                cmd,
                capture_output=True,
                text=True,
                timeout=timeout_seconds,
            )
        except FileNotFoundError as exc:
            raise PrerequisiteError(
                "helm binary not found on PATH.",
                suggestion="Install Helm: https://helm.sh/docs/intro/install/ or run 'ilum doctor' to check prerequisites.",
                error_code="ILUM-001",
            ) from exc
        except subprocess.TimeoutExpired as exc:
            raise HelmTimeoutError(
                f"Helm command timed out after {timeout_seconds}s: {' '.join(cmd)}",
                suggestion="Increase the timeout or check cluster connectivity.",
                error_code="ILUM-021",
            ) from exc

        duration = time.monotonic() - start

        if proc.returncode != 0:
            raise HelmError(
                proc.stderr.strip() or f"Helm command failed (rc={proc.returncode})",
                suggestion=f"Command was: {' '.join(cmd)}",
                error_code="ILUM-020",
            )

        json_data: Any = None
        if capture_json:
            try:
                json_data = json.loads(proc.stdout)
            except json.JSONDecodeError:
                json_data = None

        return HelmResult(
            returncode=proc.returncode,
            stdout=proc.stdout,
            stderr=proc.stderr,
            json_data=json_data,
            command=cmd,
            duration_seconds=duration,
        )

    @staticmethod
    def _values_and_set_args(
        values_files: list[Path] | None,
        set_flags: list[str] | None,
    ) -> list[str]:
        """Build ``-f`` and ``--set`` tokens from optional inputs."""
        args: list[str] = []
        for vf in values_files or []:
            args += ["-f", str(vf)]
        for sf in set_flags or []:
            args += ["--set", sf]
        return args

    # ------------------------------------------------------------------
    # Arg builders (shared by execute + preview)
    # ------------------------------------------------------------------

    def _install_args(
        self,
        release: str,
        chart: str,
        values_files: list[Path] | None = None,
        set_flags: list[str] | None = None,
        atomic: bool = False,
        wait: bool = False,
        version: str = "",
        devel: bool = False,
    ) -> list[str]:
        args: list[str] = ["install", release, chart]
        if atomic:
            args.append("--atomic")
        if wait:
            args.append("--wait")
        if version:
            args += ["--version", version]
        if devel:
            args.append("--devel")
        args += ["--timeout", self.timeout]
        args += self._values_and_set_args(values_files, set_flags)
        return args

    def _upgrade_args(
        self,
        release: str,
        chart: str,
        values_files: list[Path] | None = None,
        set_flags: list[str] | None = None,
        atomic: bool = False,
        wait: bool = False,
        version: str = "",
        reuse_values: bool = False,
        reset_then_reuse_values: bool = False,
        devel: bool = False,
    ) -> list[str]:
        args: list[str] = ["upgrade", release, chart]
        if atomic:
            args.append("--atomic")
        if wait:
            args.append("--wait")
        if version:
            args += ["--version", version]
        if devel:
            args.append("--devel")
        if reuse_values:
            args.append("--reuse-values")
        elif reset_then_reuse_values:
            args.append("--reset-then-reuse-values")
        args += ["--timeout", self.timeout]
        args += self._values_and_set_args(values_files, set_flags)
        return args

    @staticmethod
    def _uninstall_args(release: str) -> list[str]:
        return ["uninstall", release]

    # ------------------------------------------------------------------
    # Public commands
    # ------------------------------------------------------------------

    def install(
        self,
        release: str,
        chart: str,
        values_files: list[Path] | None = None,
        set_flags: list[str] | None = None,
        atomic: bool = False,
        wait: bool = False,
        version: str = "",
        devel: bool = False,
    ) -> HelmResult:
        """``helm install <release> <chart> [flags]``."""
        return self._run(
            self._install_args(
                release, chart, values_files, set_flags, atomic, wait, version, devel
            )
        )

    def upgrade(
        self,
        release: str,
        chart: str,
        values_files: list[Path] | None = None,
        set_flags: list[str] | None = None,
        atomic: bool = False,
        wait: bool = False,
        version: str = "",
        reuse_values: bool = False,
        reset_then_reuse_values: bool = False,
        devel: bool = False,
    ) -> HelmResult:
        """``helm upgrade <release> <chart> [flags]``."""
        return self._run(
            self._upgrade_args(
                release,
                chart,
                values_files,
                set_flags,
                atomic,
                wait,
                version,
                reuse_values,
                reset_then_reuse_values,
                devel,
            )
        )

    def uninstall(self, release: str) -> HelmResult:
        """``helm uninstall <release>``."""
        return self._run(self._uninstall_args(release))

    # ------------------------------------------------------------------
    # Preview (command list without execution)
    # ------------------------------------------------------------------

    def preview_install(
        self,
        release: str,
        chart: str,
        values_files: list[Path] | None = None,
        set_flags: list[str] | None = None,
        atomic: bool = False,
        wait: bool = False,
        version: str = "",
        devel: bool = False,
    ) -> list[str]:
        """Return the full command list for ``helm install`` without executing."""
        return self._build_full_cmd(
            self._install_args(
                release, chart, values_files, set_flags, atomic, wait, version, devel
            )
        )

    def preview_upgrade(
        self,
        release: str,
        chart: str,
        values_files: list[Path] | None = None,
        set_flags: list[str] | None = None,
        atomic: bool = False,
        wait: bool = False,
        version: str = "",
        reuse_values: bool = False,
        reset_then_reuse_values: bool = False,
        devel: bool = False,
    ) -> list[str]:
        """Return the full command list for ``helm upgrade`` without executing."""
        return self._build_full_cmd(
            self._upgrade_args(
                release,
                chart,
                values_files,
                set_flags,
                atomic,
                wait,
                version,
                reuse_values,
                reset_then_reuse_values,
                devel,
            )
        )

    def preview_uninstall(self, release: str) -> list[str]:
        """Return the full command list for ``helm uninstall`` without executing."""
        return self._build_full_cmd(self._uninstall_args(release))

    def list_releases(self, all_namespaces: bool = False) -> HelmResult:
        """``helm list --all --output json``.

        When *all_namespaces* is True, passes ``--all-namespaces`` to scan
        every namespace.
        """
        args = ["list", "--all", "--output", "json"]
        if all_namespaces:
            args.append("--all-namespaces")
        return self._run(args, capture_json=True, read_only=True)

    def release_exists(self, release: str) -> bool:
        """Return ``True`` if *release* exists in the current namespace."""
        try:
            result = self.list_releases()
        except HelmError:
            return False
        return any(rel.get("name") == release for rel in result.json_data or [])

    def get_values(self, release: str) -> HelmResult:
        """``helm get values <release> --output json``."""
        return self._run(
            ["get", "values", release, "--output", "json"],
            capture_json=True,
            read_only=True,
        )

    def history(self, release: str) -> HelmResult:
        """``helm history <release> --output json``."""
        return self._run(
            ["history", release, "--output", "json"],
            capture_json=True,
            read_only=True,
        )

    def template(
        self,
        chart: str,
        values_files: list[Path] | None = None,
        set_flags: list[str] | None = None,
    ) -> HelmResult:
        """``helm template <chart> [values/set flags]``."""
        args: list[str] = ["template", "ilum-template", chart]
        args += self._values_and_set_args(values_files, set_flags)
        return self._run(args, read_only=True)

    def repo_add(
        self,
        name: str = CHART_REPO_NAME,
        url: str = CHART_REPO_URL,
    ) -> HelmResult:
        """``helm repo add <name> <url>``."""
        return self._run(["repo", "add", name, url], read_only=True)

    def dependency_build(self, chart_path: str) -> HelmResult:
        """``helm dependency build <chart_path>``."""
        return self._run(["dependency", "build", chart_path])

    def rollback(self, release: str, revision: int = 0) -> HelmResult:
        """``helm rollback <release> [revision]``.

        When *revision* is 0 (the default) Helm rolls back to the
        previous release.
        """
        args: list[str] = ["rollback", release]
        if revision:
            args.append(str(revision))
        args += ["--timeout", self.timeout]
        return self._run(args)

    def repo_update(self) -> HelmResult:
        """``helm repo update``."""
        return self._run(["repo", "update"], read_only=True)

    def get_values_at_revision(self, release: str, revision: int) -> HelmResult:
        """``helm get values <release> --revision N --output json``."""
        return self._run(
            ["get", "values", release, "--revision", str(revision), "--output", "json"],
            capture_json=True,
            read_only=True,
        )

    def get_values_all(self, release: str) -> HelmResult:
        """``helm get values <release> --all --output json``.

        Returns computed values (user-supplied + chart defaults).
        """
        return self._run(
            ["get", "values", release, "--all", "--output", "json"],
            capture_json=True,
            read_only=True,
        )

    def show_values(self, chart: str, version: str = "") -> HelmResult:
        """``helm show values <chart>``."""
        args = ["show", "values", chart]
        if version:
            args += ["--version", version]
        return self._run(args, read_only=True)

    def search_repo(self, chart: str, version: str = "", devel: bool = False) -> HelmResult:
        """``helm search repo <chart> --output json``.

        Returns available chart versions from configured repos.
        When *devel* is True, includes pre-release versions.
        """
        cmd = ["search", "repo", chart, "--output", "json"]
        if version:
            cmd.extend(["--version", version])
        if devel:
            cmd.append("--devel")
        return self._run(cmd, capture_json=True, read_only=True)

    def version(self) -> HelmResult:
        """``helm version --short``."""
        return self._run(["version", "--short"], read_only=True)
