"""``configurator/passthrough`` — PCI and USB host-device passthrough (pure planners).

* **PCI**: ``<hostdev mode='subsystem' type='pci'>`` with a
  ``<source><address>`` child. PCI passthrough binds the host device to
  vfio-pci at VM start time, so every PCI add/remove requires a shutdown.
* **USB**: ``<hostdev mode='subsystem' type='usb'>`` with a
  ``<source><vendor/><product/>`` child. USB hotplug is well-supported by
  libvirt; the flag is left ``requires_shutdown=False`` so USB keys can be
  attached at runtime.

**Pure core.** Each planner accepts an *already-parsed*
:class:`xml.etree.ElementTree.Element`; the ``apply_*`` halves stay in
the adapter (``mgf-vm``).
"""

from __future__ import annotations

import xml.etree.ElementTree as ET

from mgf.vm.core.configurator.types import ConfigChange
from mgf.vm.core.configurator.xml_helpers import ensure, serialize_domain_xml
from mgf.vm.core.exceptions import SpecValidationError

SUBSYSTEM = "passthrough"


def plan_add_pci(
    domain: ET.Element,
    *,
    domain_addr: int,
    bus: int,
    slot: int,
    function: int,
) -> ConfigChange:
    """Attach a PCI passthrough host device to the injected tree.

    ``domain_addr`` is the PCI address *domain* component (renamed from
    the source's ``domain`` arg to avoid shadowing the injected ``domain``
    element parameter).
    """
    _validate_pci(domain_addr, bus, slot, function)
    old_xml = serialize_domain_xml(domain)
    devices = ensure(domain, "devices")

    for hd in devices.findall("hostdev"):
        if hd.get("type") != "pci":
            continue
        addr = hd.find("source/address")
        if addr is None:
            continue
        if (
            _hex(addr.get("domain")) == domain_addr
            and _hex(addr.get("bus")) == bus
            and _hex(addr.get("slot")) == slot
            and _hex(addr.get("function")) == function
        ):
            raise SpecValidationError(
                [f"PCI device {domain_addr}:{bus}:{slot}.{function} already attached"]
            )

    hostdev = ET.SubElement(
        devices,
        "hostdev",
        {"mode": "subsystem", "type": "pci", "managed": "yes"},
    )
    source = ET.SubElement(hostdev, "source")
    ET.SubElement(
        source,
        "address",
        {
            "domain": f"0x{domain_addr:04x}",
            "bus": f"0x{bus:02x}",
            "slot": f"0x{slot:02x}",
            "function": f"0x{function:x}",
        },
    )

    return ConfigChange(
        subsystem=SUBSYSTEM,
        summary=f"pci {domain_addr:04x}:{bus:02x}:{slot:02x}.{function} attached",
        old_xml=old_xml,
        new_xml=serialize_domain_xml(domain),
        requires_shutdown=True,
    )


def plan_remove_pci(
    domain: ET.Element,
    *,
    domain_addr: int,
    bus: int,
    slot: int,
    function: int,
) -> ConfigChange:
    """Detach a PCI passthrough host device from the injected tree."""
    _validate_pci(domain_addr, bus, slot, function)
    old_xml = serialize_domain_xml(domain)
    devices = domain.find("devices")
    if devices is None:
        raise SpecValidationError(["domain has no <devices> element"])

    for hd in list(devices.findall("hostdev")):
        if hd.get("type") != "pci":
            continue
        addr = hd.find("source/address")
        if addr is None:
            continue
        if (
            _hex(addr.get("domain")) == domain_addr
            and _hex(addr.get("bus")) == bus
            and _hex(addr.get("slot")) == slot
            and _hex(addr.get("function")) == function
        ):
            devices.remove(hd)
            return ConfigChange(
                subsystem=SUBSYSTEM,
                summary=f"pci {domain_addr:04x}:{bus:02x}:{slot:02x}.{function} detached",
                old_xml=old_xml,
                new_xml=serialize_domain_xml(domain),
                requires_shutdown=True,
            )

    raise SpecValidationError([f"PCI device {domain_addr}:{bus}:{slot}.{function} not attached"])


def plan_add_usb(
    domain: ET.Element,
    *,
    vendor: int,
    product: int,
) -> ConfigChange:
    """Attach a USB passthrough host device to the injected tree."""
    _validate_usb(vendor, product)
    old_xml = serialize_domain_xml(domain)
    devices = ensure(domain, "devices")

    hostdev = ET.SubElement(
        devices,
        "hostdev",
        {"mode": "subsystem", "type": "usb"},
    )
    source = ET.SubElement(hostdev, "source")
    ET.SubElement(source, "vendor", {"id": f"0x{vendor:04x}"})
    ET.SubElement(source, "product", {"id": f"0x{product:04x}"})

    return ConfigChange(
        subsystem=SUBSYSTEM,
        summary=f"usb {vendor:04x}:{product:04x} attached",
        old_xml=old_xml,
        new_xml=serialize_domain_xml(domain),
        requires_shutdown=False,
    )


def plan_remove_usb(
    domain: ET.Element,
    *,
    vendor: int,
    product: int,
) -> ConfigChange:
    """Detach a USB passthrough host device from the injected tree."""
    _validate_usb(vendor, product)
    old_xml = serialize_domain_xml(domain)
    devices = domain.find("devices")
    if devices is None:
        raise SpecValidationError(["domain has no <devices> element"])

    for hd in list(devices.findall("hostdev")):
        if hd.get("type") != "usb":
            continue
        v = hd.find("source/vendor")
        p = hd.find("source/product")
        if v is None or p is None:
            continue
        if _hex(v.get("id")) == vendor and _hex(p.get("id")) == product:
            devices.remove(hd)
            return ConfigChange(
                subsystem=SUBSYSTEM,
                summary=f"usb {vendor:04x}:{product:04x} detached",
                old_xml=old_xml,
                new_xml=serialize_domain_xml(domain),
                requires_shutdown=False,
            )

    raise SpecValidationError([f"USB device {vendor:04x}:{product:04x} not attached"])


def _validate_pci(domain_addr: int, bus: int, slot: int, function: int) -> None:
    if not (0 <= domain_addr <= 0xFFFF):
        raise SpecValidationError([f"pci domain {domain_addr} out of range"])
    if not (0 <= bus <= 0xFF):
        raise SpecValidationError([f"pci bus {bus} out of range"])
    if not (0 <= slot <= 0x1F):
        raise SpecValidationError([f"pci slot {slot} out of range"])
    if not (0 <= function <= 0x7):
        raise SpecValidationError([f"pci function {function} out of range"])


def _validate_usb(vendor: int, product: int) -> None:
    if not (0 < vendor <= 0xFFFF):
        raise SpecValidationError([f"usb vendor {vendor} out of range"])
    if not (0 < product <= 0xFFFF):
        raise SpecValidationError([f"usb product {product} out of range"])


def _hex(value: str | None) -> int:
    if value is None:
        return 0
    try:
        return int(value, 16)
    except ValueError:
        try:
            return int(value)
        except ValueError:
            return 0


__all__ = [
    "SUBSYSTEM",
    "plan_add_pci",
    "plan_add_usb",
    "plan_remove_pci",
    "plan_remove_usb",
]
