"""Values-safety system: snapshot I/O, drift detection, and diff computation.

Every upgrade/enable/disable operation follows a fetch-merge-diff-apply
pipeline to ensure the CLI never silently discards values that were changed
outside it (e.g. via ``helm upgrade --set custom.value=foo``).
"""

from __future__ import annotations

import copy
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from ruamel.yaml import YAML

from ilum.cli.output import _SYM_DASH

# ---------------------------------------------------------------------------
# Data structures
# ---------------------------------------------------------------------------


@dataclass(frozen=True)
class ValuesSnapshot:
    """Point-in-time capture of values the CLI applied."""

    release: str
    namespace: str
    timestamp: str  # ISO 8601
    chart_version: str
    values: dict[str, Any]
    operation: str  # "install" | "upgrade" | "enable" | "disable"


@dataclass
class ValuesDiff:
    """Diff between two values dictionaries (flattened to dot-paths)."""

    added: dict[str, Any] = field(default_factory=dict)
    removed: dict[str, Any] = field(default_factory=dict)
    changed: dict[str, tuple[Any, Any]] = field(default_factory=dict)

    @property
    def is_empty(self) -> bool:
        return not self.added and not self.removed and not self.changed


@dataclass
class DriftResult:
    """Outcome of comparing live values against last CLI snapshot."""

    has_drift: bool
    snapshot_exists: bool
    diff: ValuesDiff | None  # None when no snapshot exists
    live_values: dict[str, Any]


# ---------------------------------------------------------------------------
# Pure functions
# ---------------------------------------------------------------------------


def flatten_dict(d: dict[str, Any], prefix: str = "") -> dict[str, Any]:
    """Flatten a nested dict to dot-path keys.

    >>> flatten_dict({"a": {"b": 1, "c": 2}})
    {'a.b': 1, 'a.c': 2}
    """
    items: dict[str, Any] = {}
    for key, value in d.items():
        new_key = f"{prefix}{key}" if not prefix else f"{prefix}.{key}"
        if isinstance(value, dict) and value:
            items.update(flatten_dict(value, new_key))
        else:
            items[new_key] = value
    return items


def compute_diff(old: dict[str, Any], new: dict[str, Any]) -> ValuesDiff:
    """Compare two flattened dicts and return a :class:`ValuesDiff`."""
    flat_old = flatten_dict(old)
    flat_new = flatten_dict(new)

    all_keys = set(flat_old) | set(flat_new)

    added: dict[str, Any] = {}
    removed: dict[str, Any] = {}
    changed: dict[str, tuple[Any, Any]] = {}

    for key in sorted(all_keys):
        in_old = key in flat_old
        in_new = key in flat_new
        if in_old and not in_new:
            removed[key] = flat_old[key]
        elif not in_old and in_new:
            added[key] = flat_new[key]
        elif flat_old[key] != flat_new[key]:
            changed[key] = (flat_old[key], flat_new[key])

    return ValuesDiff(added=added, removed=removed, changed=changed)


# ---------------------------------------------------------------------------
# Snapshot I/O
# ---------------------------------------------------------------------------

_YAML = YAML()
_YAML.default_flow_style = False


def _sanitize_context(context: str) -> str:
    """Sanitize a kubeconfig context name for use in file paths."""
    return re.sub(r"[^a-zA-Z0-9_-]", "_", context)


def _get_current_context() -> str:
    """Get the current kubeconfig context name, or 'default' if unavailable."""
    try:
        from kubernetes.config import list_kube_config_contexts

        _, active = list_kube_config_contexts()
        return str(active.get("name", "default")) if active else "default"
    except Exception:
        return "default"


def _snapshot_path(release: str, namespace: str, state_dir: Path, context: str = "") -> Path:
    ctx = _sanitize_context(context) if context else _sanitize_context(_get_current_context())
    return state_dir / "snapshots" / f"{ctx}--{release}--{namespace}.yaml"


def _legacy_snapshot_path(release: str, namespace: str, state_dir: Path) -> Path:
    """Legacy path without context prefix (for migration)."""
    return state_dir / "snapshots" / f"{release}--{namespace}.yaml"


def save_snapshot(snapshot: ValuesSnapshot, state_dir: Path, context: str = "") -> None:
    """Persist a :class:`ValuesSnapshot` to the state directory."""
    path = _snapshot_path(snapshot.release, snapshot.namespace, state_dir, context)
    path.parent.mkdir(parents=True, exist_ok=True)

    data = {
        "release": snapshot.release,
        "namespace": snapshot.namespace,
        "timestamp": snapshot.timestamp,
        "chart_version": snapshot.chart_version,
        "operation": snapshot.operation,
        "values": snapshot.values,
    }
    _YAML.dump(data, path)


def load_snapshot(
    release: str,
    namespace: str,
    state_dir: Path,
    context: str = "",
) -> ValuesSnapshot | None:
    """Load a snapshot from disk, or ``None`` if it does not exist."""
    path = _snapshot_path(release, namespace, state_dir, context)
    if not path.exists():
        # Try legacy path and migrate if found
        legacy = _legacy_snapshot_path(release, namespace, state_dir)
        if legacy.exists():
            import shutil

            path.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(str(legacy), str(path))
        else:
            return None

    data = _YAML.load(path)
    if data is None:
        return None

    return ValuesSnapshot(
        release=data["release"],
        namespace=data["namespace"],
        timestamp=data.get("timestamp", ""),
        chart_version=data.get("chart_version", ""),
        values=dict(data.get("values") or {}),
        operation=data.get("operation", "unknown"),
    )


def delete_snapshot(release: str, namespace: str, state_dir: Path, context: str = "") -> None:
    """Remove a snapshot file if it exists."""
    path = _snapshot_path(release, namespace, state_dir, context)
    if path.exists():
        path.unlink()


# ---------------------------------------------------------------------------
# Drift detection
# ---------------------------------------------------------------------------


def detect_drift(
    release: str,
    namespace: str,
    live_values: dict[str, Any],
    state_dir: Path,
    context: str = "",
) -> DriftResult:
    """Compare *live_values* against the last CLI snapshot.

    Returns a :class:`DriftResult` indicating whether external changes
    were detected.
    """
    snapshot = load_snapshot(release, namespace, state_dir, context)

    if snapshot is None:
        return DriftResult(
            has_drift=False,
            snapshot_exists=False,
            diff=None,
            live_values=live_values,
        )

    diff = compute_diff(snapshot.values, live_values)
    return DriftResult(
        has_drift=not diff.is_empty,
        snapshot_exists=True,
        diff=diff,
        live_values=live_values,
    )


def make_snapshot(
    release: str,
    namespace: str,
    chart_version: str,
    values: dict[str, Any],
    operation: str,
) -> ValuesSnapshot:
    """Create a new :class:`ValuesSnapshot` with the current timestamp."""
    return ValuesSnapshot(
        release=release,
        namespace=namespace,
        timestamp=datetime.now(UTC).isoformat(),
        chart_version=chart_version,
        values=copy.deepcopy(values),
        operation=operation,
    )


def format_diff_table(diff: ValuesDiff) -> list[list[str]]:
    """Return rows suitable for :meth:`IlumConsole.table`."""
    rows: list[list[str]] = []
    for key, value in sorted(diff.added.items()):
        rows.append(["+ added", key, _SYM_DASH, str(value)])
    for key, (old, new) in sorted(diff.changed.items()):
        rows.append(["~ changed", key, str(old), str(new)])
    for key, value in sorted(diff.removed.items()):
        rows.append(["- removed", key, str(value), _SYM_DASH])
    return rows


# ---------------------------------------------------------------------------
# Backups
# ---------------------------------------------------------------------------

_MAX_BACKUPS = 10


def _backup_dir(state_dir: Path) -> Path:
    return state_dir / "backups"


def save_backup(
    release: str,
    namespace: str,
    values: dict[str, Any],
    state_dir: Path,
    context: str = "",
) -> Path:
    """Save a values backup before destructive operations.

    Returns the path to the created backup file. Caps at _MAX_BACKUPS per release.
    """
    ctx = _sanitize_context(context) if context else _sanitize_context(_get_current_context())
    bak_dir = _backup_dir(state_dir)
    bak_dir.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S")
    filename = f"{ctx}--{release}--{namespace}--{timestamp}.yaml"
    path = bak_dir / filename
    _YAML.dump({"values": values}, path)

    # Prune old backups
    _prune_backups(bak_dir, ctx, release, namespace)
    return path


def _prune_backups(bak_dir: Path, context: str, release: str, namespace: str) -> None:
    """Keep only the most recent _MAX_BACKUPS files per release."""
    prefix = f"{context}--{release}--{namespace}--"
    backups = sorted(
        [f for f in bak_dir.iterdir() if f.name.startswith(prefix) and f.suffix == ".yaml"],
        key=lambda f: f.name,
    )
    while len(backups) > _MAX_BACKUPS:
        oldest = backups.pop(0)
        oldest.unlink()


def list_backups(
    release: str,
    namespace: str,
    state_dir: Path,
    context: str = "",
) -> list[Path]:
    """Return backup paths sorted newest-first, capped at _MAX_BACKUPS."""
    ctx = _sanitize_context(context) if context else _sanitize_context(_get_current_context())
    bak_dir = _backup_dir(state_dir)
    if not bak_dir.exists():
        return []

    prefix = f"{ctx}--{release}--{namespace}--"
    backups = sorted(
        [f for f in bak_dir.iterdir() if f.name.startswith(prefix) and f.suffix == ".yaml"],
        key=lambda f: f.name,
        reverse=True,
    )
    return backups[:_MAX_BACKUPS]


def load_backup(path: Path) -> dict[str, Any]:
    """Load a backup file and return the values dict."""
    data = _YAML.load(path)
    return dict(data.get("values") or {}) if data else {}
