"""Fault injection helpers: iptables partition rules and tc netem impairment.

Pattern established here is reused by net_heal (handle-based cleanup) and
net_heal_all (cluster-wide sweep). Every rule comment tag follows D-07:
  vmcluster:<cluster_name>:<handle_id>
"""

from __future__ import annotations

import logging
from xml.etree import ElementTree as ET

from vmcluster_mcp.cluster.libvirt_client import domain_name as _domain_name
from vmcluster_mcp.net.fault_registry import _run_host_cmd

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


def build_partition_rules(
    cluster_name: str,
    handle_id: str,
    bridge: str,
    ips_a: list[str],
    ips_b: list[str],
) -> list[list[str]]:
    """Build symmetric iptables DROP rule commands for a network partition.

    For every (src, dst) pair where src ∈ ips_a and dst ∈ ips_b, and the
    corresponding reverse pair (src ∈ ips_b, dst ∈ ips_a), generates an
    iptables FORWARD chain insert command targeting the given bridge interface.

    Every rule carries the comment tag ``vmcluster:<cluster_name>:<handle_id>``
    per D-07, enabling reliable cleanup and reconciliation without depending on
    positional rule numbers.

    Args:
        cluster_name: Name of the cluster (embedded in comment tag).
        handle_id: Unique fault handle ID (embedded in comment tag).
        bridge: Name of the libvirt bridge interface (e.g. ``virbr0``).
        ips_a: IP addresses of nodes in partition group A.
        ips_b: IP addresses of nodes in partition group B.

    Returns:
        List of iptables command argument lists, ready for ``_run_host_cmd``.
    """
    comment_tag = f"vmcluster:{cluster_name}:{handle_id}"
    rules: list[list[str]] = []

    # Build symmetric pairs: A→B and B→A
    for src_group, dst_group in [(ips_a, ips_b), (ips_b, ips_a)]:
        for src in src_group:
            for dst in dst_group:
                rules.append([
                    "iptables", "-I", "FORWARD",
                    "-i", bridge, "-o", bridge,
                    "-s", src, "-d", dst,
                    "-j", "DROP",
                    "-m", "comment", "--comment", comment_tag,
                ])

    return rules


async def apply_partition_rules(rules: list[list[str]]) -> list[str]:
    """Apply iptables rules via host subprocess and return comment tag identifiers.

    Executes each rule command via ``_run_host_cmd``. If any command returns a
    non-zero exit code, raises ``RuntimeError`` with the offending command.

    Args:
        rules: List of iptables command argument lists as produced by
            ``build_partition_rules``.

    Returns:
        Deduplicated list of comment tags extracted from the applied rules.
        These serve as ``rule_identifiers`` stored in the ``FaultRegistry``.

    Raises:
        RuntimeError: If any iptables command fails.
    """
    tags_seen: list[str] = []

    for rule in rules:
        exit_code, output = await _run_host_cmd(rule)
        if exit_code != 0:
            logger.error("iptables command failed (exit %d): %s — %s", exit_code, rule, output)
            raise RuntimeError(f"iptables command failed: {' '.join(rule)}")

        # Extract comment tag from the rule for registry storage
        if "--comment" in rule:
            tag = rule[rule.index("--comment") + 1]
            if tag not in tags_seen:
                tags_seen.append(tag)

    return tags_seen


async def remove_rules_by_comment(comment_tag: str) -> int:
    """Remove all FORWARD chain rules whose comment matches *comment_tag*.

    Lists the FORWARD chain, identifies matching line numbers, then deletes
    them in descending order to avoid index shifting.

    Args:
        comment_tag: The exact comment string to match
            (e.g. ``vmcluster:mycluster:abc123``).

    Returns:
        Count of rules successfully removed. Returns 0 if no rules matched.
    """
    exit_code, output = await _run_host_cmd(
        ["iptables", "-L", "FORWARD", "-n", "--line-numbers", "-v"]
    )
    if exit_code != 0:
        logger.warning("iptables list failed (exit %d): %s", exit_code, output)
        return 0

    # Find line numbers of all rules whose text includes the comment tag
    matching_line_nums: list[int] = []
    for line in output.splitlines():
        if comment_tag in line:
            parts = line.split()
            if parts:
                try:
                    line_num = int(parts[0])
                    matching_line_nums.append(line_num)
                except ValueError:
                    pass

    if not matching_line_nums:
        return 0

    # Delete in reverse order to avoid line-number shifting
    removed = 0
    for line_num in sorted(matching_line_nums, reverse=True):
        del_exit, del_out = await _run_host_cmd(
            ["iptables", "-D", "FORWARD", str(line_num)]
        )
        if del_exit == 0:
            removed += 1
        else:
            logger.warning(
                "Failed to delete FORWARD rule %d for tag %r: %s",
                line_num, comment_tag, del_out,
            )

    return removed


# ---------------------------------------------------------------------------
# tc netem helpers (impairment)
# ---------------------------------------------------------------------------


async def discover_tap_interface(
    libvirt_client,
    cluster_name: str,
    node_name: str,
    bridge: str,
) -> str:
    """Parse domain XML to find the host tap device for a given bridge interface.

    Looks up the libvirt domain for ``{cluster_name}_{node_name}``, retrieves its
    XML descriptor, and searches for a ``<interface type='bridge'>`` element whose
    ``<source bridge='{bridge}'/>`` matches the requested bridge. Returns the tap
    device name from the corresponding ``<target dev='...'/>`` element.

    Args:
        libvirt_client: An instance of LibvirtClient.
        cluster_name: Cluster identifier (used to form the domain name).
        node_name: Node identifier (used to form the domain name).
        bridge: Name of the libvirt bridge interface to locate (e.g. ``virbr0``).

    Returns:
        Tap device name (e.g. ``vnet3``).

    Raises:
        RuntimeError: If the domain is not found or the bridge has no tap device.
    """
    dom_name = _domain_name(cluster_name, node_name)
    dom = await libvirt_client.lookup_domain(dom_name)
    if dom is None:
        raise RuntimeError(f"Domain '{dom_name}' not found")

    xml_str = await libvirt_client.get_domain_xml(dom)
    root = ET.fromstring(xml_str)

    for iface in root.findall(".//devices/interface[@type='bridge']"):
        source = iface.find("source")
        if source is not None and source.get("bridge") == bridge:
            target = iface.find("target")
            if target is not None:
                dev = target.get("dev")
                if dev:
                    return dev

    raise RuntimeError(
        f"No tap device found for bridge '{bridge}' in domain '{dom_name}'"
    )


def build_impairment_cmd(
    tap_dev: str,
    latency_ms: int = 0,
    jitter_ms: int = 0,
    loss_pct: float = 0.0,
    corrupt_pct: float = 0.0,
    reorder_pct: float = 0.0,
) -> list[str]:
    """Build a tc netem qdisc add command with the specified impairment parameters.

    Zero-valued parameters are omitted from the command per D-09 — this prevents
    no-op tokens like ``delay 0ms`` from appearing even when a parameter is
    explicitly passed as zero.

    Args:
        tap_dev: Host tap device name (e.g. ``vnet3``).
        latency_ms: Packet delay in milliseconds.
        jitter_ms: Delay variance in milliseconds (only appended if latency_ms > 0).
        loss_pct: Packet loss percentage (e.g. 5.0 for 5%).
        corrupt_pct: Packet corruption percentage.
        reorder_pct: Packet reorder percentage.

    Returns:
        Command argument list ready for ``apply_impairment`` / ``_run_host_cmd``.
    """
    cmd: list[str] = ["tc", "qdisc", "add", "dev", tap_dev, "root", "netem"]

    if latency_ms > 0:
        cmd += ["delay", f"{latency_ms}ms"]
        if jitter_ms > 0:
            cmd += [f"{jitter_ms}ms"]

    if loss_pct > 0:
        cmd += ["loss", f"{loss_pct}%"]

    if corrupt_pct > 0:
        cmd += ["corrupt", f"{corrupt_pct}%"]

    if reorder_pct > 0:
        cmd += ["reorder", f"{reorder_pct}%"]

    return cmd


async def apply_impairment(cmd: list[str]) -> None:
    """Apply a tc netem qdisc add command via host subprocess.

    Args:
        cmd: Command argument list as returned by ``build_impairment_cmd``.

    Raises:
        RuntimeError: If the tc command exits with a non-zero status.
    """
    exit_code, output = await _run_host_cmd(cmd)
    if exit_code != 0:
        logger.error("tc command failed (exit %d): %s — %s", exit_code, cmd, output)
        raise RuntimeError(f"tc command failed: {' '.join(cmd)}")


async def remove_impairment(tap_dev: str) -> bool:
    """Remove the netem qdisc from the root of a tap device.

    Args:
        tap_dev: Host tap device name (e.g. ``vnet3``).

    Returns:
        True if the qdisc was removed, False if it was already absent.
    """
    exit_code, _ = await _run_host_cmd(
        ["tc", "qdisc", "del", "dev", tap_dev, "root"]
    )
    return exit_code == 0

