"""Typed exception hierarchy for VManager.

Built on :mod:`mgf.common.exceptions`: every category base is the
mgf.common one (so a future ``mgf.cli`` or sibling project catches
the same things), and every VManager-specific concrete subclasses
the corresponding mgf.common category.

:class:`VManagerError` is an alias for :class:`mgf.common.exceptions.AppError`
so existing ``except VManagerError:`` call sites keep working unchanged.

The CLI entry point catches :class:`VManagerError` as a general fallback
but also matches specific subtypes to produce precise error messages and
distinct exit codes. GUI workers catch at the :class:`VManagerError` level
and emit ``error_signal(exc)``; the GUI then inspects the type to choose
the appropriate dialog.

The hierarchy is shallow but typed — enough for callers to distinguish
*categories* of failure without needing to pattern-match on error messages
or libvirt error codes.

See ``docs/ARCHITECTURE.md`` under "Exception Hierarchy" for the full
documented structure and ``docs/standards/ERROR_HANDLING.md`` for the
cross-project rules this hierarchy implements.
"""

from __future__ import annotations

from mgf.common.exceptions import (
    AppConfigError,
    AppError,
    CommandError,
    ConfigError,
    GuestCommandError,
    GuestError,
    GuestUnreachableError,
    OperationError,
    ProviderError,
    SchedulerError,
    SchemaValidationError,
    VaultError,
)
from mgf.common.exceptions import (
    HostEnvironmentError as _CommonEnvironmentError,
)

# ---------------------------------------------------------------------------
# Project base — alias kept for source-compat with every existing
# ``except VManagerError:`` call site.
# ---------------------------------------------------------------------------

VManagerError = AppError

# Re-export the host-environment category under the local module so
# `from mgf.vm.exceptions import EnvironmentError` keeps working.
# Upstream renamed the class from ``EnvironmentError`` to
# ``HostEnvironmentError`` to stop shadowing Python's built-in alias for
# ``OSError`` (mgf-common 0.2+; deprecated alias removed). VManager
# keeps its local ``EnvironmentError`` public name because it lands in
# ``__all__`` below — the ``noqa: A001`` covers the deliberate shadow.
EnvironmentError = _CommonEnvironmentError  # noqa: A001 — shadows builtin alias on purpose

__all_categories__ = [
    "AppConfigError",
    "CommandError",
    "ConfigError",
    "EnvironmentError",
    "GuestCommandError",
    "GuestError",
    "GuestUnreachableError",
    "OperationError",
    "ProviderError",
    "SchedulerError",
    "SchemaValidationError",
    "VManagerError",
    "VaultError",
]


# ---------------------------------------------------------------------------
# Configuration / spec validation
# ---------------------------------------------------------------------------


class SpecValidationError(SchemaValidationError):
    """A :class:`VMSpec` or :class:`LabSpec` failed validation.

    Always carries the *complete* list of errors found during validation so
    users see every problem at once instead of fixing one, re-running, and
    finding the next.

    Subclasses :class:`mgf.common.exceptions.SchemaValidationError` but
    overrides ``__init__`` so the message keeps the historical
    ``"spec validation failed:"`` prefix every existing test pins.
    """

    def __init__(self, errors: list[str]) -> None:
        if not errors:
            raise ValueError("SpecValidationError requires at least one error message")
        # Bypass SchemaValidationError.__init__ so we can supply our
        # own message prefix; AppError takes the message via its
        # plain Exception.__init__.
        self.errors: list[str] = list(errors)
        joined = "; ".join(errors)
        AppError.__init__(self, f"spec validation failed: {joined}")


class LabSpecError(ConfigError):
    """Lab spec YAML is invalid (circular ``uses:``, unknown template, etc.)."""


# ---------------------------------------------------------------------------
# Provider / libvirt failures
# ---------------------------------------------------------------------------


class DomainNotFoundError(ProviderError):
    """A domain name or UUID did not resolve to any known VM."""

    RETRYABLE: bool = False
    """Contracts-aligned retryability tag (§7.4). Aliased as
    ``DomainNotFound``."""

    def __init__(self, name: str) -> None:
        self.name: str = name
        # Tests rely on the "no VM named X" substring. The message is
        # intentionally consumer-agnostic — the CLI/GUI adds its own
        # action hint when rendering (per rule EH-09).
        super().__init__(f"no VM named {name!r}")


class DomainAlreadyExistsError(ProviderError):
    """A create operation collided with an existing domain."""

    def __init__(self, name: str) -> None:
        self.name: str = name
        # Tests rely on the "already exists" substring. The consumer
        # adds its own recovery hint when rendering (per rule EH-09).
        super().__init__(f"a VM named {name!r} already exists; pick a different name")


class HotplugNotSupportedError(ProviderError):
    """A config change cannot be applied live and requires a shutdown.

    The message should tell the user what to do next (e.g. ``"stop the VM
    and retry, or pass --next-boot"``).
    """


# ---------------------------------------------------------------------------
# Operation / runtime failures
# ---------------------------------------------------------------------------


class CreationError(OperationError):
    """``virt-install`` (or the equivalent creation path) failed."""


class SnapshotError(OperationError):
    """A snapshot create/revert/delete failed."""


class CloneError(OperationError):
    """A full or linked clone operation failed."""


class DiskOperationError(OperationError):
    """A ``qemu-img`` invocation failed."""


class BackupError(OperationError):
    """Backup export or restore failed."""


class MigrationError(OperationError):
    """A VM migration (offline or live) failed."""


class ImportExportError(OperationError):
    """OVA/OVF import or export failed."""


class CustomizeError(OperationError):
    """Offline disk customisation via ``virt-customize`` failed.

    Raised by :mod:`mgf.vm.operations.customize` for any failure
    in the customisation pipeline: refuse-running, missing
    ``virt-customize`` binary, non-zero exit from the subprocess,
    or a malformed :class:`CustomizeOp` reaching the builder.
    """


# ---------------------------------------------------------------------------
# Guest / SSH / in-guest failures
# ---------------------------------------------------------------------------


class HealthCheckError(GuestError):
    """A configured health check (TCP/SSH/HTTP) timed out."""


# ---------------------------------------------------------------------------
# Scheduler / hooks
# ---------------------------------------------------------------------------


class HookExecutionError(SchedulerError):
    """A hook script returned non-zero.

    Logged by the executor but never propagated to the triggering operation —
    hooks are deliberately fire-and-forget per the design in
    ``docs/ARCHITECTURE.md`` under ``vmanager/hooks/``.
    """


# ---------------------------------------------------------------------------
# Vault / credential storage
# ---------------------------------------------------------------------------


class KeyringUnavailableError(VaultError):
    """libsecret / GNOME Keyring is not available; vault fell back to file."""


# ---------------------------------------------------------------------------
# Host environment / pre-flight
# ---------------------------------------------------------------------------


class VirtualizationNotSupportedError(EnvironmentError):
    """Host CPU does not expose ``vmx``/``svm``."""


class KernelModuleError(EnvironmentError):
    """Loading a required kernel module (``kvm`` / ``kvm_intel`` / ``kvm_amd``) failed."""


# ===========================================================================
# Contracts-aligned hierarchy per `vm-vmanager-docs/component-specs/contracts.md`
# §7.
#
# These are the typed exceptions adapters MUST raise (per EH-02
# translation-at-seam) and that the SDK rehydrates from
# ``vmanager.api.v1.Error`` on the network seam (D-G3).
#
# Every class declares ``RETRYABLE: bool`` so the SDK + reconciler
# can decide retry-with-backoff vs. needs-human without string parsing.
# ===========================================================================

# Root alias — contracts.md uses the single-cap `VmManagerError`.
# The legacy `VManagerError` (already aliased to `AppError` above)
# remains the canonical public name for backward compat.
VmManagerError = AppError


# ---------------------------------------------------------------------------
# HypervisorError subtree
# ---------------------------------------------------------------------------


class HypervisorError(ProviderError):
    """Catch-all category for hypervisor-side failures (contracts.md §7)."""

    RETRYABLE: bool = False


class HypervisorConnectionError(HypervisorError):
    """Cannot reach the hypervisor engine (libvirt daemon down, broker socket missing)."""

    RETRYABLE: bool = True


class HypervisorTransientError(HypervisorError):
    """Engine reported a transient failure; retry with backoff."""

    RETRYABLE: bool = True


# Contracts-aligned alias for the legacy `DomainNotFoundError`.
DomainNotFound = DomainNotFoundError


class DomainStateInvalid(HypervisorError):
    """Operation is illegal in the current domain state.

    e.g., ``start`` on a domain already running with conflicting config,
    or ``snapshot --include-memory`` on a stopped domain.
    """

    RETRYABLE: bool = False


class SpecInvalid(HypervisorError):
    """The supplied ``VmSpec`` is malformed (missing required field, invalid range)."""

    RETRYABLE: bool = False


class SpecConflict(HypervisorError):
    """A domain with this name already exists with a *different* spec.

    ``define()`` is idempotent on same-spec; this raises when the spec
    differs and the caller must explicitly ``redefine()``.
    """

    RETRYABLE: bool = False


class ResourceUnavailable(HypervisorError):
    """Host lacks the CPU/RAM/storage capacity to define or start this VM."""

    RETRYABLE: bool = True
    """Retryable in the sense that capacity may free up; not retryable
    within the same operation."""


class GuestUnresponsive(HypervisorError):
    """Graceful operation (e.g., ``stop`` without ``force``) timed out
    waiting for the guest."""

    RETRYABLE: bool = True
    """Caller may retry with ``force=True``."""


class DeviceAttachFailed(HypervisorError):
    """Hypervisor refused to attach the device to the guest."""

    RETRYABLE: bool = False


class DeviceDetachFailed(HypervisorError):
    """Hypervisor refused to detach the device from the guest."""

    RETRYABLE: bool = False


class MigrationIncompatible(HypervisorError):
    """Destination host cannot host this VM (CPU features, capabilities,
    capacity).

    Raised *before* any state moves — the source remains running.
    """

    RETRYABLE: bool = False


class MigrationFailed(HypervisorError):
    """Live migration broke partway through.

    The exception payload carries the explicit state of both sides
    (source state, destination state, last successful checkpoint).
    """

    RETRYABLE: bool = False


class MigrationCancelled(HypervisorError):
    """Caller cancelled the migration mid-flight; rollback succeeded."""

    RETRYABLE: bool = False


class SnapshotExists(HypervisorError):
    """A snapshot with this name exists but with different content.

    ``snapshot()`` is idempotent on same-name; this raises when the
    user requests a re-snapshot with conflicting parameters.
    """

    RETRYABLE: bool = False


class SnapshotNotFound(HypervisorError):
    """No such snapshot for this domain."""

    RETRYABLE: bool = False


class SnapshotInUse(HypervisorError):
    """Snapshot has derived snapshots; cannot be deleted."""

    RETRYABLE: bool = False


class SnapshotHasChildren(SnapshotInUse):
    """Alias of :class:`SnapshotInUse` for the volume case."""

    RETRYABLE: bool = False


# ---------------------------------------------------------------------------
# NetworkError subtree (virtual-network management; D-G15)
# ---------------------------------------------------------------------------


class NetworkError(HypervisorError):
    """Catch-all category for virtual-network management failures (D-G15)."""

    RETRYABLE: bool = False


class NetworkNotFound(NetworkError):
    """A network name did not resolve to any known virtual network."""

    RETRYABLE: bool = False


class NetworkExists(NetworkError):
    """A define collided with an existing virtual network of the same name."""

    RETRYABLE: bool = False


# ---------------------------------------------------------------------------
# StorageError subtree
# ---------------------------------------------------------------------------


class StorageError(OperationError):
    """Catch-all category for storage-side failures (contracts.md §7)."""

    RETRYABLE: bool = False


class StorageTransientError(StorageError):
    """Storage backend reported a transient failure; retry with backoff."""

    RETRYABLE: bool = True


class PoolNotFound(StorageError):
    """No such storage pool."""

    RETRYABLE: bool = False


class PoolExists(StorageError):
    """A pool with this name exists but with a different spec."""

    RETRYABLE: bool = False


class PoolNotEmpty(StorageError):
    """Pool contains volumes and ``force=False``; refuse delete."""

    RETRYABLE: bool = False


class VolumeNotFound(StorageError):
    """No such volume."""

    RETRYABLE: bool = False


class VolumeExists(StorageError):
    """A volume with this name exists but with a different spec."""

    RETRYABLE: bool = False


class VolumeInUse(StorageError):
    """Volume is attached to a running VM; cannot be deleted or shrunk."""

    RETRYABLE: bool = False


class VolumeHasSnapshots(StorageError):
    """Volume has derived snapshots; cannot be deleted without cascade."""

    RETRYABLE: bool = False


class InsufficientCapacity(StorageError):
    """Pool lacks capacity for the requested allocation."""

    RETRYABLE: bool = True
    """Capacity may free up; retry policy decides."""


class ResizeUnsupported(StorageError):
    """The backend does not support the requested resize (e.g., shrink)."""

    RETRYABLE: bool = False


class FormatUnsupported(StorageError):
    """The backend does not support the requested disk format."""

    RETRYABLE: bool = False


# ---------------------------------------------------------------------------
# DeviceError subtree (broker / passthrough)
# ---------------------------------------------------------------------------


class DeviceError(OperationError):
    """Catch-all category for device-passthrough failures (contracts.md §7)."""

    RETRYABLE: bool = False


class BrokerConnectionError(DeviceError):
    """Cannot reach the ``mgf-hrb`` broker socket."""

    RETRYABLE: bool = True


class BrokerTransientError(DeviceError):
    """Broker reported a transient failure; retry with backoff."""

    RETRYABLE: bool = True


class DeviceNotFound(DeviceError):
    """The device URN does not correspond to any device on this host."""

    RETRYABLE: bool = False


class DeviceContended(DeviceError):
    """Device is already leased by another client.

    Caller may wait (the broker emits a ``LeaseEvent`` when the
    current lease releases).
    """

    RETRYABLE: bool = True


class DeviceNotPermitted(DeviceError):
    """Capability scope violation — this host's capability does not
    authorize touching this device."""

    RETRYABLE: bool = False


class HardwareFault(DeviceError):
    """Broker reports a physical fault on the device."""

    RETRYABLE: bool = False


class LeaseNotFound(DeviceError):
    """No such lease (either never issued or already released)."""

    RETRYABLE: bool = False


class LeaseExpired(DeviceError):
    """Lease was reclaimed by the broker (liveness fence fired).

    The client must re-claim before continuing.
    """

    RETRYABLE: bool = False


class DestUnreachable(DeviceError):
    """Lease-transfer destination host is unreachable."""

    RETRYABLE: bool = True


# ---------------------------------------------------------------------------
# TransportError subtree (SDK / gRPC)
# ---------------------------------------------------------------------------


class TransportError(VmManagerError):
    """Catch-all for SDK transport / gRPC failures (contracts.md §7)."""

    RETRYABLE: bool = False


class TransportConnectionError(TransportError):
    """Cannot connect to the remote control plane."""

    RETRYABLE: bool = True


class TransportTimeout(TransportError):
    """Operation exceeded the configured deadline."""

    RETRYABLE: bool = True


class TransportCancelled(TransportError):
    """Caller cancelled the operation."""

    RETRYABLE: bool = False


class TransportProtocolError(TransportError):
    """Wire-protocol violation — version mismatch, malformed message."""

    RETRYABLE: bool = False


# ---------------------------------------------------------------------------
# AuthError subtree
# ---------------------------------------------------------------------------


class AuthError(VmManagerError):
    """Catch-all for authentication / authorization failures (contracts.md §7)."""

    RETRYABLE: bool = False


class AuthenticationFailed(AuthError):
    """Credentials presented are invalid or expired."""

    RETRYABLE: bool = False


class AuthorizationDenied(AuthError):
    """Caller authenticated but lacks permission for this operation."""

    RETRYABLE: bool = False


class CapabilityExpired(AuthError):
    """Capability token's TTL elapsed; caller must re-issue."""

    RETRYABLE: bool = False


# ---------------------------------------------------------------------------
# SchedulerError subtree extensions
# (the legacy SchedulerError base is imported from mgf.common above)
# ---------------------------------------------------------------------------


class NoHostAvailable(SchedulerError):
    """The scheduler could not find a host matching all hard constraints."""

    RETRYABLE: bool = True
    """Capacity may free up; retry policy decides."""


class ConstraintUnsatisfiable(SchedulerError):
    """The constraint set is internally unsatisfiable (e.g., anti-affinity
    on a single-host fleet)."""

    RETRYABLE: bool = False


class PolicyViolation(SchedulerError):
    """Operation violates a tenant policy (quota, RBAC, anti-affinity)."""

    RETRYABLE: bool = False


# ---------------------------------------------------------------------------
# RegistryError subtree (image registry — Phase 2)
# ---------------------------------------------------------------------------


class RegistryError(OperationError):
    """Catch-all category for image-registry failures (contracts.md §7)."""

    RETRYABLE: bool = False


class ImageNotFound(RegistryError):
    """No such image in the registry catalog."""

    RETRYABLE: bool = False


class ImageChecksumMismatch(RegistryError):
    """Pulled image bytes don't match the catalog's expected digest.

    Indicates either a transport corruption (retryable) or a tampered
    blob (not retryable). Caller decides based on context.
    """

    RETRYABLE: bool = False


class ImageInUse(RegistryError):
    """Image is referenced by at least one VM; cannot be deleted."""

    RETRYABLE: bool = False


# ---------------------------------------------------------------------------
# Operation-cancellation primitive
# ---------------------------------------------------------------------------


class OperationCancelled(VmManagerError):
    """Long-running operation cancelled by the caller via ``CancellationToken``.

    Per contracts.md §9: adapters raise this *after* attempting clean
    rollback. If rollback failed (the operation crossed an irreversibility
    threshold), the operation may complete successfully and this is *not*
    raised — "success wins."
    """

    RETRYABLE: bool = False


__all__ = [
    # Legacy / mgf-common-rooted hierarchy
    "AppConfigError",
    "AppError",
    # AuthError subtree
    "AuthError",
    "AuthenticationFailed",
    "AuthorizationDenied",
    "BackupError",
    # DeviceError subtree
    "BrokerConnectionError",
    "BrokerTransientError",
    "CapabilityExpired",
    "CloneError",
    "CommandError",
    "ConfigError",
    # SchedulerError additions
    "ConstraintUnsatisfiable",
    "CreationError",
    "CustomizeError",
    "DestUnreachable",
    # HypervisorError subtree
    "DeviceAttachFailed",
    "DeviceContended",
    "DeviceDetachFailed",
    "DeviceError",
    "DeviceNotFound",
    "DeviceNotPermitted",
    "DiskOperationError",
    "DomainAlreadyExistsError",
    "DomainNotFound",
    "DomainNotFoundError",
    "DomainStateInvalid",
    "EnvironmentError",
    # StorageError subtree
    "FormatUnsupported",
    "GuestCommandError",
    "GuestError",
    "GuestUnreachableError",
    "GuestUnresponsive",
    "HardwareFault",
    "HealthCheckError",
    "HookExecutionError",
    "HotplugNotSupportedError",
    "HypervisorConnectionError",
    "HypervisorError",
    "HypervisorTransientError",
    # RegistryError subtree
    "ImageChecksumMismatch",
    "ImageInUse",
    "ImageNotFound",
    "ImportExportError",
    "InsufficientCapacity",
    "KernelModuleError",
    "KeyringUnavailableError",
    "LabSpecError",
    "LeaseExpired",
    "LeaseNotFound",
    "MigrationCancelled",
    "MigrationError",
    "MigrationFailed",
    "MigrationIncompatible",
    "NetworkError",
    "NetworkExists",
    "NetworkNotFound",
    "NoHostAvailable",
    # Cancellation
    "OperationCancelled",
    "OperationError",
    "PolicyViolation",
    "PoolExists",
    "PoolNotEmpty",
    "PoolNotFound",
    "ProviderError",
    "RegistryError",
    "ResizeUnsupported",
    "ResourceUnavailable",
    "SchedulerError",
    "SchemaValidationError",
    "SnapshotError",
    "SnapshotExists",
    "SnapshotHasChildren",
    "SnapshotInUse",
    "SnapshotNotFound",
    "SpecConflict",
    "SpecInvalid",
    "SpecValidationError",
    "StorageError",
    "StorageTransientError",
    # TransportError subtree
    "TransportCancelled",
    "TransportConnectionError",
    "TransportError",
    "TransportProtocolError",
    "TransportTimeout",
    "VManagerError",
    "VaultError",
    "VirtualizationNotSupportedError",
    # Contracts-aligned root alias
    "VmManagerError",
    "VolumeExists",
    "VolumeHasSnapshots",
    "VolumeInUse",
    "VolumeNotFound",
]
