"""``configurator/storage`` — disk ``<disk>`` element edits (pure planners).

XML side only. The actual disk image files are created, grown, and
deleted by the adapter's disk operations; this module just writes the
matching ``<disk>`` element onto the domain.

Three planners:

* :func:`plan_add` — attach a new ``<disk>`` pointing at an existing
  file. Hotpluggable for virtio disks; other buses typically require a
  reboot. The plan marks every add as hotpluggable and lets the provider
  raise ``HotplugNotSupportedError`` if the bus refuses.
* :func:`plan_remove` — drop the ``<disk>`` whose ``target dev`` matches
  ``target``. Never touches the file.
* :func:`plan_resize` — record a disk-file resize in the audit trail; the
  XML itself does not carry the disk size.

**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 = "storage"


def plan_add(
    domain: ET.Element,
    *,
    target: str,
    source: str,
    bus: str = "virtio",
    fmt: str = "qcow2",
    readonly: bool = False,
) -> ConfigChange:
    """Attach a new disk element to the injected domain tree.

    Raises :class:`SpecValidationError` if a disk with the same
    ``target dev`` already exists on the domain — libvirt would refuse
    the redefine and the error message is cleaner coming from here.
    """
    _validate_target(target)
    old_xml = serialize_domain_xml(domain)
    devices = ensure(domain, "devices")

    existing = [d for d in devices.findall("disk") if _disk_target(d) == target]
    if existing:
        raise SpecValidationError([f"disk with target {target!r} already exists on domain"])

    disk = ET.SubElement(devices, "disk")
    disk.set("type", "file")
    disk.set("device", "disk")
    ET.SubElement(disk, "driver", {"name": "qemu", "type": fmt})
    ET.SubElement(disk, "source", {"file": source})
    ET.SubElement(disk, "target", {"dev": target, "bus": bus})
    if readonly:
        ET.SubElement(disk, "readonly")

    return ConfigChange(
        subsystem=SUBSYSTEM,
        summary=f"disk {target} added ({source})",
        old_xml=old_xml,
        new_xml=serialize_domain_xml(domain),
        requires_shutdown=False,
    )


def plan_remove(domain: ET.Element, *, target: str) -> ConfigChange:
    """Drop the ``<disk>`` with ``target dev == target`` from the tree."""
    _validate_target(target)
    old_xml = serialize_domain_xml(domain)
    devices = domain.find("devices")
    if devices is None:
        raise SpecValidationError(["domain has no <devices> element"])

    for disk in list(devices.findall("disk")):
        if _disk_target(disk) == target:
            devices.remove(disk)
            return ConfigChange(
                subsystem=SUBSYSTEM,
                summary=f"disk {target} removed",
                old_xml=old_xml,
                new_xml=serialize_domain_xml(domain),
                requires_shutdown=False,
            )

    raise SpecValidationError([f"no disk with target {target!r}"])


def plan_resize(domain: ET.Element, *, target: str) -> ConfigChange:
    """Record a disk-file resize in the audit trail.

    The XML itself does not carry the disk size — libvirt reads that from
    the file at domain-start time. This planner is a no-op in terms of
    XML bytes and exists purely so that the resize command produces a
    single audit entry via the normal plan/apply flow.
    """
    _validate_target(target)
    old_xml = serialize_domain_xml(domain)
    devices = domain.find("devices")
    if devices is None:
        raise SpecValidationError(["domain has no <devices> element"])
    disks = [d for d in devices.findall("disk") if _disk_target(d) == target]
    if not disks:
        raise SpecValidationError([f"no disk with target {target!r}"])

    return ConfigChange(
        subsystem=SUBSYSTEM,
        summary=f"disk {target} resized",
        old_xml=old_xml,
        new_xml=serialize_domain_xml(domain),
        requires_shutdown=False,
    )


def _disk_target(disk: ET.Element) -> str | None:
    target = disk.find("target")
    if target is None:
        return None
    return target.get("dev")


def _validate_target(target: str) -> None:
    # libvirt expects vda..vdz / sda..sdz style identifiers; be
    # permissive but refuse obviously invalid values like the
    # empty string or whitespace.
    if not target or not target.strip() or " " in target:
        raise SpecValidationError([f"invalid disk target {target!r}; expected e.g. 'vda', 'vdb'"])


__all__ = [
    "SUBSYSTEM",
    "plan_add",
    "plan_remove",
    "plan_resize",
]
