"""Core domain types used across every VManager module.

This module holds the small, frozen data types that flow between modules:
:class:`VMHandle`, :class:`VMStatus`, :class:`VMMetrics`, :class:`HostMetrics`,
:class:`SnapshotInfo`, and :class:`VMState`. These types live in
``mgf.vm.domain`` (not ``mgf.vm.monitor`` or ``mgf.vm.providers``) so that
both the provider layer and the monitor layer can import them without creating
a circular dependency.

See ``docs/ARCHITECTURE.md`` under "Cross-Cutting Modules — ``vmanager/domain.py``"
for the documented design.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum, StrEnum
from typing import TYPE_CHECKING, Literal

if TYPE_CHECKING:
    from datetime import datetime


class VMStatus(Enum):
    """Power state of a VM at a single point in time.

    Values match the libvirt string representation so ``VMStatus(libvirt_str)``
    round-trips cleanly.
    """

    RUNNING = "running"
    PAUSED = "paused"
    STOPPED = "shut off"
    CRASHED = "crashed"
    UNKNOWN = "unknown"

    @classmethod
    def from_libvirt(cls, value: str) -> VMStatus:
        """Parse a libvirt status string, returning ``UNKNOWN`` on mismatch.

        libvirt's textual forms ("running", "shut off", "paused", "crashed")
        map directly onto the enum values, but older libvirt versions
        sometimes emit alternative spellings ("shutdown", "shut-off"). We
        normalise those here so callers never have to guess.
        """
        if not value:
            return cls.UNKNOWN
        normalised = value.strip().lower()
        # Direct match on the enum value.
        for member in cls:
            if member.value == normalised:
                return member
        # Known alternative spellings observed in the wild.
        aliases: dict[str, VMStatus] = {
            "shutdown": cls.STOPPED,
            "shut-off": cls.STOPPED,
            "shutoff": cls.STOPPED,
            "pmsuspended": cls.PAUSED,
            "dying": cls.CRASHED,
            "blocked": cls.RUNNING,  # libvirt emits "blocked" for running-but-stuck
        }
        return aliases.get(normalised, cls.UNKNOWN)


@dataclass(frozen=True)
class VMHandle:
    """Lightweight resolved reference to a VM.

    Passed between modules instead of a bare name string. ``uuid`` is
    resolved once at creation time (via ``VMProvider.get_domain``) so
    downstream code does not re-query libvirt for every lookup.

    ``state`` is the state observed at resolution time and is **display
    only**. Any operation that needs live state must call
    ``provider.domain_state(handle)`` — never read ``handle.state``. This
    is documented both here and in ``docs/ARCHITECTURE.md`` so the rule
    survives future refactoring.
    """

    name: str
    uuid: str
    state: VMStatus


@dataclass(frozen=True)
class SnapshotInfo:
    """Metadata about a single VM snapshot."""

    name: str
    description: str
    created_at: datetime
    parent_name: str | None
    state: VMStatus


@dataclass
class VMMetrics:
    """One resource sample for a single VM.

    Produced by ``VMProvider.domain_metrics(handle)`` and consumed by
    ``monitor/metrics.py``. Lives in this module (not ``monitor/``) so that
    the provider layer can return it without importing upward.

    ``ram_used_mb_guest`` is ``None`` when no QEMU guest agent is available
    for the domain; consumers should prefer it over ``ram_used_mb`` when
    present because host-side RAM usage under balloon pressure is
    misleading.
    """

    cpu_pct: float
    ram_used_mb: int
    ram_used_mb_guest: int | None
    disk_read_bps: int
    disk_write_bps: int
    disk_actual_bytes: int
    net_rx_bps: int
    net_tx_bps: int
    sampled_at: datetime


@dataclass
class HostMetrics:
    """Snapshot of host-level resource usage and allocation.

    Produced by ``monitor/host.py`` and shown in the dashboard header. The
    ``vcpus_allocated`` and ``ram_allocated_mb`` totals are computed from
    ``StateStore``, not libvirt, so they stay consistent with the VM list
    the user is looking at.
    """

    cpu_logical: int
    cpu_pct: float
    ram_total_mb: int
    ram_used_mb: int
    vcpus_allocated: int
    ram_allocated_mb: int
    sampled_at: datetime


@dataclass
class VMState:
    """Live state entry held by :class:`monitor.state.StateStore`.

    Unlike :class:`VMHandle`, ``VMState`` is mutable and is updated in
    place by libvirt domain-event callbacks on a background thread. Always
    protected by ``StateStore``'s internal lock when read or written.
    """

    handle: VMHandle
    status: VMStatus
    ip: str | None = None
    uptime_seconds: int | None = None
    last_state_change: datetime | None = None
    previous_status: VMStatus | None = None


# ---------------------------------------------------------------------------
# Credentials
# ---------------------------------------------------------------------------

AuthType = Literal["key", "password"]


@dataclass(frozen=True)
class VMCredential:
    """One VM's SSH authentication record.

    ``key_path`` is set when ``auth_type == 'key'`` and is the absolute
    path to the private key file (not its contents). ``password`` is set
    when ``auth_type == 'password'`` and is the plaintext secret — it is
    never written to disk unless the keyring layer fell back to the
    encrypted file backend, in which case it goes through the Fernet
    cipher.

    Lives here (not in ``mgf.vm.vault``) as a lingua-franca domain type
    so both ``common.ssh`` and ``vault.keyring`` can reference it without
    a circular import.
    """

    vm_name: str
    username: str
    auth_type: AuthType
    key_path: str | None = None
    password: str | None = None

    def __post_init__(self) -> None:
        if self.auth_type == "key" and not self.key_path:
            raise ValueError(f"{self.vm_name!r}: auth_type='key' requires key_path")
        if self.auth_type == "password" and not self.password:
            raise ValueError(f"{self.vm_name!r}: auth_type='password' requires password")


# ---------------------------------------------------------------------------
# Isolation posture
# ---------------------------------------------------------------------------


class IsolationLevel(StrEnum):
    """Declared isolation posture for a VM (see ``docs/MALWARE_ANALYSIS.md`` §3.1).

    - ``OFF`` — no isolation enforcement (the default; no behavioural
      change for callers that don't opt in).
    - ``STRICT`` — malware-analysis isolation: every isolation check is
      enforced and auto-fixes apply on demand. Suitable for
      live-malware detonation.
    - ``AIRGAP`` — STRICT plus device-stripping (no NIC, USB, video
      display); serial console + RNG only.
    """

    OFF = "off"
    STRICT = "strict"
    AIRGAP = "airgap"


# Types that describe host-side infrastructure objects. Kept here (rather than
# ``network/types.py`` / ``storage/types.py``) at the foundation layer so the
# ``VMProvider`` ABC can reference them without pulling in the full
# ``network/`` or ``storage/`` packages before Phase 4.


@dataclass(frozen=True)
class NetworkInfo:
    """Libvirt virtual network as reported by ``VMProvider.list_networks()``."""

    name: str
    uuid: str
    active: bool
    autostart: bool
    bridge_name: str | None = None
    forward_mode: str | None = None  # "nat", "bridge", "isolated", "route", None


@dataclass(frozen=True)
class LeaseInfo:
    """A single DHCP lease observed on a libvirt network."""

    mac: str
    ip: str
    hostname: str | None = None
    vm_name: str | None = None


@dataclass(frozen=True)
class StoragePoolInfo:
    """Libvirt storage pool summary.

    ``capacity_bytes``, ``allocation_bytes``, and ``available_bytes`` are
    reported by libvirt as of the most recent ``refresh_pool`` call and
    should be treated as freshness-limited.
    """

    name: str
    path: str
    capacity_bytes: int
    allocation_bytes: int
    available_bytes: int
    active: bool
    volumes: tuple[str, ...] = field(default_factory=tuple)


@dataclass(frozen=True)
class VolumeInfo:
    """A single storage volume inside a libvirt pool.

    Produced by :meth:`VMProvider.list_pool_volumes`. Phase 4
    introduces this type at the planned phase boundary so that
    ``vmanager storage info`` and ``vmanager storage list`` can
    show per-volume stats rather than only per-pool totals.
    """

    pool: str
    name: str
    path: str
    capacity_bytes: int
    allocation_bytes: int


__all__ = [
    "AuthType",
    "HostMetrics",
    "IsolationLevel",
    "LeaseInfo",
    "NetworkInfo",
    "SnapshotInfo",
    "StoragePoolInfo",
    "VMCredential",
    "VMHandle",
    "VMMetrics",
    "VMState",
    "VMStatus",
    "VolumeInfo",
]
