"""Layer 3 — Translation seam + best-effort observer + CLI exit-code map.

Production-shaped use of the typed exception hierarchy. Demonstrates
the three load-bearing patterns from the ERROR_HANDLING standard
(mgf-common standards ERROR_HANDLING):

  • The translation seam (rule EH-02): every third-party SDK
    exception is converted to a typed application exception at the
    boundary that owns the third-party call. Consumers downstream
    of the seam never see vendor types.

  • Cause chaining via `from e` (rule EH-03): the original cause
    is preserved so debugging is archaeology-free.

  • The best-effort observer (rule EH-10): code that runs inside a
    critical-path operation but whose failure MUST NOT take down
    that operation. Catches broadly, logs, returns a bool, never
    raises.

  • The CLI top-level handler (rule EH-04): the outermost catch in
    the entry point that maps each typed category to a documented
    exit code, plus a catch-all that crash-reports + returns
    EXIT_UNKNOWN.

  • Machine-readable fields (rule EH-08): structured `.argv`,
    `.returncode`, `.stderr` on `CommandError` so callers route on
    fields, not on substring matches against the message.

Run with::

    python examples/01_exceptions/03_advanced.py

Output shows each pattern in turn with annotated prefixes.
"""

from __future__ import annotations

import sys

from mgf.common.exceptions import (
    AppError,
    CommandError,
    ConfigError,
    GuestError,
    GuestUnreachableError,
    OperationError,
    ProviderError,
    ResourceNotFoundError,
    SchemaValidationError,
)

# ─────────────────────────────────────────────────────────────────
# Project-specific hierarchy
# ─────────────────────────────────────────────────────────────────
#
# Convention from docs/standards/ERROR_HANDLING.md §2:
#   - Hierarchy is shallow (2–3 levels). Deep is unmemorable.
#   - Categories at level 2 are the well-known set
#     (ConfigError, ProviderError, OperationError, GuestError, ...).
#   - Concretes carry machine-readable fields (rule EH-08).
#   - User-facing messages end in an actionable next step (rule EH-09).


class MyAppError(AppError):
    """Base for every error raised by the example app."""


class HypervisorNotFoundError(ResourceNotFoundError):
    """Specific hypervisor host could not be resolved.

    Refines the generic ResourceNotFoundError with an actionable
    hint (rule EH-09). The hint substring 'check the inventory'
    SHOULD be pinned by a substring test in the project's test
    suite (rule EH-09) so a future refactor cannot silently drop
    it. Example pin shape:

        assert "check the inventory" in str(HypervisorNotFoundError("x"))
    """

    def __init__(self, name: str) -> None:
        self.name = name  # rule EH-08 — machine-readable field
        super().__init__(
            f"no hypervisor named {name!r}; check the inventory "
            f"with 'myapp hypervisor list'"
        )


# ─────────────────────────────────────────────────────────────────
# A simulated third-party SDK
# ─────────────────────────────────────────────────────────────────
#
# Stand-in for a real vendor library (e.g., libvirt, paramiko, an
# HTTP client, a cloud SDK). Its exceptions are NOT in our typed
# hierarchy — the translation seam below converts them.


class _VendorSDKError(Exception):
    """Base for the simulated vendor's exceptions."""

    def __init__(self, code: str, message: str) -> None:
        self.code = code
        super().__init__(message)


class _VendorTimeout(_VendorSDKError):
    """Vendor-side timeout — needs translating to GuestError."""


class _VendorAuthError(_VendorSDKError):
    """Vendor-side auth failure — also needs translating."""


# ─────────────────────────────────────────────────────────────────
# The translation seam (rule EH-02)
# ─────────────────────────────────────────────────────────────────
#
# Single function per provider that maps vendor exceptions to our
# typed hierarchy. Returns the typed exception — caller raises with
# `from e` to preserve the cause chain (rule EH-03).
#
# Two equivalent shapes are valid (Shape A / Shape B in
# docs/standards/ERROR_HANDLING.md §3.1). This example uses
# Shape A: `_translate` returns; caller raises. Shape A makes the
# call site explicit and keeps the function pure-data.


def _translate(e: _VendorSDKError, *, host: str | None = None) -> AppError:
    """Map a _VendorSDKError to our typed hierarchy.

    The mapping table lives here in one place per provider — adding
    a new vendor error code is a one-liner here, not a hunt across
    every call site.
    """
    if isinstance(e, _VendorTimeout):
        return GuestUnreachableError(
            f"hypervisor {host or '<unknown>'} timed out: {e}"
        )
    if isinstance(e, _VendorAuthError):
        return GuestUnreachableError(
            f"hypervisor {host or '<unknown>'} rejected auth: {e}"
        )
    return ProviderError(f"vendor error {e.code}: {e}")


def vendor_call(host: str, *, simulate: str = "ok") -> str:
    """Simulate a call into the vendor SDK.

    The translation seam is right at the boundary — every path
    through this function either returns a typed-app value or
    raises a typed-app exception. Vendor types do not leak.
    """
    try:
        if simulate == "timeout":
            raise _VendorTimeout("E_TIMEOUT", "no response in 30s")
        if simulate == "auth":
            raise _VendorAuthError("E_AUTH", "invalid credentials")
        if simulate == "unknown":
            raise _VendorSDKError("E_UNKNOWN", "something obscure")
        return f"connected to {host}"
    except _VendorSDKError as e:
        # Rule EH-03: chain the original cause via `from e`.
        # The cause is then visible in the crash report and in
        # `traceback.format_exception(..., chain=True)`.
        raise _translate(e, host=host) from e


# ─────────────────────────────────────────────────────────────────
# Best-effort observer (rule EH-10)
# ─────────────────────────────────────────────────────────────────
#
# Code that runs inside a critical-path operation (a hook, a
# scheduler tick, an alert evaluator) but whose failure MUST NOT
# take down that operation. The shape:
#
#   - Catches broadly (AppError + bare Exception).
#   - Logs at WARNING (here: prints, since this example is
#     stdlib-only).
#   - Returns a bool.
#   - NEVER raises.
#
# A unit test for the observer MUST inject a raising observer and
# assert the loop continues (rule TS-09).


def fire_one(observer_name: str, observer_fn) -> bool:
    """Run one observer. Never raises.

    Returns True on success, False on failure. The caller continues
    to the next observer regardless of return.
    """
    try:
        observer_fn()
    except AppError as e:
        # Typed failure — known shape, log and continue.
        print(f"  observer {observer_name!r}: typed failure: {e}")
        return False
    except Exception as e:  # noqa: BLE001 — never-raises invariant
        # Untyped failure — this is a bug in the observer. Log and
        # continue so the host operation isn't hostage to it.
        print(f"  observer {observer_name!r}: unexpected error: {e}")
        return False
    return True


# ─────────────────────────────────────────────────────────────────
# The CLI top-level handler (rule EH-04)
# ─────────────────────────────────────────────────────────────────
#
# Maps each typed category to a documented exit code. Order
# matters: catch most-specific (ConfigError) before most-general
# (AppError). The catch-all at the end MUST route through the
# crash reporter (rule EH-05) — omitted here for brevity; see
# examples/05_crash_reporting/03_advanced.py for the full wiring.

EXIT_OK = 0
EXIT_CONFIG_ERROR = 1
EXIT_OPERATION_ERROR = 2
EXIT_GUEST_ERROR = 3
EXIT_PROVIDER_ERROR = 4
EXIT_UNKNOWN = 10


def run_use_case() -> None:
    """A use case that may raise multiple typed categories.

    Production code body would call into the orchestrator,
    provider, etc. Here we just demonstrate the shape.
    """
    raise HypervisorNotFoundError("hv-east-3")


def cli_main() -> int:
    """Top-level handler. Maps every typed category to an exit code."""
    try:
        run_use_case()
    except SchemaValidationError as e:
        # Special-case ConfigError subclass with a structured field
        # the user wants to see in full.
        print(f"config validation failed:", file=sys.stderr)
        for err in e.errors:
            print(f"  - {err}", file=sys.stderr)
        return EXIT_CONFIG_ERROR
    except ConfigError as e:
        print(f"config error: {e}", file=sys.stderr)
        return EXIT_CONFIG_ERROR
    except (HypervisorNotFoundError, ResourceNotFoundError) as e:
        # ResourceNotFoundError is a ProviderError — caught here
        # before the broader ProviderError clause below.
        print(f"resource not found: {e}", file=sys.stderr)
        return EXIT_PROVIDER_ERROR
    except ProviderError as e:
        print(f"provider error: {e}", file=sys.stderr)
        return EXIT_PROVIDER_ERROR
    except GuestError as e:
        print(f"guest error: {e}", file=sys.stderr)
        return EXIT_GUEST_ERROR
    except OperationError as e:
        print(f"operation failed: {e}", file=sys.stderr)
        return EXIT_OPERATION_ERROR
    except AppError as e:
        # Catch-all WITHIN our typed hierarchy. An exception that
        # reaches here is one we declared but didn't categorise —
        # consider whether it needs its own handler clause.
        print(f"error: {e}", file=sys.stderr)
        return EXIT_UNKNOWN
    return EXIT_OK


# ─────────────────────────────────────────────────────────────────
# main() — exercises every pattern
# ─────────────────────────────────────────────────────────────────


def main() -> None:
    print("─── 1. Translation seam ──────────────────")
    # Happy path
    print(f"  vendor_call ok: {vendor_call('hv-1', simulate='ok')}")
    # Each simulated vendor failure translates to a typed app error
    for sim in ("timeout", "auth", "unknown"):
        try:
            vendor_call("hv-1", simulate=sim)
        except (GuestUnreachableError, ProviderError) as e:
            # The cause is preserved through `from e`
            cause = type(e.__cause__).__name__ if e.__cause__ else "<none>"
            print(f"  sim={sim!r}: typed={type(e).__name__}: {e}")
            print(f"          cause={cause}")

    print("\n─── 2. Best-effort observer ──────────────")
    # Three observers; the second raises, but the loop continues.
    observers = {
        "alpha": lambda: print("  observer 'alpha': ran"),
        "beta": lambda: (_ for _ in ()).throw(RuntimeError("kaboom")),
        "gamma": lambda: print("  observer 'gamma': ran"),
    }
    successes = sum(fire_one(name, fn) for name, fn in observers.items())
    print(f"  → {successes}/{len(observers)} observers succeeded")

    print("\n─── 3. SchemaValidationError carries .errors ─")
    try:
        raise SchemaValidationError(
            ["field 'name' is required", "field 'cpu_count' must be ≥ 1"]
        )
    except SchemaValidationError as e:
        print(f"  message: {e}")
        print(f"  field .errors: {e.errors}")

    print("\n─── 4. CommandError carries argv/returncode/stderr ─")
    try:
        raise CommandError(
            argv=["qemu-img", "info", "/missing.qcow2"],
            returncode=2,
            stderr="qemu-img: Could not open '/missing.qcow2'",
        )
    except CommandError as e:
        print(f"  message: {e}")
        print(f"  field .argv: {e.argv}")
        print(f"  field .returncode: {e.returncode}")
        print(f"  field .stderr: {e.stderr!r}")

    print("\n─── 5. CLI top-level handler maps to exit codes ─")
    rc = cli_main()
    print(f"  cli_main() returned exit code: {rc}")
    # In a real CLI: sys.exit(rc). Here we let main() return so the
    # smoke test sees a clean exit.


if __name__ == "__main__":
    main()
