"""Well-known exception categories + generic concretes.

Categories (8) — every consumer-project exception falls under one of
these so callers can match on a category without enumerating every
concrete subclass:

  * :class:`ConfigError`           — config schema / validation failures
  * :class:`ProviderError`         — anything raised by a provider impl
  * :class:`OperationError`        — runtime failure during a use case
  * :class:`GuestError`            — failure talking to a remote agent
  * :class:`HostEnvironmentError`  — host pre-flight failure
  * :class:`VaultError`            — credential storage failure
  * :class:`SchedulerError`        — scheduler / hook subsystem failure

Generic concretes (4) — useful on their own AND useful as bases for
project-specific subclasses. Each carries machine-readable fields per
rule EH-08:

  * :class:`SchemaValidationError`   — carries ``.errors`` (list)
  * :class:`CommandError`            — carries ``.argv`` / ``.returncode`` / ``.stderr``
  * :class:`ResourceNotFoundError`   — carries ``.name``
  * :class:`ResourceAlreadyExistsError` — carries ``.name``

See ``docs/standards/ERROR_HANDLING.md`` §2 for the design rationale
+ the drop-in template that produced this file.
"""

from __future__ import annotations

from enum import StrEnum

from mgf.common.exceptions._base import AppError

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------


class ConfigError(AppError):
    """Base for configuration / spec validation failures."""


class SchemaValidationError(ConfigError):
    """A typed spec or config payload failed validation.

    Always carries the *complete* list of errors so callers can show
    every problem at once (don't make the user re-run for each one).

    The message prefix is configurable via the
    :attr:`_MESSAGE_PREFIX` class attribute. Subclasses that want a
    different prefix override it without bypassing this class's
    ``__init__`` (closes FEEDBACK PAPER-05). Example::

        class SpecValidationError(SchemaValidationError):
            _MESSAGE_PREFIX = "spec validation failed"

    The default ``"validation failed"`` is pinned by the test suite
    so existing consumers depending on it stay green.
    """

    # Override in subclasses to customise the message prefix.
    # Class attribute (not constructor arg) so subclasses don't have
    # to forward arguments.
    _MESSAGE_PREFIX: str = "validation failed"

    def __init__(self, errors: list[str]) -> None:
        if not errors:
            raise ValueError("SchemaValidationError requires at least one error")
        self.errors: list[str] = list(errors)  # copy — defensive
        super().__init__(f"{self._MESSAGE_PREFIX}: {'; '.join(errors)}")


class AppConfigErrorKind(StrEnum):
    """Discriminator for :class:`AppConfigError` failure modes (closes PAPER-11).

    Set on the exception by the raising site so consumers can
    translate to project-flavoured wording without string-matching
    on the private message format::

        try:
            doc = store.load()
        except AppConfigError as e:
            match e.kind:
                case AppConfigErrorKind.JSON_PARSE_FAILURE | AppConfigErrorKind.YAML_PARSE_FAILURE:
                    raise OperationError("... is unreadable") from e
                case AppConfigErrorKind.UNSUPPORTED_VERSION | AppConfigErrorKind.MISSING_VERSION:
                    raise OperationError("... has unsupported schema_version") from e
                case _:
                    raise OperationError(f"... is malformed: {e}") from e

    The string values are stable contract: they double as a
    machine-readable kind for any non-Python consumer that ends up
    reading a serialised crash report.

    ``e.kind`` is ``None`` for any :class:`AppConfigError` raised
    outside a kind-aware site (legacy / general-purpose). New raise
    sites SHOULD set ``kind=`` on every code path; existing sites
    upgrade incrementally.

    Coverage today:
      - :mod:`mgf.common.versioned` — every load-path raise sets
        ``kind``. Save-path raises don't yet (consumer pain hasn't
        been filed).
    """

    FILE_NOT_FOUND = "file_not_found"
    IO_READ_FAILED = "io_read_failed"
    JSON_PARSE_FAILURE = "json_parse_failure"
    YAML_PARSE_FAILURE = "yaml_parse_failure"
    NON_DICT_ROOT = "non_dict_root"
    MISSING_VERSION = "missing_version"
    UNSUPPORTED_VERSION = "unsupported_version"
    MIGRATE_FAILED = "migrate_failed"
    SCHEMA_VALIDATION = "schema_validation"


class AppConfigError(ConfigError):
    """The application config file is missing, malformed, or wrong version.

    Raised by :func:`mgf.common.config.load_settings` and friends.
    The message is human-formatted; consumer projects MAY subclass
    to add app-specific action hints (rule EH-09).

    :attr:`kind` is a typed :class:`AppConfigErrorKind` discriminator
    set by raising sites that opt in (today: every load-path raise in
    :mod:`mgf.common.versioned`). Defaults to ``None`` — consumers
    using ``match e.kind`` MUST handle the ``None`` case (legacy /
    general-purpose raises). See PAPER-11 for the design rationale.
    """

    #: Discriminator set by raising sites. ``None`` for legacy /
    #: general-purpose raises that don't yet opt in.
    kind: AppConfigErrorKind | None = None


# ---------------------------------------------------------------------------
# Provider (third-party SDK / hardware boundary)
# ---------------------------------------------------------------------------


class ProviderError(AppError):
    """Base for failures inside a provider implementation.

    A provider implementation MUST translate every third-party
    exception into a ProviderError (or subclass) at the boundary
    that owns the third-party call (rule EH-02).
    """


class ResourceNotFoundError(ProviderError):
    """A name or id did not resolve to any known resource.

    Carries ``.name`` for callers that need to programmatically
    decide what to do (e.g., create the missing resource). Subclass
    in your project to add an actionable message hint per rule
    EH-09 — for example, vm-vmanager-app's ``DomainNotFoundError`` extends
    the message with "run 'vmanager vm list' to see known VMs".
    """

    def __init__(self, name: str) -> None:
        self.name: str = name
        super().__init__(f"no resource named {name!r}")


class ResourceAlreadyExistsError(ProviderError):
    """A create operation collided with an existing resource."""

    def __init__(self, name: str) -> None:
        self.name: str = name
        super().__init__(f"a resource named {name!r} already exists")


# ---------------------------------------------------------------------------
# Operations (runtime failures during a long-running use case)
# ---------------------------------------------------------------------------


class OperationError(AppError):
    """Base for runtime failures during a long-running operation."""


class CommandError(OperationError):
    """A subprocess invoked via the shell wrapper exited non-zero.

    Carries argv, returncode, and stderr so callers can surface a
    precise error without re-running the command. Tests rely on the
    "command failed (exit N):" prefix and on the trailing
    ": <stderr>" tail when stderr is non-empty.
    """

    def __init__(
        self,
        argv: list[str],
        returncode: int,
        stderr: str = "",
    ) -> None:
        self.argv: list[str] = list(argv)
        self.returncode: int = returncode
        self.stderr: str = stderr
        cmd = " ".join(argv)
        tail = f": {stderr.strip()}" if stderr.strip() else ""
        super().__init__(f"command failed (exit {returncode}): {cmd}{tail}")


# ---------------------------------------------------------------------------
# Guest / network (remote agent communication)
# ---------------------------------------------------------------------------


class GuestError(AppError):
    """Base for failures communicating with a remote agent."""


class GuestUnreachableError(GuestError):
    """Could not open a connection to a remote host.

    Raised by SSH client wrappers, guest-agent ping helpers, and any
    other code that opens a network connection to a remote system.
    The message typically includes the host + port + the underlying
    cause; consumer projects MAY subclass to add an action hint per
    rule EH-09.
    """


class GuestCommandError(GuestError):
    """A command ran on the remote host but returned non-zero.

    Distinct from :class:`GuestUnreachableError` (network/auth
    failure) and from :class:`CommandError` (host-side subprocess
    failure). Used by the SSH client wrapper to surface remote
    exit codes.
    """


# ---------------------------------------------------------------------------
# Vault (credential storage)
# ---------------------------------------------------------------------------


class VaultError(AppError):
    """Base for credential vault failures."""


# ---------------------------------------------------------------------------
# Environment (host pre-flight)
# ---------------------------------------------------------------------------


class HostEnvironmentError(AppError):
    """Base for host pre-flight failures.

    Raised when a host-level pre-flight check fails: CPU lacks
    vmx, kernel module missing, required binary absent, etc.

    Named ``HostEnvironmentError`` (not ``EnvironmentError``) to
    avoid shadowing Python's built-in alias for ``OSError``. The
    legacy ``EnvironmentError`` alias (which raised an
    ``MgfDeprecationWarning`` on use) was removed in v0.33.0 per
    its AP-12 deprecation cycle; consumers catching up should run
    ``mgf-common migrate``, which carries the
    ``EnvironmentError → HostEnvironmentError`` rule.
    """


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


class SchedulerError(AppError):
    """Base for scheduler and hook subsystem failures."""


# ---------------------------------------------------------------------------
# HTTP / web (closes FEEDBACK HTTP-01)
# ---------------------------------------------------------------------------


class HttpError(AppError):
    """Base for web/API exceptions carrying an HTTP status code.

    The eighth category in the typed-exception taxonomy
    (alongside ``ConfigError``, ``ProviderError``,
    ``OperationError``, ``GuestError``, ``HostEnvironmentError``,
    ``VaultError``, ``SchedulerError``). Closes FEEDBACK HTTP-01:
    the systems-management-domain seven didn't have a natural
    home for HTTP-status-coded exceptions, so web/API consumers
    were forced to either stretch ``OperationError`` for
    everything or skip the category layer entirely.

    The category-catch property holds at three levels:

      - ``except HttpError:``        — every HTTP-shaped failure
      - ``except HttpClientError:``  — every 4xx
      - ``except HttpServerError:``  — every 5xx

    Web-app consumers subclass the concrete leaves
    (``RoomNotFoundError(HttpNotFoundError)``, etc.) and inherit the
    ``http_status`` class attribute. Framework integration
    (Django middleware, FastAPI exception handler) reads
    ``exc.http_status`` to map the response status code.

    Stability tier: experimental (per AP-11; new names start
    here, promote to ``stable`` after one MINOR cycle of
    consumer evidence).
    """

    http_status: int = 500
    """The HTTP status code this exception maps to. Subclasses
    override; consumer subclasses MAY override further. Class
    attribute (not constructor arg) so subclasses don't have to
    forward arguments — instances inherit the class default."""


class HttpClientError(HttpError):
    """Base for 4xx — the request was bad.

    The category-catch property: ``except HttpClientError:``
    catches every concrete 4xx subclass (consumer-defined or
    bundled).
    """

    http_status: int = 400


class HttpServerError(HttpError):
    """Base for 5xx — the server failed to fulfill a valid request.

    The category-catch property: ``except HttpServerError:``
    catches every concrete 5xx subclass (consumer-defined or
    bundled).
    """

    http_status: int = 500


# 4xx concrete leaves. Consumers subclass these for project-specific
# cases and inherit http_status.
class HttpBadRequestError(HttpClientError):
    """400 — generic client-side validation / parse failure.

    Project-specific subclasses (``RoomBadInputError``,
    ``MessageMalformedError``, etc.) inherit ``http_status=400``
    automatically.
    """

    http_status: int = 400


class HttpUnauthorizedError(HttpClientError):
    """401 — authentication required (no / invalid credentials)."""

    http_status: int = 401


class HttpForbiddenError(HttpClientError):
    """403 — authenticated but not permitted."""

    http_status: int = 403


class HttpNotFoundError(HttpClientError):
    """404 — resource doesn't exist.

    Distinct from :class:`ResourceNotFoundError` (which is a
    provider-domain exception used by SDK / hardware boundaries).
    Web consumers subclass ``HttpNotFoundError`` for HTTP-shaped 404s.
    """

    http_status: int = 404


class HttpConflictError(HttpClientError):
    """409 — request conflicts with current state."""

    http_status: int = 409


class HttpGoneError(HttpClientError):
    """410 — resource existed but is permanently removed."""

    http_status: int = 410


class HttpUnprocessableError(HttpClientError):
    """422 — request well-formed but semantically invalid.

    The canonical "validation failed" status for web APIs.
    Distinct from :class:`SchemaValidationError` (config-domain).
    """

    http_status: int = 422


# 5xx concrete leaves.
class HttpInternalServerError(HttpServerError):
    """500 — generic server-side failure."""

    http_status: int = 500


class HttpServiceUnavailableError(HttpServerError):
    """503 — the service is temporarily unavailable.

    Used for circuit-breaker-open, dependency-down,
    maintenance-mode, etc. Consumers SHOULD set ``Retry-After``
    on the response when raising this.
    """

    http_status: int = 503


# Domain-specific HTTP-01 leaves used to live here (e.g.
# ``HmacVerificationError``). Per the federation rule (decision **D3**
# in ``docs/inprogress/federation_roadmap.md``), siblings own their
# domain concretes; mgf-common owns hierarchy roots + status leaves.
# ``HmacVerificationError`` moved to ``mgf.fastapi.exceptions`` at the
# v0.28 / mgf-fastapi v0.1 cutover.


# ---------------------------------------------------------------------------
# Cancellation (cooperative; rule DP-09)
# ---------------------------------------------------------------------------


class CancellationError(OperationError):
    """A long-running operation was cancelled cooperatively.

    Raised by :class:`mgf.common.progress.CancellationToken.raise_if_cancelled`
    (and its async sibling) when ``cancel()`` has been signalled.
    Subclass of :class:`OperationError` so existing CLI top-level
    handlers that map ``OperationError`` → exit code catch it
    without code change (rule EH-04).

    The standard rule (``DESIGN_PRINCIPLES.md`` §8): cancellation
    MUST raise a typed exception, never a bare
    :class:`KeyboardInterrupt`. ``CancellationError`` is the
    canonical type to use; consumer call sites either catch it
    explicitly to render a nice "operation cancelled" message,
    or let it propagate up to the top-level handler.
    """

    def __init__(self, message: str = "operation cancelled by user") -> None:
        super().__init__(message)


# ---------------------------------------------------------------------------
# Lifecycle (bootstrap / shutdown)
# ---------------------------------------------------------------------------


class ProcessError(OperationError):
    """Base for failures launching or supervising child processes via :mod:`mgf.common.process`.

    Subclass of :class:`OperationError` so consumer top-level handlers
    that already map ``OperationError`` to a non-zero exit (rule EH-04)
    catch ProcessError-shaped failures without code change.

    Concrete leaves carry the launched command's identity for
    log-correlation:

    - ``.command``       — the argv list as passed to :func:`run` /
      :class:`Service` (never the shell-string form — :mod:`mgf.common.process`
      is list-only by construction; eliminates command-injection at
      the API surface).
    - ``.command_id``    — short opaque id stamped onto every log
      record for the launched process (look up the captured output by
      grepping ``logs/<app>.log`` for this id).
    """

    def __init__(
        self,
        message: str,
        *,
        command: list[str] | None = None,
        command_id: str | None = None,
    ) -> None:
        super().__init__(message)
        self.command: list[str] | None = command
        self.command_id: str | None = command_id


class ProcessTimeoutError(ProcessError):
    """A child process exceeded the ``timeout=`` deadline."""

    def __init__(
        self,
        *,
        command: list[str],
        command_id: str,
        timeout: float,
    ) -> None:
        self.timeout: float = timeout
        super().__init__(
            f"process {command[0]!r} did not finish within {timeout}s (command_id={command_id})",
            command=command,
            command_id=command_id,
        )


class ProcessNonZeroExitError(ProcessError):
    """A child process exited with a non-zero return code.

    Only raised when :func:`run` was called with ``check=True`` (the
    default). When ``check=False``, the :class:`ProcessResult` carries
    ``returncode`` for the caller to inspect — no exception is raised.
    """

    def __init__(
        self,
        *,
        command: list[str],
        command_id: str,
        returncode: int,
        stderr_tail: str = "",
    ) -> None:
        self.returncode: int = returncode
        self.stderr_tail: str = stderr_tail
        suffix = f"; last stderr: {stderr_tail!r}" if stderr_tail else ""
        super().__init__(
            f"process {command[0]!r} exited with code {returncode} "
            f"(command_id={command_id}){suffix}",
            command=command,
            command_id=command_id,
        )


class ProcessSignaledError(ProcessError):
    """A child process was terminated by a signal (POSIX-only failure mode).

    Distinct from :class:`ProcessNonZeroExitError` because a SIGTERM /
    SIGKILL exit is qualitatively different from a process choosing
    to exit with code N: the latter ran to completion, the former was
    cut short.
    """

    def __init__(
        self,
        *,
        command: list[str],
        command_id: str,
        signal: int,
    ) -> None:
        self.signal: int = signal
        super().__init__(
            f"process {command[0]!r} was killed by signal {signal} (command_id={command_id})",
            command=command,
            command_id=command_id,
        )


class ServiceNotReadyError(ProcessError):
    """A :class:`mgf.common.process.Service`'s readiness probe never resolved.

    Raised by :meth:`Service.start` when the readiness probe (or
    composite probe) does not return ``True`` within the configured
    timeout. The service is torn down before the exception is raised
    — no resource leak. Carries the probe's last error message (when
    available) for diagnosis.
    """

    def __init__(
        self,
        *,
        command: list[str],
        command_id: str,
        probe_description: str,
        timeout: float,
        last_error: str = "",
    ) -> None:
        self.probe_description: str = probe_description
        self.timeout: float = timeout
        self.last_error: str = last_error
        suffix = f"; last error: {last_error}" if last_error else ""
        super().__init__(
            f"service {command[0]!r} did not become ready within "
            f"{timeout}s (probe={probe_description}, "
            f"command_id={command_id}){suffix}",
            command=command,
            command_id=command_id,
        )


class BootstrapError(OperationError):
    """Library bootstrap failed mid-wiring.

    Raised by :func:`mgf.common.bootstrap` when one of the wiring
    steps (identity / logging / OTel / excepthooks / crash sinks)
    raises. Bootstrap is **transactional** (closes FEEDBACK design
    ref §14.2): on failure, every wiring step that succeeded so
    far is rolled back, the process state is restored to its
    pre-bootstrap shape, and this exception is raised carrying:

    - ``.failed_step``      — the wiring step that raised
      (e.g., ``"otel"``, ``"crash_sinks"``).
    - ``__cause__``         — the original exception (chained via
      ``raise BootstrapError(...) from cause``).

    Catching ``BootstrapError`` and continuing is almost always
    wrong — the library is uninitialised; subsequent observability
    calls would silently no-op. The right shape is: log to stderr,
    fix the cause, exit with a non-zero code.

    Subclass of :class:`OperationError` so consumer top-level
    handlers that already map ``OperationError`` to an exit code
    (rule EH-04) catch it without code change.
    """

    def __init__(self, failed_step: str, *, cause: BaseException) -> None:
        self.failed_step: str = failed_step
        super().__init__(
            f"bootstrap failed at step {failed_step!r}: {cause}; "
            f"the library is uninitialised. Fix the cause and "
            f"retry mgf.common.bootstrap()."
        )


__all__ = [
    "AppConfigError",
    "BootstrapError",
    "CancellationError",
    "CommandError",
    "ConfigError",
    "GuestCommandError",
    "GuestError",
    "GuestUnreachableError",
    "HostEnvironmentError",
    "HttpBadRequestError",
    "HttpClientError",
    "HttpConflictError",
    "HttpError",
    "HttpForbiddenError",
    "HttpGoneError",
    "HttpInternalServerError",
    "HttpNotFoundError",
    "HttpServerError",
    "HttpServiceUnavailableError",
    "HttpUnauthorizedError",
    "HttpUnprocessableError",
    "OperationError",
    "ProcessError",
    "ProcessNonZeroExitError",
    "ProcessSignaledError",
    "ProcessTimeoutError",
    "ProviderError",
    "ResourceAlreadyExistsError",
    "ResourceNotFoundError",
    "SchedulerError",
    "SchemaValidationError",
    "ServiceNotReadyError",
    "VaultError",
]
