"""Layer 3 — Production-shaped tiny app: every standard wired in.

The headline integration example. A small CLI app written the way
the MGF engineering standards prescribe (mgf-common standards <TOPIC>),
annotated line-by-line with the rule each piece implements. If
you're starting a new MGF-family project, this file is the closest
thing to a blank template that's already standards-compliant.

What this example wires up:

  • Identity + wiring via bootstrap() BEFORE any observability call.
    [DESIGN_PRINCIPLES Cross-cutting §2 in mgf-common's CLAUDE.md]

  • Typed Settings via BaseAppSettings + settings_config
    [rules CF-01, CF-02, CF-08, AP-09]

  • Logging: console + rotating file + JSON formatter + Redactor
    [rules LG-01, LG-02, LG-04, LG-05, LG-06, LG-08, LG-09, LG-11]

  • OTel: bootstrap-wired + per-use-case operation_span
    [rules LG-03, LG-12, LOGGING.md §7]

  • Top-level handlers: bootstrap-wired sys.excepthook +
    threading.excepthook + asyncio handler stub +
    CLI _run_and_translate_exceptions
    [rules EH-04, EH-05, EH-06]

  • Crash reports on every uncaught path
    [rules EH-05, SC-09 (redactor scrubs payload)]

  • Typed exception hierarchy with exit-code mapping
    [rules EH-01, EH-02, EH-03, EH-07, EH-08, EH-09]

  • Provider ABC pattern (mock provider used by the example;
    in production the real provider lives behind the same ABC).
    [rule DP-02, DESIGN_PRINCIPLES.md §1.2]

  • Resource management (context managers, atomic cleanup).
    [rules DP-13, CF-04]

  • UTC timestamps in logs (auto, via the JSON formatter).
    [rule DP-12]

The example uses argparse (stdlib) instead of click so the smoke
test runs without extra deps. A real production app would use
click for the CLI; the rest of the wiring is identical.

Run with::

    python examples/06_full_app/03_advanced.py greet --name world
    python examples/06_full_app/03_advanced.py greet --name ''     # demo error path
"""

from __future__ import annotations

import argparse
import logging
import os
import sys
import tempfile
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import UTC, datetime

import mgf.common
from mgf.common.config import BaseAppSettings, settings_config
from mgf.common.exceptions import (
    AppError,
    ConfigError,
    OperationError,
    ProviderError,
    ResourceNotFoundError,
)
from mgf.common.observability import operation_span, report_now
from mgf.common.settings import LoggingSettings, MgfSettings

# ─────────────────────────────────────────────────────────────────
# 1. Typed Settings (rule CF-01, CF-08)
# ─────────────────────────────────────────────────────────────────
#
# Single typed source of truth. Loaded once at the entry point and
# passed down by constructor injection. No module reads env vars
# or the config file directly.
#
# `settings_config(env_prefix="MYAPP_")` is the canonical
# way to set model_config on a subclass — the naive
# `**BaseAppSettings.model_config` spread does NOT work
# (rule CF-* mental model + Cross-cutting pattern §3 in CLAUDE.md).


class Settings(BaseAppSettings):
    """Single typed config for myapp."""

    model_config = settings_config(env_prefix="MYAPP_")

    log_level: str = "INFO"
    log_format: str = "auto"
    backend_uri: str = "mock://demo"


# ─────────────────────────────────────────────────────────────────
# 2. Provider ABC pattern (rule DP-02)
# ─────────────────────────────────────────────────────────────────
#
# The real backend (libvirt, an HTTP API, a cloud SDK) sits behind
# this ABC. The ABC's abstract method set is the contract — a
# lock-in test pins it (rule TS-08 / DESIGN_PRINCIPLES §1.2).
#
# This example uses an in-memory MockGreeter so it runs without
# external services. A production GreeterProvider would translate
# its third-party SDK exceptions to the typed hierarchy at the seam
# (rule EH-02).


@dataclass(frozen=True)
class Greeting:
    """Frozen domain type — never bare strings cross module
    boundaries (rule DP-03)."""

    name: str
    message: str
    issued_at: datetime  # UTC always (rule DP-12)


class GreeterProvider(ABC):
    """The seam between core code and the underlying greeting source.

    Replacing the backend (in-memory → DB-backed → HTTP) is a new
    class implementing this ABC, NOT a refactor of the use case
    (rule DP-08).
    """

    @abstractmethod
    def greet(self, name: str) -> Greeting: ...


class MockGreeter(GreeterProvider):
    """In-memory provider used by the demo + tests."""

    def greet(self, name: str) -> Greeting:
        if not name:
            # Translation seam (rule EH-02): the underlying check
            # produces a typed app exception, not a bare ValueError.
            raise OperationError(
                "name cannot be empty; pass --name <value>"
            )
        if name == "ghost":
            raise ResourceNotFoundError(name)
        return Greeting(
            name=name,
            message=f"hello, {name}",
            issued_at=datetime.now(UTC),  # rule DP-12: UTC at boundaries
        )


# ─────────────────────────────────────────────────────────────────
# 3. The use case (L2 in the layered model)
# ─────────────────────────────────────────────────────────────────
#
# Wraps the real work in operation_span (rule LG-03 + §7.3 of
# LOGGING.md). The provider is injected — never constructed inline
# (rule DP-08, CF-08).


def use_case_greet(
    LOG: logging.Logger,
    provider: GreeterProvider,
    name: str,
) -> Greeting:
    """Produce a Greeting via the injected provider.

    Wrapped in an operation_span so the per-attempt latency +
    success/failure is visible in the trace tool. Logs operation
    start/end (rule LG-11).
    """
    # NOTE: `name` is a reserved attribute on Python's LogRecord
    # (the logger name). Use `subject` (or any distinct field) in
    # `extra={...}` to avoid KeyError("Attempt to overwrite 'name'").
    LOG.info(
        "operation start",
        extra={"operation": "greeting.create", "subject": name},
    )
    with operation_span("greeting.create", subject_name=name):
        try:
            greeting = provider.greet(name)
        except AppError:
            # Re-raise — top-level handler will categorise and map
            # to exit code. Logged at ERROR with traceback by the
            # exception's eventual handler, not here.
            raise
        LOG.info(
            "operation end",
            extra={
                "operation": "greeting.create",
                "subject": name,
                "issued_at": greeting.issued_at.isoformat(),
            },
        )
        return greeting


# ─────────────────────────────────────────────────────────────────
# 4. Top-level handler (rule EH-04)
# ─────────────────────────────────────────────────────────────────
#
# Maps each typed category to a documented exit code; the catch-all
# routes through report_now (rule EH-05). Order matters — most-
# specific first.


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


def _run_and_translate_exceptions(
    argv: list[str], provider: GreeterProvider
) -> int:
    """Outer catch — every typed category maps to an exit code.

    The rationale for each except clause matches the EXIT_* constant
    above. New typed categories MUST add a clause here AND a constant
    AND a CHANGELOG row (rule AP-12).
    """
    parser = argparse.ArgumentParser(prog="myapp")
    sub = parser.add_subparsers(dest="cmd", required=True)

    p_greet = sub.add_parser("greet")
    p_greet.add_argument("--name", default="world")

    args = parser.parse_args(argv)

    LOG = logging.getLogger("myapp.cli")

    try:
        if args.cmd == "greet":
            greeting = use_case_greet(LOG, provider, args.name)
            print(greeting.message)
            return EXIT_OK
        # Unknown subcommand — argparse already errored above; this is
        # a defensive path for future subcommands.
        raise ConfigError(f"unknown command: {args.cmd!r}")
    except ConfigError as e:
        # Config errors are user-fixable. Print to stderr; no crash report.
        print(f"config error: {e}", file=sys.stderr)
        LOG.error("config error", extra={"error": str(e)})
        return EXIT_CONFIG_ERROR
    except ResourceNotFoundError as e:
        # ResourceNotFoundError is a ProviderError — caught here first
        # so the user sees the actionable hint with the right prefix.
        print(f"not found: {e}", file=sys.stderr)
        LOG.warning("resource not found", extra={"resource": getattr(e, "name", None)})
        return EXIT_PROVIDER_ERROR
    except ProviderError as e:
        print(f"provider error: {e}", file=sys.stderr)
        LOG.error("provider failed", extra={"error": str(e)})
        return EXIT_PROVIDER_ERROR
    except OperationError as e:
        print(f"operation failed: {e}", file=sys.stderr)
        LOG.error("operation failed", extra={"error": str(e)})
        return EXIT_OPERATION_ERROR
    except AppError as e:
        print(f"error: {e}", file=sys.stderr)
        LOG.error("typed error", extra={"error": str(e)})
        return EXIT_UNKNOWN
    except Exception as e:  # noqa: BLE001 — outer catch-all, rule EH-05
        # Untyped exception is a bug. Crash-report it AND tell the
        # user where the report landed (rule EH-09 actionable hint).
        path = report_now(e, source="cli")
        print(
            f"internal error: {e}\nA crash report was written to {path}.",
            file=sys.stderr,
        )
        LOG.exception("unhandled exception")
        return EXIT_UNKNOWN


# ─────────────────────────────────────────────────────────────────
# 5. Wiring at the entry point
# ─────────────────────────────────────────────────────────────────


def cli_main(argv: list[str] | None = None) -> int:
    """The full entry-point shape. Production CLI looks like this."""
    argv = list(argv or sys.argv[1:])

    # Settings first — load once, pass everywhere (rule CF-08).
    settings = Settings()

    # bootstrap() wires identity + logging + OTel + excepthooks in
    # one transactional call. log_level / log_format come from the
    # typed settings; the redactor uses the default key set
    # (extend via MgfSettings.redaction.extra_keys if needed).
    # Save and replace prior excepthook with no-op so pytest's prior
    # hook doesn't capture our synthetic exception (the smoke test
    # invokes this in a subprocess; in real CLI, pytest is not in
    # the picture).
    prior = sys.excepthook
    sys.excepthook = lambda *_args: None
    try:
        with mgf.common.bootstrap(
            app_name="myapp",
            app_version="0.1.0",
            settings=MgfSettings(
                logging=LoggingSettings(
                    level=settings.log_level, format=settings.log_format,
                ),
            ),
        ):
            # bootstrap() above installed the excepthooks
            # transactionally (rule EH-04) — no separate
            # install_excepthook calls needed.

            # Provider injected at the top — switching to a real backend
            # is a one-line change here, not a refactor of the use case.
            provider = MockGreeter()

            return _run_and_translate_exceptions(argv, provider)
    finally:
        sys.excepthook = prior


def main() -> None:
    """Smoke-test entry point. Exercises both happy and sad paths."""
    with tempfile.TemporaryDirectory() as tmp:
        # Isolate every disk side-effect — log file, crash reports.
        os.environ["XDG_STATE_HOME"] = tmp
        os.environ["HOME"] = tmp

        # Happy path
        print("─── greet --name world ──────────────")
        rc = cli_main(["greet", "--name", "world"])
        print(f"  exit code: {rc} (expect 0)")

        # Sad path — empty name → OperationError → exit 2
        print("\n─── greet --name '' (sad path) ──────")
        rc = cli_main(["greet", "--name", ""])
        print(f"  exit code: {rc} (expect 2)")

        # Sad path — known-not-found → ResourceNotFoundError → exit 4
        print("\n─── greet --name ghost (sad path) ───")
        rc = cli_main(["greet", "--name", "ghost"])
        print(f"  exit code: {rc} (expect 4)")


if __name__ == "__main__":
    sys.exit(main() or 0)
