"""Fault registry: disk-backed, handle-keyed registry of active network faults.

Mirrors the atomic persistence pattern established by ArtifactRegistry (Phase 5).
All writes use write-to-.tmp then os.replace() per D-03.
"""

from __future__ import annotations

import asyncio
import json
import logging
import os
from datetime import UTC, datetime

from vmcluster_mcp.models import NetFaultHandle, NetFaultInfo

logger = logging.getLogger("vmcluster_mcp.net")


async def _run_host_cmd(cmd: list[str]) -> tuple[int, str]:
    """Run a host command, return (exit_code, stdout)."""
    proc = await asyncio.create_subprocess_exec(
        *cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )
    stdout, _ = await proc.communicate()
    return proc.returncode, stdout.decode(errors="replace")


class FaultRegistry:
    """Disk-backed registry of active network faults with atomic JSON persistence."""

    def __init__(self, registry_path: str) -> None:
        self._path = registry_path
        self._registry: dict[str, dict] = {}

    def load(self) -> None:
        """Load registry from disk; initializes to empty if file is absent."""
        if os.path.isfile(self._path):
            with open(self._path) as f:
                self._registry = json.load(f)
        else:
            self._registry = {}

    def save(self) -> None:
        """Atomically write registry to disk (write-to-.tmp then os.replace)."""
        os.makedirs(os.path.dirname(os.path.abspath(self._path)), exist_ok=True)
        tmp_path = f"{self._path}.tmp"
        with open(tmp_path, "w") as f:
            json.dump(self._registry, f, indent=2)
        os.replace(tmp_path, self._path)

    def register(
        self,
        handle_id: str,
        cluster_name: str,
        fault_type: str,
        affected_nodes: list[str],
        parameters: dict,
        rule_identifiers: dict,
    ) -> NetFaultHandle:
        """Store a new fault entry keyed by handle_id and save immediately."""
        created_at = datetime.now(UTC).isoformat()
        self._registry[handle_id] = {
            "cluster_name": cluster_name,
            "fault_type": fault_type,
            "affected_nodes": affected_nodes,
            "created_at": created_at,
            "parameters": parameters,
            "rule_identifiers": rule_identifiers,
        }
        self.save()
        return NetFaultHandle(
            handle_id=handle_id,
            fault_type=fault_type,  # type: ignore[arg-type]
            affected_nodes=affected_nodes,
            created_at=created_at,
        )

    def get(self, handle_id: str) -> dict | None:
        """Retrieve a fault entry by handle_id; returns None if not found."""
        return self._registry.get(handle_id)

    def remove(self, handle_id: str) -> bool:
        """Delete a fault entry by handle_id; returns True if removed, False if not found."""
        if handle_id not in self._registry:
            return False
        del self._registry[handle_id]
        self.save()
        return True

    def list_faults(self, cluster_name: str) -> list[NetFaultInfo]:
        """Return all active fault entries for the given cluster."""
        result = []
        for handle_id, entry in self._registry.items():
            if entry["cluster_name"] == cluster_name:
                result.append(
                    NetFaultInfo(
                        handle_id=handle_id,
                        fault_type=entry["fault_type"],  # type: ignore[arg-type]
                        affected_nodes=entry["affected_nodes"],
                        created_at=entry["created_at"],
                        parameters=entry["parameters"],
                    )
                )
        return result

    def all_handles(self) -> list[str]:
        """Return all handle_ids (used for reconciliation iteration)."""
        return list(self._registry.keys())

    async def reconcile(self) -> int:
        """Validate persisted entries against live kernel state; prune stale entries.

        For partition faults: checks iptables FORWARD chain for the comment tag
        ``vmcluster:<cluster>:<handle_id>`` per D-07.
        For impairment faults: checks ``tc qdisc show dev <interface>`` for netem.

        Returns the count of stale entries pruned.
        """
        if not self._registry:
            return 0

        stale: list[str] = []
        for handle_id, entry in list(self._registry.items()):
            fault_type = entry["fault_type"]
            rule_ids = entry.get("rule_identifiers", {})
            cluster_name = entry["cluster_name"]

            if fault_type == "partition":
                comment_tag = rule_ids.get("comment_tag", f"vmcluster:{cluster_name}:{handle_id}")
                _, stdout = await _run_host_cmd(
                    ["iptables", "-L", "FORWARD", "-n", "--line-numbers", "-v"]
                )
                if comment_tag not in stdout:
                    logger.warning(
                        "Stale partition entry %s: rule tag %r not found in iptables FORWARD",
                        handle_id,
                        comment_tag,
                    )
                    stale.append(handle_id)

            elif fault_type == "impairment":
                interface = rule_ids.get("interface", "")
                _, stdout = await _run_host_cmd(["tc", "qdisc", "show", "dev", interface])
                if "netem" not in stdout:
                    logger.warning(
                        "Stale impairment entry %s: netem qdisc not found on interface %s",
                        handle_id,
                        interface,
                    )
                    stale.append(handle_id)

        for handle_id in stale:
            del self._registry[handle_id]

        if stale:
            self.save()

        return len(stale)
