"""Async facade over :class:`~mcp_vm_blackbox.coordination_db.CoordinationDB` for VM registry operations.

All storage is delegated to the SQLite-backed ``vm_registry`` table.
Write failures are logged and swallowed — the registry is a cache.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime
from typing import TYPE_CHECKING

import aiosqlite

from mcp_vm_blackbox._helpers import default_blackbox_data_root
from mcp_vm_blackbox.coordination_db import get_db
from mcp_vm_blackbox.models import VmListEntry, VMRegistryEntry

if TYPE_CHECKING:
    from pathlib import Path

_DEFAULT_ROOT = default_blackbox_data_root()

_logger = logging.getLogger(__name__)


class VMRegistry:
    """Async facade over :class:`~mcp_vm_blackbox.coordination_db.CoordinationDB`.

    All VM registry state is stored in the SQLite ``vm_registry`` table.
    The public API is intentionally unchanged — only ``def`` becomes
    ``async def``.  Errors from the database layer are logged at WARNING
    level and never re-raised; callers receive empty results on failure.

    Usage::

        registry = VMRegistry(root=default_blackbox_data_root())
        entries = await registry.list_all()
    """

    def __init__(self, root: Path) -> None:
        """Initialise the registry facade.

        Args:
            root: Repo-local storage root (typically from :func:`default_blackbox_data_root`).
                Passed to :func:`~mcp_vm_blackbox.coordination_db.get_db` to
                select the correct singleton.
        """
        self._root = root

    async def populate(self, entries: list[VmListEntry]) -> None:
        """Overwrite the registry with *entries* from a ``vm_list`` call.

        Each :class:`VmListEntry` is converted to a :class:`VMRegistryEntry`
        with ``last_refreshed`` set to the current UTC time, then upserted
        into the SQLite ``vm_registry`` table.

        Args:
            entries: VM list entries returned by ``vm_list``.
        """
        now = datetime.now(UTC).isoformat()
        registry_entries = [
            VMRegistryEntry(vm_name=e.name, uuid=e.uuid, running=e.running, last_refreshed=now) for e in entries
        ]
        try:
            db = await get_db(self._root)
            await db.upsert_vms(registry_entries)
        except (aiosqlite.Error, OSError):
            _logger.warning("Failed to populate VM registry", exc_info=True)

    async def get(self, vm_name: str) -> VMRegistryEntry | None:
        """Return the registry entry for *vm_name*, or ``None`` if not found.

        Args:
            vm_name: VirtualBox VM name to look up.

        Returns:
            The :class:`VMRegistryEntry`, or ``None`` if not found or on error.
        """
        try:
            db = await get_db(self._root)
            return await db.get_vm(vm_name)
        except (aiosqlite.Error, OSError):
            _logger.warning("Failed to get VM registry entry for %s", vm_name, exc_info=True)
            return None

    async def list_all(self) -> list[VMRegistryEntry]:
        """Return all persisted VM entries.

        Returns an empty list when the registry is unpopulated or on error.

        Returns:
            List of :class:`VMRegistryEntry` instances.
        """
        try:
            db = await get_db(self._root)
            return await db.list_all_vms()
        except (aiosqlite.Error, OSError):
            _logger.warning("Failed to list VM registry entries", exc_info=True)
            return []

    async def update(self, vm_name: str, **fields: object) -> VMRegistryEntry | None:
        """Update fields on the registry entry for *vm_name* and persist.

        Only fields present on :class:`VMRegistryEntry` are accepted.
        Returns the updated entry, or ``None`` if the VM is not in the registry.

        Args:
            vm_name: VirtualBox VM name to update.
            **fields: Keyword arguments matching VMRegistryEntry field names.

        Returns:
            The updated :class:`VMRegistryEntry`, or ``None`` if not found.

        Raises:
            ValueError: If any key in *fields* is not a valid VMRegistryEntry field.
        """
        valid_fields = set(VMRegistryEntry.model_fields)
        invalid_keys = set(fields) - valid_fields
        if invalid_keys:
            sorted_invalid = sorted(invalid_keys)
            sorted_valid = sorted(valid_fields)
            msg = f"Invalid field(s) for VMRegistryEntry: {sorted_invalid}. Valid fields: {sorted_valid}"
            raise ValueError(msg)

        try:
            db = await get_db(self._root)
            entry = await db.get_vm(vm_name)
        except (aiosqlite.Error, OSError):
            _logger.warning("Failed to fetch VM registry entry for %s during update", vm_name, exc_info=True)
            return None

        if entry is None:
            return None

        for key, value in fields.items():
            setattr(entry, key, value)

        try:
            await db.upsert_vm(entry)
        except (aiosqlite.Error, OSError):
            _logger.warning("Failed to persist VM registry update for %s", vm_name, exc_info=True)
        return entry

    async def refresh(self) -> list[VMRegistryEntry]:
        """Refresh the registry from live VM state.

        .. note::

            This is a placeholder — the real implementation (which calls
            ``vm_list`` via MCP) will be wired in S03.  For now it simply
            returns the current persisted entries.

        Returns:
            The current list of registry entries.
        """
        return await self.list_all()


# Shared singleton — import this instead of constructing VMRegistry directly.
default_registry = VMRegistry(root=_DEFAULT_ROOT)
