"""Graphics-related fix executors.

- :func:`graphics_localhost_listen` — set ``listen='127.0.0.1'``
  on every ``<graphics>`` element.
- :func:`graphics_set_password` — generate a 24-char random
  password via :func:`secrets.token_urlsafe` and set the
  ``passwd`` attribute. Idempotent: leaves an already-set
  password alone (rotation is a separate operation).
"""

from __future__ import annotations

import secrets
import xml.etree.ElementTree as ET  # noqa: TC003
from typing import TYPE_CHECKING, Any

from mgf.vm.core.security.isolation.fixes._helpers import devices_element
from mgf.vm.core.security.isolation.types import isolation_fix

if TYPE_CHECKING:
    from mgf.vm.core.common.progress import ProgressReporter


__all__ = ["graphics_localhost_listen", "graphics_set_password"]


_LOOPBACK = "127.0.0.1"


@isolation_fix("graphics.localhost_listen")
def graphics_localhost_listen(
    *,
    target: ET.ElementTree,
    reporter: ProgressReporter,
    dry_run: bool = False,
    **_kw: Any,
) -> None:
    """Bind every SPICE/VNC endpoint to the loopback address."""
    devices = devices_element(target)
    if devices is None:
        reporter.report("graphics.localhost_listen: no <devices> element")
        return

    changed = 0
    for graphics in devices.findall("graphics"):
        # Two libvirt forms: attribute on <graphics> OR child element.
        attr_listen = graphics.get("listen")
        child_listen = graphics.find("listen")

        already_loopback = attr_listen == _LOOPBACK or (
            child_listen is not None and child_listen.get("address") == _LOOPBACK
        )
        if already_loopback:
            continue

        if dry_run:
            reporter.report(
                f"graphics.localhost_listen: (dry-run) would bind "
                f"{graphics.get('type', '?')!r} to {_LOOPBACK}",
            )
        else:
            # Use the attribute form (simpler; takes precedence over
            # the child element when both are present per libvirt
            # docs).
            graphics.set("listen", _LOOPBACK)
            # Drop a non-loopback child <listen> so the two forms
            # don't disagree.
            if child_listen is not None and child_listen.get("address") != _LOOPBACK:
                graphics.remove(child_listen)
        changed += 1

    if changed == 0:
        reporter.report("graphics.localhost_listen: already satisfied")
        return
    if not dry_run:
        reporter.report(
            f"graphics.localhost_listen: bound {changed} graphics endpoint(s) to {_LOOPBACK}",
        )


@isolation_fix("graphics.set_password")
def graphics_set_password(
    *,
    target: ET.ElementTree,
    reporter: ProgressReporter,
    dry_run: bool = False,
    **_kw: Any,
) -> None:
    """Generate a 24-char random password for every passwordless graphics
    endpoint.

    Idempotent: leaves existing passwords alone. Rotation is a
    separate operation (a future ``--rotate-passwords`` flag).
    """
    devices = devices_element(target)
    if devices is None:
        reporter.report("graphics.set_password: no <devices> element")
        return

    set_count = 0
    for graphics in devices.findall("graphics"):
        if graphics.get("passwd"):
            continue
        if dry_run:
            reporter.report(
                f"graphics.set_password: (dry-run) would generate password for "
                f"{graphics.get('type', '?')!r}",
            )
        else:
            password = secrets.token_urlsafe(18)  # ~24 chars after URL-safe encode
            graphics.set("passwd", password)
        set_count += 1

    if set_count == 0:
        reporter.report("graphics.set_password: already satisfied")
        return
    if not dry_run:
        reporter.report(
            f"graphics.set_password: generated password(s) for {set_count} endpoint(s)",
        )
