"""``MonitorHistory`` — the pure persistence seam for the monitoring slice.

This module is the pure-core contribution from ``mgf-vm``'s
``monitor`` subsystem. ``mgf-vm`` ships a SQLite-backed ``History``
store plus the orchestrators that drive it (``EventSubscriber``,
``MetricsPoller``, ``run_maintenance``). Those are a monitoring
**service**: they orchestrate the engine provider plus a concrete
sqlite file and therefore cannot move into the pure core — the
purity contract (D-B2, enforced by import-linter) forbids
sqlite/file-I/O/libvirt/psutil here.

What *does* belong in core (D-G10) is the typed **seam** consumers
program against and the pure value types that flow across it:

* :class:`MonitorHistory` — an abstract ``typing.Protocol`` capturing
  the public persistence surface of ``History``, typed against core
  domain types (:class:`~mgf.vm.core.domain.VMMetrics`,
  :class:`~mgf.vm.core.domain.VMStatus`,
  :class:`~mgf.vm.core.domain.VMHandle`);
* :class:`EventRow` and :class:`HourlyMetrics` — the pure frozen
  row types returned by the query methods.

The concrete sqlite ``History`` and the orchestrators live in the
service/adapter layer (``vm-vmanager-app`` or a dedicated monitor
adapter) and consume this Protocol. A first-class in-memory
implementation, :class:`~mgf.vm.core.monitor.fake_history.FakeMonitorHistory`,
ships in core the same way the engine-Protocol fakes do (D-F3).

Storage-specific durability semantics (WAL mode, ``synchronous=FULL``
on the audit-event write path, schema-version gating) are a property
of the concrete sqlite ``History`` — they are deliberately NOT part of
this behavioural contract.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable

if TYPE_CHECKING:
    from collections.abc import Iterable
    from datetime import datetime

    from mgf.vm.core.domain import VMHandle, VMMetrics, VMStatus


#: Category filter accepted by :meth:`MonitorHistory.query_events`.
EventFilter = Literal["all", "lifecycle", "config"]


@dataclass(frozen=True)
class EventRow:
    """One recorded audit event.

    ``from_state`` / ``to_state`` are ``None`` for config-change rows.
    ``detail`` is a free-text human summary — ``"vcpus 2→4"``,
    ``"disk vdb +20G"``, ``"nic aa:bb:cc:dd:ee:ff detached"``, etc.

    The state columns are stored as plain strings (the
    :class:`~mgf.vm.core.domain.VMStatus` *value*, e.g. ``"running"``)
    so a row read back from any backend round-trips without depending
    on the enum identity of the writer.
    """

    vm_name: str
    occurred_at: datetime
    from_state: str | None
    to_state: str | None
    trigger: str
    detail: str


@dataclass(frozen=True)
class HourlyMetrics:
    """One hourly rollup row.

    Each row aggregates every raw :class:`~mgf.vm.core.domain.VMMetrics`
    sample for a single VM within a single whole-hour bucket
    (``hour`` is the top-of-hour UTC timestamp; samples from
    ``[hour, hour+1h)`` roll up into this row).

    Rolls up six metrics (``cpu_pct``, ``ram_used_mb``,
    ``disk_read_bps``, ``disk_write_bps``, ``net_rx_bps``,
    ``net_tx_bps``) with both average and max so callers can answer
    "how busy was this VM over the last 24 hours?" and "how spiky"
    separately. ``ram_used_mb_guest`` and ``disk_actual_bytes`` are
    deliberately NOT rolled up:

    * ``ram_used_mb_guest`` is ``None`` whenever the guest agent is
      unavailable — aggregating a mostly-null column across an hour
      would yield ``None`` averages that the UI would have to
      special-case.
    * ``disk_actual_bytes`` is a file size, not a rate; its trend is
      readable from the latest raw sample alone.
    """

    vm_name: str
    hour: datetime
    sample_count: int
    cpu_pct_avg: float
    cpu_pct_max: float
    ram_used_mb_avg: float
    ram_used_mb_max: int
    disk_read_bps_avg: float
    disk_read_bps_max: int
    disk_write_bps_avg: float
    disk_write_bps_max: int
    net_rx_bps_avg: float
    net_rx_bps_max: int
    net_tx_bps_avg: float
    net_tx_bps_max: int


@runtime_checkable
class MonitorHistory(Protocol):
    """The persistence seam for the monitoring slice (D-G10).

    Captures the public surface of ``mgf-vm``'s sqlite ``History``
    without committing consumers to any particular storage backend.
    Implementations record audit events + metric samples and answer
    queries over them; the concrete sqlite implementation, the
    in-memory :class:`FakeMonitorHistory`, and a future Postgres
    backend all satisfy this same contract.

    Implementations MUST be thread-safe — engine event callbacks
    record on a background thread while the CLI / GUI main thread
    queries (the same reason ``StateStore`` is thread-safe).
    """

    # ------------------------------------------------------------------
    # Audit events
    # ------------------------------------------------------------------

    def record_lifecycle(
        self,
        vm_name: str,
        from_state: VMStatus | None,
        to_state: VMStatus,
        *,
        trigger_source: str = "user",
    ) -> None:
        """Append a ``lifecycle`` event row.

        ``trigger_source`` is recorded as the row ``detail`` so callers
        can show ``"started (managed-save)"`` etc. without a new
        column. ``from_state`` is ``None`` for a newly-defined domain.
        """
        ...

    def record_config_change(
        self,
        handle: VMHandle,
        subsystem: str,
        detail: str,
    ) -> None:
        """Append a ``config_change:<subsystem>`` event row.

        Called after a ``configurator/*.apply()`` succeeds.
        ``subsystem`` is one of ``cpu``, ``memory``, ``storage``,
        ``network``, ``display``, ``shares``, ``passthrough``,
        ``firmware``, ``metadata``. ``detail`` is a one-line human
        summary like ``"vcpus 2→4"``.
        """
        ...

    def query_events(
        self,
        vm_name: str | None = None,
        *,
        since: datetime | None = None,
        filter: EventFilter = "all",  # noqa: A002 — matches the CLI flag name
        limit: int | None = None,
    ) -> list[EventRow]:
        """Return event rows matching ``vm_name`` / ``since`` / ``filter``.

        ``vm_name=None`` returns events across every VM. ``since``
        filters to events at or after the timestamp (naïve datetimes
        are treated as UTC). ``filter`` is ``"all"`` (both kinds),
        ``"lifecycle"``, or ``"config"``. Rows are ordered
        newest-first.
        """
        ...

    # ------------------------------------------------------------------
    # Raw metrics
    # ------------------------------------------------------------------

    def record_metrics(self, vm_name: str, metrics: VMMetrics) -> None:
        """Append one :class:`VMMetrics` sample for ``vm_name``."""
        ...

    def record_metrics_batch(self, samples: Iterable[tuple[str, VMMetrics]]) -> None:
        """Append every ``(vm_name, metrics)`` sample in one transaction.

        ``samples`` is consumed once and may be any iterable; empty
        input is a no-op.
        """
        ...

    def query_metrics(
        self,
        vm_name: str,
        *,
        since: datetime | None = None,
        limit: int | None = None,
    ) -> list[VMMetrics]:
        """Return raw samples for ``vm_name`` newest-first."""
        ...

    def prune_metrics_older_than(self, cutoff: datetime) -> int:
        """Delete every raw sample with ``sampled_at < cutoff``.

        Returns the number of rows removed.
        """
        ...

    # ------------------------------------------------------------------
    # Hourly rollup
    # ------------------------------------------------------------------

    def rollup_hourly_metrics(
        self,
        *,
        since: datetime | None = None,
        now: datetime | None = None,
    ) -> int:
        """Aggregate raw metrics into hourly buckets.

        Only whole completed hours are rolled up (the in-progress hour
        is excluded). Idempotent on ``(vm_name, hour)``. Returns the
        number of rollup rows written or replaced.
        """
        ...

    def query_metrics_hourly(
        self,
        vm_name: str,
        *,
        since: datetime | None = None,
        limit: int | None = None,
    ) -> list[HourlyMetrics]:
        """Return hourly rollup rows for ``vm_name`` newest-first."""
        ...

    def prune_metrics_hourly_older_than(self, cutoff: datetime) -> int:
        """Delete every hourly rollup row with ``hour < cutoff``.

        Returns the number of rows removed.
        """
        ...

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def close(self) -> None:
        """Release any backing resources. Idempotent."""
        ...


__all__ = [
    "EventFilter",
    "EventRow",
    "HourlyMetrics",
    "MonitorHistory",
]
