"""Additional node-level handlers with durable ops persistence."""

from __future__ import annotations

import copy
import json
import secrets
import time
from typing import Any

from fastapi import Request

from app.api.errors import ApiError
from app.api.registry import HandlerRegistry
from app.handlers.common import database, require_node, subdirs, values
from app.handlers.nodes import default_node_ops, load_node_ops, save_node_ops
from app.tasks.repository import TaskRepository
from app.tasks.upid import Upid

DEFAULT_HARDWARE: dict[str, Any] = {
    "pci": [
        {
            "id": "0000:00:1f.2",
            "vendor_name": "Intel Corporation",
            "device_name": "SATA Controller",
            "iommugroup": 0,
        },
        {
            "id": "0000:01:00.0",
            "vendor_name": "NVIDIA Corporation",
            "device_name": "GP102 [GeForce GTX 1080 Ti]",
            "iommugroup": 1,
            "mdev": 1,
        },
    ],
    "usb": [
        {"busnum": 1, "devnum": 1, "level": 0, "port": "1", "prodid": "0002", "vendid": "1d6b"},
        {"busnum": 2, "devnum": 2, "level": 1, "port": "2", "prodid": "5591", "vendid": "0781"},
    ],
    "mdev": {
        "0000:01:00.0": [
            {"type": "nvidia-11", "available": 4, "description": "GRID profile"},
        ]
    },
}

DEFAULT_SCAN: dict[str, list[dict[str, Any]]] = {
    "cifs": [{"server": "files.local", "share": "backups"}],
    "iscsi": [{"portal": "10.0.0.50:3260", "target": "iqn.2024-01.local:storage"}],
    "lvm": [{"vg": "pve", "size": 500_000_000_000, "free": 100_000_000_000}],
    "lvmthin": [{"lv": "data", "vg": "pve", "lv_size": 400_000_000_000}],
    "nfs": [{"server": "nfs.local", "path": "/export/pve", "options": "vers=4"}],
    "pbs": [{"server": "pbs.local", "datastore": "store1"}],
    "zfs": [{"pool": "rpool", "name": "rpool/data", "size": 800_000_000_000}],
}

DEFAULT_SUBSCRIPTION: dict[str, Any] = {
    "status": "notfound",
    "message": "There is no subscription key",
    "serverid": "SIMULATOR",
    "sockets": 1,
    "productname": "Proxmox VE",
    "url": "https://www.proxmox.com/en/proxmox-virtual-environment/pricing",
}

DEFAULT_CONFIG: dict[str, Any] = {
    "description": "Simulator node",
    "startall-onboot-delay": 0,
    "wakeonlan": "",
}

DEFAULT_DNS: dict[str, Any] = {
    "search": "local",
    "dns1": "1.1.1.1",
    "dns2": "8.8.8.8",
    "dns3": "",
}

DEFAULT_TIME: dict[str, Any] = {
    "timezone": "UTC",
    "time": 0,
    "localtime": 0,
}


def _certificates(ops: dict[str, Any]) -> dict[str, Any]:
    certs = ops.setdefault(
        "certificates",
        {
            "custom": None,
            "acme": {"account": "default", "domains": [], "certificate": None},
            "info": [],
        },
    )
    if not isinstance(certs, dict):
        certs = {"custom": None, "acme": {}, "info": []}
        ops["certificates"] = certs
    certs.setdefault("acme", {"account": "default", "domains": [], "certificate": None})
    certs.setdefault("info", [])
    return certs


def _hardware(ops: dict[str, Any]) -> dict[str, Any]:
    hardware = ops.get("hardware")
    if not isinstance(hardware, dict) or not hardware:
        hardware = copy.deepcopy(DEFAULT_HARDWARE)
        ops["hardware"] = hardware
    hardware.setdefault("pci", copy.deepcopy(DEFAULT_HARDWARE["pci"]))
    hardware.setdefault("usb", copy.deepcopy(DEFAULT_HARDWARE["usb"]))
    hardware.setdefault("mdev", copy.deepcopy(DEFAULT_HARDWARE["mdev"]))
    return hardware


def _scan_cache(ops: dict[str, Any]) -> dict[str, Any]:
    scan = ops.get("scan")
    if not isinstance(scan, dict) or not scan:
        scan = copy.deepcopy(DEFAULT_SCAN)
        ops["scan"] = scan
    for key, value in DEFAULT_SCAN.items():
        scan.setdefault(key, copy.deepcopy(value))
    return scan


def _subscription(ops: dict[str, Any]) -> dict[str, Any]:
    subscription = ops.get("subscription")
    if not isinstance(subscription, dict) or not subscription:
        subscription = copy.deepcopy(DEFAULT_SUBSCRIPTION)
        ops["subscription"] = subscription
    return subscription


def _disk_items(ops: dict[str, Any], kind: str) -> list[dict[str, Any]]:
    disks = ops.setdefault("disks", default_node_ops()["disks"])
    if not isinstance(disks, dict):
        disks = default_node_ops()["disks"]
        ops["disks"] = disks
    items = disks.setdefault(kind, [])
    if not isinstance(items, list):
        items = []
        disks[kind] = items
    return items


def _public_cert(entry: dict[str, Any] | None) -> dict[str, Any] | None:
    if entry is None:
        return None
    return {key: value for key, value in entry.items() if key not in {"key", "private-key"}}


async def _node_task(request: Request, *, node: str, task_type: str, worker: str) -> str:
    from app.db.primitives import ConflictError

    pool = database(request).pool
    upid = str(Upid.allocate(node, worker, "0", str(request.state.principal)))
    try:
        task = await TaskRepository(pool).create(
            upid=upid,
            task_type=task_type,
            payload={"node": node},
            resource_key=f"node:{node}:{task_type}",
        )
    except ConflictError as error:
        raise ApiError(409, str(error)) from error
    return task.upid


async def _set_guest_status(request: Request, node: str, status: str) -> None:
    await database(request).pool.execute(
        """UPDATE resources AS r
        SET state = jsonb_set(COALESCE(r.state, '{}'::jsonb), '{status}', to_jsonb($2::text), true),
            updated_at=now()
        WHERE r.node_id=(SELECT id FROM nodes WHERE name=$1) AND r.kind IN ('qemu', 'lxc')""",
        node,
        status,
    )


async def _migrate_guests(request: Request, node: str, target: str) -> None:
    target_row = await database(request).pool.fetchrow("SELECT id FROM nodes WHERE name=$1", target)
    if target_row is None:
        raise ApiError(404, "target node does not exist")
    await database(request).pool.execute(
        """UPDATE resources SET node_id=$2, updated_at=now()
        WHERE node_id=(SELECT id FROM nodes WHERE name=$1) AND kind IN ('qemu', 'lxc')""",
        node,
        target_row["id"],
    )


def register_nodes_extra_handlers(registry: HandlerRegistry) -> None:
    async def disks_create(request: Request, inputs: dict[str, Any], kind: str) -> dict[str, Any]:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        name = str(
            payload.get("name")
            or payload.get("device")
            or payload.get("vgname")
            or payload.get("pool")
            or f"{kind}-{secrets.token_hex(2)}"
        )
        ops = await load_node_ops(request, node)
        items = _disk_items(ops, kind)
        if any(str(item.get("name")) == name for item in items):
            raise ApiError(400, f"{kind} '{name}' already exists")
        entry = {
            key: value for key, value in payload.items() if key not in {"node", "delete", "digest"}
        }
        entry["name"] = name
        items.append(entry)
        ops.setdefault("disks", default_node_ops()["disks"])[kind] = items
        await save_node_ops(request, node, ops)
        return entry

    async def disks_delete(request: Request, inputs: dict[str, Any], kind: str) -> None:
        payload = values(inputs)
        node = str(payload["node"])
        name = str(payload["name"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        items = _disk_items(ops, kind)
        remaining = [item for item in items if str(item.get("name")) != name]
        if len(remaining) == len(items):
            raise ApiError(404, f"{kind} does not exist")
        ops.setdefault("disks", {})[kind] = remaining
        await save_node_ops(request, node, ops)

    async def disks_zfs_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        node = str(payload["node"])
        name = str(payload["name"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        for item in _disk_items(ops, "zfs"):
            if str(item.get("name")) == name:
                return dict(item)
        raise ApiError(404, "zfs pool does not exist")

    async def certificates_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]:
        await require_node(request, str(values(inputs)["node"]))
        return subdirs("acme", "custom", "info")

    async def certificates_acme_index(
        request: Request, inputs: dict[str, Any]
    ) -> list[dict[str, str]]:
        await require_node(request, str(values(inputs)["node"]))
        return subdirs("certificate")

    async def certificates_acme_mutate(request: Request, inputs: dict[str, Any]) -> str | None:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        certs = _certificates(ops)
        acme = dict(certs.get("acme") or {})
        method = request.method.upper()
        if method == "DELETE":
            acme["certificate"] = None
            acme["domains"] = []
        else:
            domains = payload.get("domains") or payload.get("domain") or acme.get("domains") or []
            if isinstance(domains, str):
                domains = [part.strip() for part in domains.split(",") if part.strip()]
            acme["domains"] = list(domains)
            acme["account"] = str(payload.get("account") or acme.get("account") or "default")
            acme["certificate"] = {
                "pem": str(payload.get("certificates") or payload.get("certificate") or "SIM-ACME"),
                "issued": int(time.time()),
            }
        certs["acme"] = acme
        ops["certificates"] = certs
        await save_node_ops(request, node, ops)
        if method == "DELETE":
            return None
        return await _node_task(request, node=node, task_type="acme", worker="acme")

    async def certificates_custom(request: Request, inputs: dict[str, Any]) -> None:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        certs = _certificates(ops)
        if request.method.upper() == "DELETE":
            certs["custom"] = None
        else:
            certificates = str(payload.get("certificates") or payload.get("cert") or "")
            if not certificates:
                raise ApiError(400, "parameter verification failed - 'certificates' missing")
            key = str(payload.get("key") or payload.get("private-key") or "")
            certs["custom"] = {
                "certificates": certificates,
                "key": key,
                "restart": int(payload.get("restart") or 0),
                "filename": str(payload.get("filename") or "pveproxy-ssl.pem"),
            }
            info = list(certs.get("info") or [])
            info = [item for item in info if item.get("filename") != certs["custom"]["filename"]]
            info.append(
                {
                    "filename": certs["custom"]["filename"],
                    "fingerprint": secrets.token_hex(20),
                    "issuer": "CN=Simulator",
                    "subject": "CN=pve.local",
                    "notbefore": int(time.time()) - 86_400,
                    "notafter": int(time.time()) + 365 * 86_400,
                    "san": ["DNS:pve.local"],
                    "public-key-type": "rsa",
                    "public-key-bits": 2048,
                }
            )
            certs["info"] = info
        ops["certificates"] = certs
        await save_node_ops(request, node, ops)

    async def certificates_info(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        certs = _certificates(ops)
        info = list(certs.get("info") or [])
        custom = _public_cert(
            certs.get("custom") if isinstance(certs.get("custom"), dict) else None
        )
        if custom and not any(item.get("filename") == custom.get("filename") for item in info):
            info.append(
                {
                    "filename": custom.get("filename", "pveproxy-ssl.pem"),
                    "fingerprint": secrets.token_hex(20),
                    "issuer": "CN=Custom",
                    "subject": "CN=pve.local",
                }
            )
            certs["info"] = info
            ops["certificates"] = certs
            await save_node_ops(request, node, ops)
        return [dict(item) for item in info]

    async def scan_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]:
        await require_node(request, str(values(inputs)["node"]))
        return subdirs("cifs", "iscsi", "lvm", "lvmthin", "nfs", "pbs", "zfs")

    async def scan_kind(
        request: Request, inputs: dict[str, Any], kind: str
    ) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        scan = _scan_cache(ops)
        ops["scan"] = scan
        await save_node_ops(request, node, ops)
        items = scan.get(kind, [])
        return [dict(item) for item in items] if isinstance(items, list) else []

    async def capabilities_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]:
        await require_node(request, str(values(inputs)["node"]))
        return subdirs("qemu")

    async def capabilities_qemu(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]:
        await require_node(request, str(values(inputs)["node"]))
        return subdirs("cpu", "cpu-flags", "machines", "migration")

    async def capabilities_cpu(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        await require_node(request, str(values(inputs)["node"]))
        return [
            {"name": "host", "vendor": "QEMU", "custom": 0},
            {"name": "x86-64-v2-AES", "vendor": "QEMU", "custom": 0},
            {"name": "kvm64", "vendor": "QEMU", "custom": 0},
        ]

    async def capabilities_cpu_flags(
        request: Request, inputs: dict[str, Any]
    ) -> list[dict[str, Any]]:
        await require_node(request, str(values(inputs)["node"]))
        return [
            {"name": "aes", "introduces": "Westmere"},
            {"name": "avx", "introduces": "SandyBridge"},
            {"name": "avx2", "introduces": "Haswell"},
        ]

    async def capabilities_machines(
        request: Request, inputs: dict[str, Any]
    ) -> list[dict[str, Any]]:
        await require_node(request, str(values(inputs)["node"]))
        return [
            {"id": "pc-i440fx-9.0", "type": "i440fx", "version": "9.0"},
            {"id": "pc-q35-9.0", "type": "q35", "version": "9.0"},
        ]

    async def capabilities_migration(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        await require_node(request, str(values(inputs)["node"]))
        return {"network": "", "type": "secure", "enabled": 1}

    async def hardware_index(request: Request, inputs: dict[str, Any]) -> list[dict[str, str]]:
        await require_node(request, str(values(inputs)["node"]))
        return subdirs("pci", "usb")

    async def hardware_pci(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        hardware = _hardware(ops)
        ops["hardware"] = hardware
        await save_node_ops(request, node, ops)
        return [dict(item) for item in hardware.get("pci", [])]

    async def hardware_pci_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        node = str(payload["node"])
        pci_id = str(payload.get("pci-id-or-mapping") or payload.get("pciid") or "")
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        for item in _hardware(ops).get("pci", []):
            if str(item.get("id")) == pci_id:
                return dict(item)
        raise ApiError(404, "pci device does not exist")

    async def hardware_pci_mdev(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        payload = values(inputs)
        node = str(payload["node"])
        pci_id = str(payload.get("pci-id-or-mapping") or payload.get("pciid") or "")
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        hardware = _hardware(ops)
        mdev = hardware.get("mdev", {})
        items = mdev.get(pci_id, []) if isinstance(mdev, dict) else []
        return [dict(item) for item in items] if isinstance(items, list) else []

    async def hardware_usb(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        hardware = _hardware(ops)
        ops["hardware"] = hardware
        await save_node_ops(request, node, ops)
        return [dict(item) for item in hardware.get("usb", [])]

    async def subscription_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        public = dict(_subscription(ops))
        public.pop("key", None)
        return public

    async def subscription_mutate(
        request: Request, inputs: dict[str, Any]
    ) -> dict[str, Any] | None:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        current = _subscription(ops)
        method = request.method.upper()
        if method == "DELETE":
            ops["subscription"] = copy.deepcopy(DEFAULT_SUBSCRIPTION)
            await save_node_ops(request, node, ops)
            return None
        if method == "POST":
            current["checktime"] = int(time.time())
            current["status"] = current.get("status") or "Active"
            ops["subscription"] = current
            await save_node_ops(request, node, ops)
            return dict(current)
        key = str(payload.get("key") or current.get("key") or "")
        updated = {
            **current,
            **{k: v for k, v in payload.items() if k not in {"node", "delete", "digest"}},
            "key": key,
            "status": "Active" if key else current.get("status", "notfound"),
            "message": "OK" if key else current.get("message", "There is no subscription key"),
        }
        ops["subscription"] = updated
        await save_node_ops(request, node, ops)
        public = dict(updated)
        public.pop("key", None)
        return public

    async def aplinfo_get(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        items = ops.get("aplinfo")
        if not isinstance(items, list):
            items = [
                {
                    "package": "alpine-3-standard",
                    "section": "system",
                    "type": "lxc",
                    "version": "3.20",
                }
            ]
            ops["aplinfo"] = items
            await save_node_ops(request, node, ops)
        return [dict(item) for item in items]

    async def aplinfo_download(request: Request, inputs: dict[str, Any]) -> str:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        downloads = list(ops.get("aplinfo_downloads") or [])
        downloads.append(
            {
                "template": str(payload.get("template") or payload.get("storage") or "unknown"),
                "at": int(time.time()),
            }
        )
        ops["aplinfo_downloads"] = downloads
        await save_node_ops(request, node, ops)
        return await _node_task(request, node=node, task_type="download", worker="download")

    async def apt_repositories_mutate(request: Request, inputs: dict[str, Any]) -> None:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        apt = ops.setdefault("apt", copy.deepcopy(default_node_ops()["apt"]))
        repositories = list(apt.get("repositories") or [])
        method = request.method.upper()
        if method == "POST":
            entry = {
                key: value
                for key, value in payload.items()
                if key not in {"node", "delete", "digest"}
            }
            entry.setdefault("path", f"/etc/apt/sources.list.d/sim-{secrets.token_hex(2)}.list")
            entry.setdefault("enabled", 1)
            repositories.append(entry)
        else:
            path = payload.get("path")
            handle = payload.get("handle")
            index = payload.get("index")
            updated: list[dict[str, Any]] = []
            for idx, item in enumerate(repositories):
                match = False
                if path is not None and item.get("path") == path:
                    match = True
                if handle is not None and item.get("handle") == handle:
                    match = True
                if index is not None and idx == int(index):
                    match = True
                if match or (path is None and handle is None and index is None and idx == 0):
                    merged = {
                        **item,
                        **{
                            key: value
                            for key, value in payload.items()
                            if key not in {"node", "delete", "digest", "path", "handle", "index"}
                        },
                    }
                    updated.append(merged)
                else:
                    updated.append(item)
            repositories = updated
        apt["repositories"] = repositories
        ops["apt"] = apt
        await save_node_ops(request, node, ops)

    async def node_config_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        config = ops.get("config")
        if not isinstance(config, dict):
            config = copy.deepcopy(DEFAULT_CONFIG)
            ops["config"] = config
            await save_node_ops(request, node, ops)
        return dict(config)

    async def node_config_put(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        config = dict(ops.get("config") or DEFAULT_CONFIG)
        config.update(
            {
                key: value
                for key, value in payload.items()
                if key not in {"node", "digest", "delete"}
            }
        )
        ops["config"] = config
        await save_node_ops(request, node, ops)
        return config

    async def dns_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        dns = ops.get("dns")
        if not isinstance(dns, dict):
            dns = copy.deepcopy(DEFAULT_DNS)
            ops["dns"] = dns
            await save_node_ops(request, node, ops)
        return dict(dns)

    async def dns_put(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        dns = dict(ops.get("dns") or DEFAULT_DNS)
        dns.update(
            {
                key: value
                for key, value in payload.items()
                if key not in {"node", "digest", "delete"}
            }
        )
        ops["dns"] = dns
        await save_node_ops(request, node, ops)
        return dns

    async def time_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        current = dict(ops.get("time") or DEFAULT_TIME)
        now = int(time.time())
        current["time"] = now
        current["localtime"] = now
        current.setdefault("timezone", "UTC")
        ops["time"] = current
        await save_node_ops(request, node, ops)
        return current

    async def time_put(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        current = dict(ops.get("time") or DEFAULT_TIME)
        if "timezone" in payload:
            current["timezone"] = str(payload["timezone"])
        now = int(time.time())
        current["time"] = now
        current["localtime"] = now
        ops["time"] = current
        await save_node_ops(request, node, ops)
        return current

    async def execute(request: Request, inputs: dict[str, Any]) -> list[str]:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        commands = payload.get("commands") or payload.get("command") or []
        if isinstance(commands, str):
            try:
                parsed = json.loads(commands)
                commands = parsed if isinstance(parsed, list) else [commands]
            except json.JSONDecodeError:
                commands = [commands]
        ops = await load_node_ops(request, node)
        log = list(ops.get("execute_log") or [])
        output: list[str] = []
        for command in commands:
            entry = {"command": str(command), "at": int(time.time())}
            log.append(entry)
            output.append(f"OK: {command}")
        ops["execute_log"] = log[-100:]
        await save_node_ops(request, node, ops)
        return output

    async def hosts_get(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        hosts = ops.get("hosts")
        if not isinstance(hosts, dict):
            hosts = {
                "data": f"127.0.0.1 localhost\n10.0.0.10 {node}\n",
                "digest": secrets.token_hex(8),
            }
            ops["hosts"] = hosts
            await save_node_ops(request, node, ops)
        return {"data": str(hosts.get("data", "")), "digest": str(hosts.get("digest", ""))}

    async def hosts_post(request: Request, inputs: dict[str, Any]) -> None:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        ops["hosts"] = {
            "data": str(payload.get("data") or ""),
            "digest": secrets.token_hex(8),
        }
        await save_node_ops(request, node, ops)

    async def journal(request: Request, inputs: dict[str, Any]) -> list[str]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        start = int(values(inputs).get("startcursor") or values(inputs).get("start") or 0)
        limit = int(values(inputs).get("limit") or 50)
        lines = [
            f"{index}: {node} systemd[1]: Started simulated service {index}."
            for index in range(start, start + limit)
        ]
        return lines

    async def syslog(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        limit = int(values(inputs).get("limit") or 50)
        return [
            {"n": index, "t": f"{node} kernel: simulated syslog line {index}"}
            for index in range(limit)
        ]

    async def netstat(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        return [
            {"in": 1_000_000, "out": 900_000, "vnet": "vmbr0", "hwaddr": "bc:24:11:00:00:01"},
            {"in": 500_000, "out": 450_000, "vnet": "vmbr1", "hwaddr": "bc:24:11:00:00:02"},
        ]

    async def report(request: Request, inputs: dict[str, Any]) -> str:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        return f"==== Proxmox node report for {node} ====\nuptime: simulated\n"

    async def rrd(_request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        await require_node(_request, str(values(inputs)["node"]))
        return {"filename": "/var/lib/rrdcached/db/pve-node.rrd"}

    async def rrddata(_request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        await require_node(_request, str(values(inputs)["node"]))
        now = int(time.time())
        return [
            {"time": now - 120, "cpu": 0.05, "memused": 1_000_000_000},
            {"time": now - 60, "cpu": 0.07, "memused": 1_100_000_000},
            {"time": now, "cpu": 0.04, "memused": 1_050_000_000},
        ]

    async def startall(request: Request, inputs: dict[str, Any]) -> str:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        await _set_guest_status(request, node, "running")
        return await _node_task(request, node=node, task_type="startall", worker="startall")

    async def stopall(request: Request, inputs: dict[str, Any]) -> str:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        await _set_guest_status(request, node, "stopped")
        return await _node_task(request, node=node, task_type="stopall", worker="stopall")

    async def suspendall(request: Request, inputs: dict[str, Any]) -> str:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        await _set_guest_status(request, node, "paused")
        return await _node_task(request, node=node, task_type="suspendall", worker="suspendall")

    async def migrateall(request: Request, inputs: dict[str, Any]) -> str:
        payload = values(inputs)
        node = str(payload["node"])
        target = str(payload.get("target") or "")
        await require_node(request, node)
        if not target:
            raise ApiError(400, "parameter verification failed - 'target' missing")
        await _migrate_guests(request, node, target)
        return await _node_task(request, node=node, task_type="migrateall", worker="migrateall")

    async def status_post(request: Request, inputs: dict[str, Any]) -> str | None:
        payload = values(inputs)
        node = str(payload["node"])
        await require_node(request, node)
        command = str(payload.get("command") or "reboot")
        ops = await load_node_ops(request, node)
        ops["last_status_command"] = {"command": command, "at": int(time.time())}
        await save_node_ops(request, node, ops)
        return await _node_task(request, node=node, task_type=command, worker=command)

    async def wakeonlan(request: Request, inputs: dict[str, Any]) -> str:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        ops["wakeonlan"] = {"at": int(time.time())}
        await save_node_ops(request, node, ops)
        return "OK"

    async def _shell_proxy(request: Request, inputs: dict[str, Any], kind: str) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        payload = {
            "port": 5900 if kind == "vnc" else 22 if kind == "term" else 3128,
            "ticket": secrets.token_urlsafe(24),
            "user": str(getattr(request.state, "principal", "root@pam")),
            "upid": f"UPID:{node}:{secrets.token_hex(4)}:{kind}shell:0:root@pam:",
        }
        ops = await load_node_ops(request, node)
        shells = ops.setdefault("shells", {})
        shells[kind] = {key: value for key, value in payload.items() if key != "ticket"}
        ops["shells"] = shells
        await save_node_ops(request, node, ops)
        return payload

    async def spiceshell(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        return await _shell_proxy(request, inputs, "spice")

    async def termproxy(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        return await _shell_proxy(request, inputs, "term")

    async def vncshell(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        return await _shell_proxy(request, inputs, "vnc")

    async def network_reload(request: Request, inputs: dict[str, Any]) -> None:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        if not isinstance(ops.get("network"), list):
            ops["network"] = copy.deepcopy(default_node_ops()["network"])
        ops["network_applied"] = False
        await save_node_ops(request, node, ops)

    async def query_oci_repo_tags(request: Request, inputs: dict[str, Any]) -> list[dict[str, Any]]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        repo = str(values(inputs).get("repo") or "library/alpine")
        ops = await load_node_ops(request, node)
        cache = ops.setdefault("oci_tags", {})
        if repo not in cache:
            cache[repo] = [{"tag": "latest"}, {"tag": "3.20"}]
            ops["oci_tags"] = cache
            await save_node_ops(request, node, ops)
        return [dict(item) for item in cache[repo]]

    async def query_url_metadata(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        url = str(values(inputs).get("url") or "")
        ops = await load_node_ops(request, node)
        cache = ops.setdefault("url_metadata", {})
        if url not in cache:
            cache[url] = {
                "filename": url.rsplit("/", 1)[-1] or "download.bin",
                "mimetype": "application/octet-stream",
                "size": 1024,
            }
            ops["url_metadata"] = cache
            await save_node_ops(request, node, ops)
        return dict(cache[url])

    async def vncwebsocket(request: Request, inputs: dict[str, Any]) -> dict[str, Any]:
        node = str(values(inputs)["node"])
        await require_node(request, node)
        ops = await load_node_ops(request, node)
        shell = (ops.get("shells") or {}).get("vnc") or {"port": 5900}
        return {
            "port": shell.get("port", 5900),
            "ticket": secrets.token_urlsafe(24),
        }

    # Disks mutations (GET collections already registered in nodes.py)
    registry.register(
        "/nodes/{node}/disks/directory",
        "POST",
        lambda request, inputs: disks_create(request, inputs, "directory"),
    )
    registry.register(
        "/nodes/{node}/disks/directory/{name}",
        "DELETE",
        lambda request, inputs: disks_delete(request, inputs, "directory"),
    )
    registry.register(
        "/nodes/{node}/disks/lvm",
        "POST",
        lambda request, inputs: disks_create(request, inputs, "lvm"),
    )
    registry.register(
        "/nodes/{node}/disks/lvm/{name}",
        "DELETE",
        lambda request, inputs: disks_delete(request, inputs, "lvm"),
    )
    registry.register(
        "/nodes/{node}/disks/lvmthin",
        "POST",
        lambda request, inputs: disks_create(request, inputs, "lvmthin"),
    )
    registry.register(
        "/nodes/{node}/disks/lvmthin/{name}",
        "DELETE",
        lambda request, inputs: disks_delete(request, inputs, "lvmthin"),
    )
    registry.register(
        "/nodes/{node}/disks/zfs",
        "POST",
        lambda request, inputs: disks_create(request, inputs, "zfs"),
    )
    registry.register("/nodes/{node}/disks/zfs/{name}", "GET", disks_zfs_get)
    registry.register(
        "/nodes/{node}/disks/zfs/{name}",
        "DELETE",
        lambda request, inputs: disks_delete(request, inputs, "zfs"),
    )

    registry.register("/nodes/{node}/certificates", "GET", certificates_index)
    registry.register("/nodes/{node}/certificates/acme", "GET", certificates_acme_index)
    registry.register(
        "/nodes/{node}/certificates/acme/certificate", "POST", certificates_acme_mutate
    )
    registry.register(
        "/nodes/{node}/certificates/acme/certificate", "PUT", certificates_acme_mutate
    )
    registry.register(
        "/nodes/{node}/certificates/acme/certificate", "DELETE", certificates_acme_mutate
    )
    registry.register("/nodes/{node}/certificates/custom", "POST", certificates_custom)
    registry.register("/nodes/{node}/certificates/custom", "DELETE", certificates_custom)
    registry.register("/nodes/{node}/certificates/info", "GET", certificates_info)

    registry.register("/nodes/{node}/scan", "GET", scan_index)
    registry.register("/nodes/{node}/scan/cifs", "GET", lambda r, i: scan_kind(r, i, "cifs"))
    registry.register("/nodes/{node}/scan/iscsi", "GET", lambda r, i: scan_kind(r, i, "iscsi"))
    registry.register("/nodes/{node}/scan/lvm", "GET", lambda r, i: scan_kind(r, i, "lvm"))
    registry.register("/nodes/{node}/scan/lvmthin", "GET", lambda r, i: scan_kind(r, i, "lvmthin"))
    registry.register("/nodes/{node}/scan/nfs", "GET", lambda r, i: scan_kind(r, i, "nfs"))
    registry.register("/nodes/{node}/scan/pbs", "GET", lambda r, i: scan_kind(r, i, "pbs"))
    registry.register("/nodes/{node}/scan/zfs", "GET", lambda r, i: scan_kind(r, i, "zfs"))

    registry.register("/nodes/{node}/capabilities", "GET", capabilities_index)
    registry.register("/nodes/{node}/capabilities/qemu", "GET", capabilities_qemu)
    registry.register("/nodes/{node}/capabilities/qemu/cpu", "GET", capabilities_cpu)
    registry.register("/nodes/{node}/capabilities/qemu/cpu-flags", "GET", capabilities_cpu_flags)
    registry.register("/nodes/{node}/capabilities/qemu/machines", "GET", capabilities_machines)
    registry.register("/nodes/{node}/capabilities/qemu/migration", "GET", capabilities_migration)

    registry.register("/nodes/{node}/hardware", "GET", hardware_index)
    registry.register("/nodes/{node}/hardware/pci", "GET", hardware_pci)
    registry.register("/nodes/{node}/hardware/pci/{pci-id-or-mapping}", "GET", hardware_pci_get)
    registry.register(
        "/nodes/{node}/hardware/pci/{pci-id-or-mapping}/mdev", "GET", hardware_pci_mdev
    )
    registry.register("/nodes/{node}/hardware/usb", "GET", hardware_usb)

    registry.register("/nodes/{node}/subscription", "GET", subscription_get)
    registry.register("/nodes/{node}/subscription", "PUT", subscription_mutate)
    registry.register("/nodes/{node}/subscription", "POST", subscription_mutate)
    registry.register("/nodes/{node}/subscription", "DELETE", subscription_mutate)

    registry.register("/nodes/{node}/aplinfo", "GET", aplinfo_get)
    registry.register("/nodes/{node}/aplinfo", "POST", aplinfo_download)
    registry.register("/nodes/{node}/apt/repositories", "POST", apt_repositories_mutate)
    registry.register("/nodes/{node}/apt/repositories", "PUT", apt_repositories_mutate)
    registry.register("/nodes/{node}/config", "GET", node_config_get)
    registry.register("/nodes/{node}/config", "PUT", node_config_put)
    registry.register("/nodes/{node}/dns", "GET", dns_get)
    registry.register("/nodes/{node}/dns", "PUT", dns_put)
    registry.register("/nodes/{node}/time", "GET", time_get)
    registry.register("/nodes/{node}/time", "PUT", time_put)
    registry.register("/nodes/{node}/execute", "POST", execute)
    registry.register("/nodes/{node}/hosts", "GET", hosts_get)
    registry.register("/nodes/{node}/hosts", "POST", hosts_post)
    registry.register("/nodes/{node}/journal", "GET", journal)
    registry.register("/nodes/{node}/syslog", "GET", syslog)
    registry.register("/nodes/{node}/netstat", "GET", netstat)
    registry.register("/nodes/{node}/report", "GET", report)
    registry.register("/nodes/{node}/rrd", "GET", rrd)
    registry.register("/nodes/{node}/rrddata", "GET", rrddata)
    registry.register("/nodes/{node}/migrateall", "POST", migrateall)
    registry.register("/nodes/{node}/startall", "POST", startall)
    registry.register("/nodes/{node}/stopall", "POST", stopall)
    registry.register("/nodes/{node}/suspendall", "POST", suspendall)
    registry.register("/nodes/{node}/status", "POST", status_post)
    registry.register("/nodes/{node}/wakeonlan", "POST", wakeonlan)
    registry.register("/nodes/{node}/spiceshell", "POST", spiceshell)
    registry.register("/nodes/{node}/termproxy", "POST", termproxy)
    registry.register("/nodes/{node}/vncshell", "POST", vncshell)
    registry.register("/nodes/{node}/network", "DELETE", network_reload)
    registry.register("/nodes/{node}/query-oci-repo-tags", "GET", query_oci_repo_tags)
    registry.register("/nodes/{node}/query-url-metadata", "GET", query_url_metadata)
    registry.register("/nodes/{node}/vncwebsocket", "GET", vncwebsocket)
