"""Thread-safe async libvirt operations wrapper.

All libvirt C library calls must go through a single ThreadPoolExecutor(max_workers=1)
because the libvirt C library is not thread-safe (see design decision D-08).
"""

from __future__ import annotations

import asyncio
import hashlib
import logging
from concurrent.futures import ThreadPoolExecutor

import libvirt

logger = logging.getLogger("vmcluster_mcp.cluster.libvirt_client")

# Single-worker executor enforces thread-safe access to libvirt (D-08)
_executor = ThreadPoolExecutor(max_workers=1)

# Map libvirt VIR_DOMAIN_* integer constants → NodeStatus.domain_state literals
_DOMAIN_STATE_MAP: dict[int, str] = {
    1: "running",   # VIR_DOMAIN_RUNNING
    3: "paused",    # VIR_DOMAIN_PAUSED
    5: "shut off",  # VIR_DOMAIN_SHUTOFF
    6: "crashed",   # VIR_DOMAIN_CRASHED
}


# ---------------------------------------------------------------------------
# Utility functions (sync, no libvirt I/O)
# ---------------------------------------------------------------------------


def domain_name(cluster_name: str, node_name: str) -> str:
    """Return the libvirt domain name for a cluster node (D-10 naming convention)."""
    return f"{cluster_name}_{node_name}"


def generate_mac(cluster_name: str, node_name: str) -> str:
    """Generate a deterministic, stable MAC address in the 52:54:00:xx:xx:xx range.

    Uses SHA-256 hash of "{cluster_name}:{node_name}" — same inputs always produce
    the same MAC address (idempotent defines), different inputs produce different MACs.
    The 52:54:00 prefix is the QEMU/KVM OUI used by libvirt conventions.
    """
    digest = hashlib.sha256(f"{cluster_name}:{node_name}".encode()).digest()
    b = digest[0:3]
    return f"52:54:00:{b[0]:02x}:{b[1]:02x}:{b[2]:02x}"


def map_domain_state(state_int: int) -> str:
    """Map a libvirt VIR_DOMAIN_* integer to a NodeStatus.domain_state literal."""
    return _DOMAIN_STATE_MAP.get(state_int, "unknown")


# ---------------------------------------------------------------------------
# LibvirtClient
# ---------------------------------------------------------------------------


class LibvirtClient:
    """Async interface to libvirt with all calls serialised through a single-worker executor."""

    def __init__(self, uri: str) -> None:
        self._uri = uri
        self._conn: libvirt.virConnect | None = None

    def _get_conn(self) -> libvirt.virConnect:
        """Open and cache the libvirt connection lazily on first use."""
        if self._conn is None:
            logger.debug("Opening libvirt connection to %s", self._uri)
            self._conn = libvirt.open(self._uri)
        return self._conn

    async def _run(self, func, *args):
        """Dispatch a synchronous callable to the single-worker executor."""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(_executor, func, *args)

    # --- Domain operations ---

    async def define_domain(self, xml: str) -> libvirt.virDomain:
        """Define a persistent domain from XML."""
        conn = self._get_conn()
        return await self._run(conn.defineXML, xml)

    async def lookup_domain(self, name: str) -> libvirt.virDomain | None:
        """Look up a domain by name; returns None if not found."""
        conn = self._get_conn()
        try:
            return await self._run(conn.lookupByName, name)
        except libvirt.libvirtError:
            return None

    async def start_domain(self, dom: libvirt.virDomain) -> None:
        """Start (boot) a defined domain."""
        await self._run(dom.create)

    async def shutdown_domain(self, dom: libvirt.virDomain) -> None:
        """Send ACPI shutdown signal to a running domain."""
        await self._run(dom.shutdown)

    async def destroy_domain(self, dom: libvirt.virDomain) -> None:
        """Force-kill a running domain."""
        await self._run(dom.destroy)

    async def undefine_domain(self, dom: libvirt.virDomain) -> None:
        """Remove a domain definition from libvirt."""
        await self._run(dom.undefine)

    async def get_domain_state(self, dom: libvirt.virDomain) -> str:
        """Return the domain state as a NodeStatus.domain_state string."""
        state_tuple = await self._run(dom.state)
        return map_domain_state(state_tuple[0])

    async def is_domain_active(self, dom: libvirt.virDomain) -> bool:
        """Return True if the domain is currently active (running)."""
        result = await self._run(dom.isActive)
        return bool(result)

    # --- Network operations ---

    async def define_network(self, xml: str) -> libvirt.virNetwork:
        """Define a persistent virtual network from XML."""
        conn = self._get_conn()
        return await self._run(conn.networkDefineXML, xml)

    async def lookup_network(self, name: str) -> libvirt.virNetwork | None:
        """Look up a network by name; returns None if not found."""
        conn = self._get_conn()
        try:
            return await self._run(conn.networkLookupByName, name)
        except libvirt.libvirtError:
            return None

    async def start_network(self, net: libvirt.virNetwork) -> None:
        """Activate a defined network."""
        await self._run(net.create)

    async def destroy_network(self, net: libvirt.virNetwork) -> None:
        """Deactivate an active network."""
        await self._run(net.destroy)

    async def undefine_network(self, net: libvirt.virNetwork) -> None:
        """Remove a network definition from libvirt."""
        await self._run(net.undefine)

    async def is_network_active(self, net: libvirt.virNetwork) -> bool:
        """Return True if the network is currently active."""
        result = await self._run(net.isActive)
        return bool(result)

    # --- Snapshot operations ---

    async def snapshot_create_xml(
        self, dom: libvirt.virDomain, xml: str, flags: int
    ) -> libvirt.virDomainSnapshot:
        """Create a domain snapshot from XML; dispatched through single-worker executor."""
        return await self._run(dom.snapshotCreateXML, xml, flags)

    async def list_all_snapshots(
        self, dom: libvirt.virDomain, flags: int = 0
    ) -> list:
        """List all snapshots for a domain."""
        return await self._run(dom.listAllSnapshots, flags)

    async def snapshot_lookup_by_name(
        self, dom: libvirt.virDomain, name: str
    ) -> libvirt.virDomainSnapshot | None:
        """Look up a snapshot by name; returns None if not found."""
        try:
            return await self._run(dom.snapshotLookupByName, name, 0)
        except libvirt.libvirtError:
            return None

    async def snapshot_get_xml_desc(
        self, snap: libvirt.virDomainSnapshot, flags: int = 0
    ) -> str:
        """Return the XML description of a snapshot."""
        return await self._run(snap.getXMLDesc, flags)

    async def get_domain_xml(
        self, dom: libvirt.virDomain, flags: int = 0
    ) -> str:
        """Return the XML description of a domain."""
        return await self._run(dom.XMLDesc, flags)

    async def revert_to_snapshot(
        self,
        dom: libvirt.virDomain,
        snap: libvirt.virDomainSnapshot,
        flags: int = 0,
    ) -> None:
        """Revert a domain to a snapshot; dispatched through single-worker executor."""
        await self._run(dom.revertToSnapshot, snap, flags)

    async def start_domain_by_name(self, name: str) -> None:
        """Look up a domain by name and start it with a fresh handle.

        Obtains a fresh virDomain handle via lookup_domain before calling create,
        because the handle held during revert_to_snapshot may be stale.
        """
        dom = await self.lookup_domain(name)
        if dom is None:
            raise RuntimeError(f"Domain '{name}' not found")
        await self._run(dom.create)

    async def delete_snapshot(
        self, snap: libvirt.virDomainSnapshot, flags: int = 0
    ) -> None:
        """Delete a domain snapshot; dispatched through single-worker executor."""
        await self._run(snap.delete, flags)

    # --- Connection lifecycle ---

    async def close(self) -> None:
        """Close the libvirt connection if it was opened."""
        if self._conn is not None:
            logger.debug("Closing libvirt connection")
            self._conn.close()
            self._conn = None
