"""``configurator/reader`` — read a VM's hardware config from domain XML.

:func:`read_config` walks an *already-parsed* libvirt domain tree and
produces a :class:`~mgf.vm.core.configurator.types.VMConfig` — the one
aggregate the CLI renders for ``vmanager vm config show`` and the GUI
shows in its detail/config views.

**Pure core (boundary decided 2026-05-29).** This module imports stdlib
:mod:`xml.etree.ElementTree` for the ``Element`` *type* only and never
parses a byte stream. The umbrella's original ``read_config`` took a
``(provider, handle)`` pair and called ``provider.domain_xml(handle)``
itself, then parsed the result with ``defusedxml`` via
``parse_domain_xml``. Both the FETCH (engine I/O) and the PARSE
(``defusedxml``, forbidden in the pure core) are adapter concerns: the
adapter reads the live domain XML from the provider, parses it with
``defusedxml`` into a standard :class:`xml.etree.ElementTree.Element`, and
injects that ``Element`` here. Core only does Element -> VMConfig.

``name`` and ``uuid`` default to ``""`` when absent from the tree; the
umbrella backfilled them from the handle, which is no longer available
once the fetch is a caller concern. Callers that need the handle's
name/uuid as a fallback can substitute them on the returned config.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from mgf.vm.core.configurator.types import (
    CPUConfig,
    DiskConfig,
    DisplayConfig,
    FirmwareConfig,
    MemoryConfig,
    Metadata,
    NICConfig,
    PCIDevice,
    ShareConfig,
    USBDevice,
    VMConfig,
)

if TYPE_CHECKING:
    import xml.etree.ElementTree as ET


# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------


def read_config(element: ET.Element) -> VMConfig:
    """Return a structured read-back of a domain's hardware config.

    ``element`` is the parsed domain root element (injected by the
    adapter). This function is pure over the already-parsed tree: it
    never parses a string and never touches a provider/engine.
    """
    return VMConfig(
        name=_text(element, "name", default=""),
        uuid=_text(element, "uuid", default=""),
        cpu=_read_cpu(element),
        memory=_read_memory(element),
        firmware=_read_firmware(element),
        metadata=_read_metadata(element),
        display=_read_display(element),
        disks=tuple(_read_disks(element)),
        nics=tuple(_read_nics(element)),
        shares=tuple(_read_shares(element)),
        pci_devices=tuple(_read_pci(element)),
        usb_devices=tuple(_read_usb(element)),
    )


# ---------------------------------------------------------------------------
# Per-subsystem walkers
# ---------------------------------------------------------------------------


def _read_cpu(root: ET.Element) -> CPUConfig:
    vcpu_el = root.find("vcpu")
    if vcpu_el is None or not (vcpu_el.text or "").strip():
        vcpus_max = 1
    else:
        vcpus_max = int(vcpu_el.text)  # type: ignore[arg-type]
    current_attr = vcpu_el.get("current") if vcpu_el is not None else None
    vcpus = int(current_attr) if current_attr else vcpus_max
    model_el = root.find("cpu/model")
    model = (model_el.text or None) if model_el is not None else None
    return CPUConfig(vcpus=vcpus, vcpus_max=vcpus_max, model=model)


def _read_memory(root: ET.Element) -> MemoryConfig:
    mem_el = root.find("memory")
    cur_el = root.find("currentMemory")
    max_mb = _kib_element_to_mb(mem_el)
    current_mb = _kib_element_to_mb(cur_el) if cur_el is not None else max_mb
    hugepages = root.find("memoryBacking/hugepages") is not None
    return MemoryConfig(
        current_mb=current_mb,
        max_mb=max_mb,
        hugepages=hugepages,
    )


def _read_firmware(root: ET.Element) -> FirmwareConfig:
    os_el = root.find("os")
    if os_el is None:
        return FirmwareConfig(type="bios")
    loader = os_el.find("loader")
    fw_type = "uefi" if loader is not None and "OVMF" in (loader.text or "") else "bios"
    secure = False
    if loader is not None and loader.get("secure") == "yes":
        secure = True
    return FirmwareConfig(type=fw_type, secure_boot=secure)


def _read_metadata(root: ET.Element) -> Metadata:
    title = _text(root, "title", default="") or None
    description = _text(root, "description", default="") or None
    return Metadata(title=title, description=description)


def _read_display(root: ET.Element) -> DisplayConfig:
    graphics = root.find("devices/graphics")
    if graphics is None:
        return DisplayConfig(type="none")
    listen_el = graphics.find("listen")
    listen_addr = listen_el.get("address") if listen_el is not None else graphics.get("listen")
    port_attr = graphics.get("port")
    port: int | None = None
    if port_attr and port_attr != "-1":
        try:
            port = int(port_attr)
        except ValueError:
            port = None
    return DisplayConfig(
        type=graphics.get("type") or "none",
        listen=listen_addr,
        port=port,
    )


def _read_disks(root: ET.Element) -> list[DiskConfig]:
    out: list[DiskConfig] = []
    for disk_el in root.findall("devices/disk"):
        if disk_el.get("device") not in (None, "disk"):
            # cdrom / floppy are skipped — Phase 4 treats the CD
            # drive as part of firmware/boot, not storage.
            continue
        source_el = disk_el.find("source")
        target_el = disk_el.find("target")
        driver_el = disk_el.find("driver")
        source_path = ""
        if source_el is not None:
            source_path = source_el.get("file") or source_el.get("dev") or ""
        out.append(
            DiskConfig(
                target=(target_el.get("dev") if target_el is not None else "") or "",
                source=source_path,
                bus=(target_el.get("bus") if target_el is not None else "") or "virtio",
                format=(driver_el.get("type") if driver_el is not None else "") or "qcow2",
                readonly=disk_el.find("readonly") is not None,
            )
        )
    return out


def _read_nics(root: ET.Element) -> list[NICConfig]:
    out: list[NICConfig] = []
    for iface in root.findall("devices/interface"):
        mac_el = iface.find("mac")
        model_el = iface.find("model")
        source_el = iface.find("source")
        network = None
        bridge = None
        if source_el is not None:
            network = source_el.get("network")
            bridge = source_el.get("bridge")
        out.append(
            NICConfig(
                mac=(mac_el.get("address") if mac_el is not None else "") or "",
                model=(model_el.get("type") if model_el is not None else "") or "virtio",
                network=network,
                bridge=bridge,
            )
        )
    return out


def _read_shares(root: ET.Element) -> list[ShareConfig]:
    out: list[ShareConfig] = []
    for fs_el in root.findall("devices/filesystem"):
        driver_el = fs_el.find("driver")
        source_el = fs_el.find("source")
        target_el = fs_el.find("target")
        out.append(
            ShareConfig(
                driver=(driver_el.get("type") if driver_el is not None else "") or "virtiofs",
                source=(source_el.get("dir") if source_el is not None else "") or "",
                target=(target_el.get("dir") if target_el is not None else "") or "",
                readonly=fs_el.find("readonly") is not None,
            )
        )
    return out


def _read_pci(root: ET.Element) -> list[PCIDevice]:
    out: list[PCIDevice] = []
    for hd in root.findall("devices/hostdev"):
        if hd.get("type") != "pci":
            continue
        addr = hd.find("source/address")
        if addr is None:
            continue
        out.append(
            PCIDevice(
                domain=_hex_attr(addr, "domain"),
                bus=_hex_attr(addr, "bus"),
                slot=_hex_attr(addr, "slot"),
                function=_hex_attr(addr, "function"),
            )
        )
    return out


def _read_usb(root: ET.Element) -> list[USBDevice]:
    out: list[USBDevice] = []
    for hd in root.findall("devices/hostdev"):
        if hd.get("type") != "usb":
            continue
        vendor = hd.find("source/vendor")
        product = hd.find("source/product")
        if vendor is None or product is None:
            continue
        out.append(
            USBDevice(
                vendor=_hex_attr(vendor, "id"),
                product=_hex_attr(product, "id"),
            )
        )
    return out


# ---------------------------------------------------------------------------
# Small helpers
# ---------------------------------------------------------------------------


def _text(root: ET.Element, path: str, *, default: str) -> str:
    el = root.find(path)
    if el is None:
        return default
    return (el.text or "").strip() or default


def _kib_element_to_mb(el: ET.Element | None) -> int:
    if el is None or not (el.text or "").strip():
        return 0
    value = int(el.text)  # type: ignore[arg-type]
    unit = (el.get("unit") or "KiB").lower()
    if unit == "kib":
        return value // 1024
    if unit == "mib":
        return value
    if unit == "gib":
        return value * 1024
    if unit == "bytes":
        return value // (1024 * 1024)
    return value // 1024  # default assumption: KiB


def _hex_attr(el: ET.Element, name: str) -> int:
    raw = el.get(name) or "0"
    try:
        return int(raw, 16)
    except ValueError:
        try:
            return int(raw)
        except ValueError:
            return 0


__all__ = ["read_config"]
