"""Rename a stopped domain and update the in-memory ``StateStore``.

A definition-only operation — no files are touched. The caller owns
the backend, the rename seam, and the store; this function routes the
rename through them so the two views stay in sync.

Rename on a running domain is **refused**. Some engines support it,
but the semantics are flaky (the guest keeps thinking it is called the
old name until the next reboot), so the user has to stop the VM first.

Purity
------
The :class:`HypervisorBackend` supplies the read-only running-state
check. The engine-side rename itself is **not** part of the
:class:`HypervisorBackend` contract (``contracts.md`` §3 has no
``rename``), so this module takes the rename as an **injected** narrow
Protocol, :class:`DomainRenamer`. Orchestrating through an injected
Protocol is pure and allowed; the concrete rename (libvirt
``virDomainRename`` + redefine dance) lives in the adapter that
supplies the :class:`DomainRenamer`.

Name validation reuses the engine-neutral
:data:`mgf.vm.core.names.VM_NAME_REGEX`, so the rename path stays in
sync with the create path without duplicating the regex.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Protocol, runtime_checkable

from mgf.common.observability import operation_span

from mgf.vm.core.exceptions import (
    DomainAlreadyExistsError,
    DomainNotFound,
    OperationError,
    ProviderError,
    SpecValidationError,
)
from mgf.vm.core.names import VM_NAME_REGEX, VM_NAME_RULE, is_valid_vm_name

if TYPE_CHECKING:
    from mgf.vm.core.common.progress import ProgressReporter
    from mgf.vm.core.contracts import HypervisorBackend
    from mgf.vm.core.ids import DomainId
    from mgf.vm.core.monitor.state import StateStore


@runtime_checkable
class DomainRenamer(Protocol):
    """Narrow engine seam: rename a defined domain in place.

    Declared here (rather than reaching into the adapter Protocol)
    because :class:`~mgf.vm.core.contracts.HypervisorBackend` has no
    ``rename`` method, yet renaming through an injected Protocol keeps
    this module pure. The adapter supplies a concrete implementation;
    the fake supplies an in-memory one for tests.

    Implementations rename the domain identified by ``domain`` to
    ``new_name`` and return the (possibly new) :class:`DomainId`. They
    raise :class:`~mgf.vm.core.exceptions.DomainAlreadyExistsError`
    when ``new_name`` is taken and
    :class:`~mgf.vm.core.exceptions.DomainNotFound` when ``domain`` is
    gone.
    """

    def rename_domain(self, domain: DomainId, new_name: str) -> DomainId:
        """Rename ``domain`` to ``new_name``; return the resulting id."""
        ...


def rename(
    backend: HypervisorBackend,
    renamer: DomainRenamer,
    store: StateStore | None,
    domain: DomainId,
    old_name: str,
    new_name: str,
    *,
    reporter: ProgressReporter | None = None,
) -> DomainId:
    """Rename a stopped domain to ``new_name``.

    Args:
        backend: injected :class:`HypervisorBackend`, used only for the
            read-only running-state check.
        renamer: injected :class:`DomainRenamer` performing the engine
            rename.
        store: optional in-memory store; when supplied, the entry keyed
            under ``old_name`` is re-keyed to ``new_name`` after the
            engine accepts the rename so ``vmanager vm list`` reflects
            the new name immediately.
        domain: the current :class:`DomainId`.
        old_name: the domain's current name (the store key to re-key).
        new_name: target name. Must match :data:`VM_NAME_REGEX`.
        reporter: optional progress reporter.

    Returns:
        The :class:`DomainId` after the rename. Engines that mint a new
        id on rename return the new one; engines that keep a stable id
        return the same value.

    Raises:
        :class:`~mgf.vm.core.exceptions.SpecValidationError`: the new
            name is malformed.
        :class:`~mgf.vm.core.exceptions.DomainAlreadyExistsError`:
            another VM already has ``new_name``.
        :class:`~mgf.vm.core.exceptions.OperationError`: the VM is
            running, or the engine refused the rename.
    """
    if not is_valid_vm_name(new_name):
        raise SpecValidationError(
            [f"{new_name!r} is not a valid VM name; {VM_NAME_RULE} (regex {VM_NAME_REGEX.pattern})"]
        )

    if new_name == old_name:
        return domain  # no-op

    with operation_span(
        "vm.rename",
        domain_id=domain.value,
        old_name=old_name,
        new_name=new_name,
    ):
        try:
            info = backend.get_domain(domain)
        except DomainNotFound:
            raise
        except ProviderError as e:
            raise OperationError(f"could not read state of {old_name!r}: {e}") from e

        if info.is_running:
            raise OperationError(f"{old_name!r} is running; stop it before renaming")

        _announce(reporter, f"renaming {old_name} -> {new_name}")

        try:
            new_domain = renamer.rename_domain(domain, new_name)
        except (DomainAlreadyExistsError, DomainNotFound):
            raise
        except ProviderError as e:
            raise OperationError(f"could not rename {old_name!r} to {new_name!r}: {e}") from e

        if store is not None:
            store.rename(old_name, new_name)

        _announce(reporter, f"renamed to {new_name}")
        return new_domain


def _announce(reporter: ProgressReporter | None, message: str) -> None:
    if reporter is not None:
        reporter.report(message)


__all__ = ["DomainRenamer", "rename"]
