"""Network fault injection tools — net_partition, net_impair, net_heal, net_heal_all.

Registers MCP tools for host-side iptables-based network fault injection and
tc netem traffic impairment. All tool handlers are wrapped with
@structured_tool_handler (D-14). FaultRegistry persists active fault handles to
disk for startup reconciliation (D-03, D-04).
"""

from __future__ import annotations

import logging
import uuid

from vmcluster_mcp.cluster.libvirt_client import LibvirtClient
from vmcluster_mcp.models import ErrorDetail, NetFaultHandle, NetFaultInfo, ToolResult
from vmcluster_mcp.net.fault import (
    apply_impairment,
    apply_partition_rules,
    build_impairment_cmd,
    build_partition_rules,
    discover_tap_interface,
    remove_impairment,
    remove_rules_by_comment,
)
from vmcluster_mcp.net.fault_registry import FaultRegistry
from vmcluster_mcp.server import config, mcp, structured_tool_handler
from vmcluster_mcp.topology.parser import load_topology

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

_registry = FaultRegistry(config.fault_registry)
_registry.load()

_reconciled = False


async def _ensure_reconciled() -> None:
    global _reconciled
    if not _reconciled:
        await _registry.reconcile()
        _reconciled = True


# Lazily-connected libvirt client for tap interface discovery
_libvirt_client = LibvirtClient(config.libvirt_uri)


@mcp.tool()
@structured_tool_handler
async def net_partition(
    cluster_name: str,
    partition_a: list[str],
    partition_b: list[str],
) -> ToolResult[NetFaultHandle]:
    """Install symmetric iptables DROP rules between two node groups on the libvirt bridge.

    Every rule is tagged with ``vmcluster:<cluster_name>:<handle_id>`` (D-07) for
    reliable identification by ``net_heal``, ``net_heal_all``, and startup reconciliation.
    Nodes within the same partition group remain connected — only cross-partition
    traffic is dropped.

    Calling net_partition a second time with the same groups creates a new handle
    (not idempotent). Use net_heal or net_heal_all to remove active partitions.

    Args:
        cluster_name: Name of the topology (cluster) to partition.
        partition_a: List of node names forming the first partition group.
        partition_b: List of node names forming the second partition group.

    Returns:
        ToolResult[NetFaultHandle] with the new handle on success, or an error detail.
    """
    topo = load_topology(cluster_name, config.topology_dir)
    node_ip_map = {n.name: n.ip for n in topo.nodes}

    # Validate that all specified nodes exist in the topology
    all_nodes = partition_a + partition_b
    missing = [name for name in all_nodes if name not in node_ip_map]
    if missing:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="NODE_NOT_FOUND",
                message=f"Nodes not found in cluster '{cluster_name}': {', '.join(missing)}",
                recoverable=False,
            ),
        )

    ips_a = [node_ip_map[name] for name in partition_a]
    ips_b = [node_ip_map[name] for name in partition_b]

    handle_id = str(uuid.uuid4())
    rules = build_partition_rules(
        cluster_name, handle_id, topo.network.bridge, ips_a, ips_b
    )

    rule_identifiers = await apply_partition_rules(rules)

    handle = _registry.register(
        handle_id=handle_id,
        cluster_name=cluster_name,
        fault_type="partition",
        affected_nodes=all_nodes,
        parameters={
            "partition_a": partition_a,
            "partition_b": partition_b,
            "bridge": topo.network.bridge,
        },
        rule_identifiers={
            "comment_tag": rule_identifiers[0] if rule_identifiers else f"vmcluster:{cluster_name}:{handle_id}"
        },
    )

    logger.info(
        "net_partition: cluster=%s handle=%s bridge=%s partition_a=%s partition_b=%s",
        cluster_name, handle_id, topo.network.bridge, partition_a, partition_b,
    )

    return ToolResult(success=True, result=handle)


@mcp.tool()
@structured_tool_handler
async def net_impair(
    cluster_name: str,
    source_node: str,
    target_node: str = "all",
    latency_ms: int = 0,
    jitter_ms: int = 0,
    loss_pct: float = 0.0,
    corrupt_pct: float = 0.0,
    reorder_pct: float = 0.0,
) -> ToolResult[NetFaultHandle]:
    """Apply tc netem traffic degradation on a source node's egress tap interface.

    A single impairment handle may combine latency, jitter, loss, corruption, and
    reordering parameters. Zero-valued parameters are omitted from the effective tc
    command (D-09). The ``target_node`` parameter is stored in the registry as
    metadata; the tc rule targets the source node's entire egress tap.

    At least one impairment parameter must be non-zero. Use ``net_heal`` to remove
    a specific impairment by handle, or ``net_heal_all`` to clear the cluster.

    Args:
        cluster_name: Name of the topology (cluster) containing the source node.
        source_node: Node name whose egress tap receives the tc netem rule.
        target_node: Target node name or ``"all"`` (stored as metadata, default ``"all"``).
        latency_ms: Packet delay in milliseconds (0 = omit).
        jitter_ms: Delay variance in milliseconds; only effective when latency_ms > 0.
        loss_pct: Packet loss percentage (0.0 = omit).
        corrupt_pct: Packet corruption percentage (0.0 = omit).
        reorder_pct: Packet reorder percentage (0.0 = omit).

    Returns:
        ToolResult[NetFaultHandle] with the new handle on success, or an error detail.
    """
    if not (latency_ms > 0 or jitter_ms > 0 or loss_pct > 0 or corrupt_pct > 0 or reorder_pct > 0):
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="NO_PARAMETERS",
                message="At least one impairment parameter must be non-zero",
                recoverable=True,
            ),
        )

    topo = load_topology(cluster_name, config.topology_dir)
    node_names = [n.name for n in topo.nodes]

    if source_node not in node_names:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="NODE_NOT_FOUND",
                message=f"Node '{source_node}' not found in cluster '{cluster_name}'",
                recoverable=False,
            ),
        )

    tap_dev = await discover_tap_interface(
        _libvirt_client, cluster_name, source_node, topo.network.bridge
    )

    cmd = build_impairment_cmd(tap_dev, latency_ms, jitter_ms, loss_pct, corrupt_pct, reorder_pct)
    await apply_impairment(cmd)

    handle_id = str(uuid.uuid4())
    affected_nodes = [source_node] if target_node == "all" else [source_node, target_node]

    handle = _registry.register(
        handle_id=handle_id,
        cluster_name=cluster_name,
        fault_type="impairment",
        affected_nodes=affected_nodes,
        parameters={
            "source_node": source_node,
            "target_node": target_node,
            "latency_ms": latency_ms,
            "jitter_ms": jitter_ms,
            "loss_pct": loss_pct,
            "corrupt_pct": corrupt_pct,
            "reorder_pct": reorder_pct,
        },
        rule_identifiers={"interface": tap_dev},
    )

    logger.info(
        "net_impair: cluster=%s handle=%s source=%s tap=%s latency=%dms loss=%.1f%%",
        cluster_name, handle_id, source_node, tap_dev, latency_ms, loss_pct,
    )

    return ToolResult(success=True, result=handle)


@mcp.tool()
@structured_tool_handler
async def net_heal(
    cluster_name: str,
    fault_handle: str,
) -> ToolResult[bool]:
    """Remove all host-side state for a specific fault handle and deregister it.

    Dispatches to the appropriate cleanup routine based on the registry entry's
    ``fault_type``:

    - ``partition``: Calls ``remove_rules_by_comment`` with the stored comment tag.
    - ``impairment``: Calls ``remove_impairment`` with the stored tap device.

    Cleanup is considered successful even if the live host-side state is already
    absent (D-10). The registry entry is removed only after cleanup succeeds or
    is confirmed absent.

    Args:
        cluster_name: Name of the topology (cluster) that owns the fault.
        fault_handle: Handle ID returned by ``net_partition`` or ``net_impair``.

    Returns:
        ToolResult[bool] with result=True on success, or HANDLE_NOT_FOUND error.
    """
    entry = _registry.get(fault_handle)
    if entry is None:
        return ToolResult(
            success=False,
            error=ErrorDetail(
                code="HANDLE_NOT_FOUND",
                message=f"No active fault with handle '{fault_handle}'",
                recoverable=True,
            ),
        )

    fault_type = entry["fault_type"]
    rule_ids = entry.get("rule_identifiers", {})

    if fault_type == "partition":
        comment_tag = rule_ids.get(
            "comment_tag", f"vmcluster:{cluster_name}:{fault_handle}"
        )
        await remove_rules_by_comment(comment_tag)
    elif fault_type == "impairment":
        tap_dev = rule_ids.get("interface", "")
        await remove_impairment(tap_dev)

    _registry.remove(fault_handle)

    logger.info(
        "net_heal: cluster=%s handle=%s type=%s", cluster_name, fault_handle, fault_type
    )

    return ToolResult(success=True, result=True)


@mcp.tool()
@structured_tool_handler
async def net_heal_all(
    cluster_name: str,
) -> ToolResult[int]:
    """Remove all active faults for a cluster and return the count of handles removed.

    Iterates all registry entries for the given cluster. Each handle is cleaned up
    using the same dispatch logic as ``net_heal``. Best-effort semantics (D-11):
    a failure on one handle is logged but does not abort processing of remaining handles.

    Args:
        cluster_name: Name of the topology (cluster) to sweep.

    Returns:
        ToolResult[int] with the count of handles successfully removed.
    """
    faults = _registry.list_faults(cluster_name)
    removed_count = 0

    for fault_info in faults:
        handle_id = fault_info.handle_id
        try:
            entry = _registry.get(handle_id)
            if entry is None:
                continue

            fault_type = entry["fault_type"]
            rule_ids = entry.get("rule_identifiers", {})

            if fault_type == "partition":
                comment_tag = rule_ids.get(
                    "comment_tag", f"vmcluster:{cluster_name}:{handle_id}"
                )
                await remove_rules_by_comment(comment_tag)
            elif fault_type == "impairment":
                tap_dev = rule_ids.get("interface", "")
                await remove_impairment(tap_dev)

            _registry.remove(handle_id)
            removed_count += 1

        except Exception:
            logger.exception(
                "net_heal_all: failed to remove handle %s in cluster %s, continuing",
                handle_id, cluster_name,
            )

    logger.info(
        "net_heal_all: cluster=%s removed=%d total=%d",
        cluster_name, removed_count, len(faults),
    )

    return ToolResult(success=True, result=removed_count)


@mcp.tool()
@structured_tool_handler
async def net_fault_list(
    cluster_name: str,
) -> ToolResult[list[NetFaultInfo]]:
    """Return all active fault entries for a cluster from the registry.

    Provides visibility into the active fault state for a cluster, completing the
    inject→list→heal cycle. Returns every fault registered under cluster_name in
    the fault registry, including partition and impairment faults with their
    parameters and handle IDs.

    Args:
        cluster_name: Name of the cluster to list active faults for.

    Returns:
        ToolResult[list[NetFaultInfo]] with all active faults (empty list if none).
    """
    await _ensure_reconciled()
    faults = _registry.list_faults(cluster_name)
    return ToolResult(success=True, result=faults)

