"""F2 — the libvirt exception-translation seam.

Every public adapter method MUST translate ``libvirt.libvirtError`` into
the :mod:`mgf.vm.core.exceptions` taxonomy at the seam (contracts.md §7,
EH-02/EH-03), chaining the original via ``raise ... from exc`` so the
debug chain survives.

This module provides:

* :class:`LibvirtErrorTranslator` — a :class:`mgf.common.providers.ProviderABC`
  subclass whose :meth:`translate_exception` maps a ``libvirtError`` by
  its ``get_error_code()`` to the right core type.
* :func:`translate_at_seam` (re-exported from ``mgf.common.providers``) —
  the decorator backend methods wrap themselves with.

Decisions baked in (see ``vm-vmanager-docs/component-specs/contracts.md``):

* **Conservative mapping.** An ambiguous or unknown code maps to the
  *generic parent* (``HypervisorError`` / ``StorageError``), never a
  wrong specific subclass. Surfacing a generic-but-correct error beats a
  specific-but-wrong one — the latter would mislead the reconciler's
  retry decisions. (The legacy ``libvirt_provider._translate`` covered
  ~3 codes and once aliased ``OPERATION_INVALID`` to
  ``DomainAlreadyExists``; that class of bug is exactly what the
  conservative rule prevents.)
* **Context is enriched at call sites, not here.** ``translate_exception``
  is pure over the exception — it never knows the ``vm_name`` / pool /
  volume. The backend method catches the typed error (or sets attributes
  before re-raising) when it wants to attach operation context. This
  keeps the table a pure function of the libvirt code, which is what the
  ~30-case unit matrix pins.

The error-code integers are the **stable** values from
``libvirt-libvirt-common.h`` (``virErrorNumber``). They are part of
libvirt's ABI and do not change across versions, so the table is keyed
by the integer rather than by importing the (optional) ``libvirt``
module — that lets this seam be imported and unit-tested on a host with
no libvirt binding at all.
"""

from __future__ import annotations

from mgf.common.providers import ProviderABC, translate_at_seam
from mgf.vm.core.exceptions import (
    AppError,
    AuthError,
    DeviceAttachFailed,
    DomainNotFound,
    DomainStateInvalid,
    GuestUnresponsive,
    HypervisorConnectionError,
    HypervisorError,
    HypervisorTransientError,
    MigrationFailed,
    OperationCancelled,
    PoolExists,
    PoolNotFound,
    ResourceUnavailable,
    SnapshotExists,
    SnapshotNotFound,
    SpecInvalid,
    StorageError,
    VolumeExists,
    VolumeNotFound,
)

# ---------------------------------------------------------------------------
# virErrorNumber — the stable libvirt error-code integers.
# https://libvirt.org/html/libvirt-libvirt-common.html#virErrorNumber
# Named here (not imported from ``libvirt``) so the seam is importable
# without the binding. Values verified against libvirt-python 12.3.
# ---------------------------------------------------------------------------

VIR_ERR_INTERNAL_ERROR = 1
VIR_ERR_NO_MEMORY = 2
VIR_ERR_NO_SUPPORT = 3
VIR_ERR_UNKNOWN_HOST = 4
VIR_ERR_NO_CONNECT = 5
VIR_ERR_INVALID_CONN = 6
VIR_ERR_INVALID_DOMAIN = 7
VIR_ERR_INVALID_ARG = 8
VIR_ERR_OPERATION_FAILED = 9
VIR_ERR_XML_ERROR = 27
VIR_ERR_DOM_EXIST = 28
VIR_ERR_OPERATION_DENIED = 29
VIR_ERR_OPEN_FAILED = 30
VIR_ERR_READ_FAILED = 31
VIR_ERR_PARSE_FAILED = 32
VIR_ERR_WRITE_FAILED = 34
VIR_ERR_XML_DETAIL = 35
VIR_ERR_SYSTEM_ERROR = 38
VIR_ERR_RPC = 39
VIR_ERR_GNUTLS_ERROR = 40
VIR_ERR_NO_DOMAIN = 42
VIR_ERR_INVALID_MAC = 44
VIR_ERR_AUTH_FAILED = 45
VIR_ERR_INVALID_STORAGE_POOL = 46
VIR_ERR_INVALID_STORAGE_VOL = 47
VIR_ERR_NO_STORAGE_POOL = 49
VIR_ERR_NO_STORAGE_VOL = 50
VIR_ERR_OPERATION_INVALID = 55
VIR_ERR_CONFIG_UNSUPPORTED = 67
VIR_ERR_OPERATION_TIMEOUT = 68
VIR_ERR_MIGRATE_PERSIST_FAILED = 69
VIR_ERR_HOOK_SCRIPT_FAILED = 70
VIR_ERR_INVALID_DOMAIN_SNAPSHOT = 71
VIR_ERR_NO_DOMAIN_SNAPSHOT = 72
VIR_ERR_ARGUMENT_UNSUPPORTED = 74
VIR_ERR_STORAGE_PROBE_FAILED = 75
VIR_ERR_STORAGE_POOL_BUILT = 76
VIR_ERR_SNAPSHOT_REVERT_RISKY = 77
VIR_ERR_OPERATION_ABORTED = 78
VIR_ERR_AUTH_CANCELLED = 79
VIR_ERR_MIGRATE_UNSAFE = 81
VIR_ERR_OVERFLOW = 82
VIR_ERR_BLOCK_COPY_ACTIVE = 83
VIR_ERR_OPERATION_UNSUPPORTED = 84
VIR_ERR_SSH = 85
VIR_ERR_AGENT_UNRESPONSIVE = 86
VIR_ERR_RESOURCE_BUSY = 87
VIR_ERR_ACCESS_DENIED = 88
VIR_ERR_DBUS_SERVICE = 89
VIR_ERR_STORAGE_VOL_EXIST = 90
VIR_ERR_CPU_INCOMPATIBLE = 91
VIR_ERR_XML_INVALID_SCHEMA = 92
VIR_ERR_MIGRATE_FINISH_OK = 93
VIR_ERR_AUTH_UNAVAILABLE = 94
VIR_ERR_NO_SERVER = 95
VIR_ERR_NO_CLIENT = 96
VIR_ERR_AGENT_UNSYNCED = 97
VIR_ERR_LIBSSH = 98
VIR_ERR_DEVICE_MISSING = 99
VIR_ERR_NO_HOSTNAME = 108
VIR_ERR_MULTIPLE_DOMAINS = 110


# ---------------------------------------------------------------------------
# The code -> core-exception-class table.
#
# Each value is the *class* to instantiate. The translator builds the
# instance with a single message argument so every concrete core
# exception (which all accept ``*args`` via Exception.__init__) works.
#
# Conservative-mapping rule: codes whose meaning is genuinely ambiguous
# at the seam (e.g. OPERATION_FAILED) are deliberately ABSENT from this
# table and fall through to the generic parent in
# ``translate_exception``.
# ---------------------------------------------------------------------------

_CODE_MAP: dict[int, type[AppError]] = {
    # --- connectivity / transport (retryable) ------------------------------
    VIR_ERR_NO_CONNECT: HypervisorConnectionError,
    VIR_ERR_INVALID_CONN: HypervisorConnectionError,
    VIR_ERR_NO_SERVER: HypervisorConnectionError,
    VIR_ERR_NO_CLIENT: HypervisorConnectionError,
    VIR_ERR_RPC: HypervisorConnectionError,
    VIR_ERR_GNUTLS_ERROR: HypervisorConnectionError,
    VIR_ERR_SSH: HypervisorConnectionError,
    VIR_ERR_LIBSSH: HypervisorConnectionError,
    VIR_ERR_DBUS_SERVICE: HypervisorConnectionError,
    VIR_ERR_UNKNOWN_HOST: HypervisorConnectionError,
    VIR_ERR_NO_HOSTNAME: HypervisorConnectionError,
    # --- transient engine hiccups (retryable) ------------------------------
    VIR_ERR_OPERATION_TIMEOUT: HypervisorTransientError,
    VIR_ERR_RESOURCE_BUSY: HypervisorTransientError,
    VIR_ERR_SYSTEM_ERROR: HypervisorTransientError,
    VIR_ERR_AGENT_UNSYNCED: HypervisorTransientError,
    # --- not-found ---------------------------------------------------------
    VIR_ERR_NO_DOMAIN: DomainNotFound,
    VIR_ERR_INVALID_DOMAIN: DomainNotFound,
    VIR_ERR_NO_DOMAIN_SNAPSHOT: SnapshotNotFound,
    VIR_ERR_NO_STORAGE_POOL: PoolNotFound,
    VIR_ERR_INVALID_STORAGE_POOL: PoolNotFound,
    VIR_ERR_NO_STORAGE_VOL: VolumeNotFound,
    VIR_ERR_INVALID_STORAGE_VOL: VolumeNotFound,
    VIR_ERR_DEVICE_MISSING: DomainStateInvalid,
    # --- already-exists ----------------------------------------------------
    VIR_ERR_STORAGE_VOL_EXIST: VolumeExists,
    VIR_ERR_STORAGE_POOL_BUILT: PoolExists,
    # --- state-invalid / illegal operation ---------------------------------
    VIR_ERR_OPERATION_INVALID: DomainStateInvalid,
    VIR_ERR_OPERATION_DENIED: DomainStateInvalid,
    VIR_ERR_BLOCK_COPY_ACTIVE: DomainStateInvalid,
    VIR_ERR_MULTIPLE_DOMAINS: DomainStateInvalid,
    # --- spec / XML / config invalid (not retryable) -----------------------
    VIR_ERR_XML_ERROR: SpecInvalid,
    VIR_ERR_XML_DETAIL: SpecInvalid,
    VIR_ERR_XML_INVALID_SCHEMA: SpecInvalid,
    VIR_ERR_INVALID_ARG: SpecInvalid,
    VIR_ERR_INVALID_MAC: SpecInvalid,
    VIR_ERR_CONFIG_UNSUPPORTED: SpecInvalid,
    VIR_ERR_ARGUMENT_UNSUPPORTED: SpecInvalid,
    VIR_ERR_OPERATION_UNSUPPORTED: SpecInvalid,
    VIR_ERR_NO_SUPPORT: SpecInvalid,
    VIR_ERR_OVERFLOW: SpecInvalid,
    # --- capacity ----------------------------------------------------------
    VIR_ERR_NO_MEMORY: ResourceUnavailable,
    # --- guest agent -------------------------------------------------------
    VIR_ERR_AGENT_UNRESPONSIVE: GuestUnresponsive,
    # --- snapshot ----------------------------------------------------------
    VIR_ERR_SNAPSHOT_REVERT_RISKY: SnapshotExists,
    # --- migration ---------------------------------------------------------
    VIR_ERR_MIGRATE_PERSIST_FAILED: MigrationFailed,
    VIR_ERR_MIGRATE_UNSAFE: MigrationFailed,
    VIR_ERR_CPU_INCOMPATIBLE: MigrationFailed,
    # --- device ------------------------------------------------------------
    # Conservative: a generic device failure surfaces as attach-failed only
    # when the seam knows it was an attach; the bare ``OPERATION_FAILED``
    # is intentionally NOT mapped (falls through to HypervisorError).
    # --- storage I/O -------------------------------------------------------
    VIR_ERR_STORAGE_PROBE_FAILED: StorageError,
    VIR_ERR_OPEN_FAILED: StorageError,
    VIR_ERR_READ_FAILED: StorageError,
    VIR_ERR_WRITE_FAILED: StorageError,
    # --- cancellation ------------------------------------------------------
    VIR_ERR_OPERATION_ABORTED: OperationCancelled,
    VIR_ERR_AUTH_CANCELLED: OperationCancelled,
    # --- auth --------------------------------------------------------------
    VIR_ERR_AUTH_FAILED: AuthError,
    VIR_ERR_AUTH_UNAVAILABLE: AuthError,
    VIR_ERR_ACCESS_DENIED: AuthError,
    # --- device-attach hint (a few callers re-tag this) --------------------
    VIR_ERR_HOOK_SCRIPT_FAILED: DeviceAttachFailed,
}


def _error_code(exc: Exception) -> int | None:
    """Best-effort extraction of a libvirt error code.

    ``libvirt.libvirtError`` exposes ``get_error_code()``. We duck-type
    so this function never needs to import ``libvirt`` and works on the
    test doubles too. Any failure to read the code returns ``None`` (→
    generic parent), keeping the conservative rule.
    """
    getter = getattr(exc, "get_error_code", None)
    if not callable(getter):
        return None
    try:
        code = getter()
    except Exception:
        return None
    return code if isinstance(code, int) else None


class LibvirtErrorTranslator(ProviderABC):
    """Translate ``libvirt.libvirtError`` to the core taxonomy.

    Subclass / mix into the libvirt backend (or hold one as a
    collaborator). Backend methods decorate themselves with
    :func:`translate_at_seam`, which calls this
    :meth:`translate_exception` for any non-:class:`AppError` raised
    inside the method body and re-raises the result ``from`` the
    original.
    """

    def translate_exception(self, exc: Exception) -> AppError:
        """Map ``exc`` to a typed core :class:`AppError`.

        Pure over ``exc`` (no call-site context — see module docstring).
        Returns; never raises (the decorator does the raise-from).
        """
        # An AppError reaching here means a backend raised a typed error
        # and it got routed through translation anyway — pass it through
        # unchanged rather than wrapping a typed error in a generic one.
        if isinstance(exc, AppError):
            return exc

        code = _error_code(exc)
        message = str(exc) or exc.__class__.__name__

        if code is not None and code in _CODE_MAP:
            return _CODE_MAP[code](message)

        # Conservative fallback: a code we don't recognise (or no code at
        # all) becomes the generic hypervisor parent — correct-but-broad,
        # never a wrong specific subclass.
        return HypervisorError(message)


__all__ = [
    "LibvirtErrorTranslator",
    "translate_at_seam",
]
