"""Layer 3 — Custom Redactor + LOG.exception + log levels + JSON inspection.

Production-shaped logging configuration. Demonstrates the
patterns from docs/standards/LOGGING.md:

  • Per-module loggers via ``logging.getLogger(__name__)`` so
    output can be filtered by source (rule LG-01).

  • Single setup point per entry point (rule LG-02). The
    ``mgf.common.bootstrap(...)`` call wires console + rotating file +
    redaction in one place.

  • Custom Redactor extending the default key set (rule LG-04 +
    SC-09). Per-project secret-bearing names go through
    ``Redactor(extra_keys=...)``, NEVER by editing the default.

  • Lazy %-style formatting (rule LG-08). The message template
    stays stable across records so aggregators group by template.

  • Structured fields via ``extra={...}`` (rule LG-09) — every
    consumer-facing identifier is a queryable top-level key in
    JSON output, not buried in the message string.

  • Operation start/end records with a stable ``operation`` field
    (rule LG-11) so a single log query reconstructs every attempt
    of a named operation across users / time.

  • ERROR records use ``LOG.exception`` (rule LG-05) so the
    traceback is included; never just the stringified exception
    in the message.

  • Levels are calibrated (rule §3 of LOGGING.md): DEBUG for
    engineer-only state; INFO for operator-visible lifecycle;
    WARNING for recovered-from anomalies; ERROR for user-visible
    failure.

  • The redactor pin from SECURITY_STANDARD §15 (mgf-common standards
    SECURITY_STANDARD) lives in tests/ — this example doesn't add
    tests but shows the behaviour.

Run with::

    python examples/03_logging/03_advanced.py
"""

from __future__ import annotations

import json
import logging
import os
import tempfile
import time
from pathlib import Path

import mgf.common
from mgf.common.settings import LoggingSettings, MgfSettings, RedactionSettings


def _show_records(log_file: Path, label: str) -> None:
    """Pretty-print the JSON records written to ``log_file``.

    A real consumer never reads its own log files like this — the
    aggregator (Loki / ELK / Datadog) does. We do it here so the
    example shows the on-wire shape that the formatter produces.
    """
    print(f"\n─── {label} ─────────────────────────")
    for line in log_file.read_text().splitlines():
        rec = json.loads(line)
        # Compact one-line view; show structured extras explicitly.
        extras = {
            k: v
            for k, v in rec.items()
            if k not in {"ts", "level", "logger", "msg", "module", "func", "line",
                         "thread", "process", "exception", "stack"}
        }
        print(f"  {rec['level']:<8} {rec['logger']}: {rec['msg']}")
        if extras:
            print(f"    {extras}")
        if "exception" in rec:
            # The traceback is included thanks to LOG.exception (LG-05).
            first_line = rec["exception"].splitlines()[0]
            print(f"    [exception captured: {first_line}]")


def do_use_case(
    LOG: logging.Logger,
    *,
    user: str,
    api_key: str,
    items: list[str],
) -> None:
    """A simulated use case that emits the canonical operation
    start/end records (rule LG-11) plus a per-item DEBUG.

    Note `api_key` is never explicitly logged — but the redactor
    catches it anyway if it slips through. Defense in depth.
    """
    # Operation start (rule LG-11). The `operation` field is the
    # join key for reconstructing every attempt of this op type.
    LOG.info(
        "operation start",
        extra={
            "operation": "items.process",
            "user": user,
            "item_count": len(items),
            # NOT explicitly logging api_key — but if a future bug
            # passed creds through `extra`, the redactor catches it
            # before any sink sees the value.
        },
    )

    start = time.monotonic()
    for i, item in enumerate(items):
        # DEBUG = engineer-only detail. Production default is INFO,
        # so these don't appear unless someone bumps the level.
        LOG.debug(
            "processed item",
            extra={"item": item, "index": i},
        )

    elapsed_ms = round((time.monotonic() - start) * 1000, 1)

    # Operation end (rule LG-11). Same `operation` field, plus the
    # duration so dashboards can compute p99 latency.
    LOG.info(
        "operation end",
        extra={
            "operation": "items.process",
            "user": user,
            "item_count": len(items),
            "duration_ms": elapsed_ms,
        },
    )


def main() -> None:
    with tempfile.TemporaryDirectory() as tmp:
        # Belt-and-suspenders environment isolation. Without this,
        # the default `_default_log_path()` would write under the
        # user's actual ~/.local/state/. Rule TS-03 (test isolation)
        # generalises: any code that exercises identity-based path
        # resolution MUST contain its disk side-effects.
        os.environ["XDG_STATE_HOME"] = tmp
        os.environ["HOME"] = tmp

        log_file = Path(tmp) / "myapp.log"

        # Custom redactor extension via MgfSettings.redaction — catches
        # project-specific secret-bearing keys (`x-internal-token`,
        # `session_uuid`) in addition to the default set (password,
        # token, api_key, authorization, ...).
        #
        # NEVER edit the default key set — extending via the settings
        # object keeps per-project additions composable and reviewable
        # (rule LG-04 / SC-09).
        #
        # fmt="json" forces structured output. The rotating file ALWAYS
        # uses JSON regardless of fmt — JSON is the contract every
        # mainstream aggregator (Loki / ELK / Datadog) parses.
        ctx = mgf.common.bootstrap(
            app_name="example-advanced-log",
            app_version="0.1.0",
            settings=MgfSettings(
                logging=LoggingSettings(level="DEBUG", format="json", file=log_file),
                redaction=RedactionSettings(
                    extra_keys=frozenset({"x-internal-token", "session_uuid"}),
                ),
            ),
        )

        LOG = logging.getLogger(__name__)

        # ── 1. Lazy %-formatting (rule LG-08) ──────────────
        # The format only happens if the level passes — keeps the
        # message template stable so aggregators group by template.
        LOG.info("server starting on port %d", 8080)

        # ── 2. Structured fields via extra={...} (rule LG-09) ─
        LOG.info(
            "client connected",
            extra={"client_ip": "10.0.0.5", "session_uuid": "uuid-abc-123"},
        )

        # ── 3. Default redactor catches well-known secret keys ─
        LOG.info(
            "user authenticating",
            extra={
                "username": "alice",
                "password": "hunter2",          # → [REDACTED]
                "api_key": "sk_live_xyz789",    # → [REDACTED]
            },
        )

        # ── 4. Custom-extension key also redacted ──────────
        LOG.info(
            "internal call",
            extra={
                "x-internal-token": "tok_secret_42",   # → [REDACTED]
                "endpoint": "/api/v1/widgets",          # NOT redacted
            },
        )

        # ── 5. ERROR with traceback (rule LG-05) ───────────
        try:
            int("not-a-number")
        except ValueError:
            # LOG.exception is the canonical shape inside an except —
            # equivalent to LOG.error(..., exc_info=True) but more
            # readable. The traceback lands in the JSON `exception`
            # field automatically.
            LOG.exception(
                "config parse failed",
                extra={"config_field": "port", "raw_value": "not-a-number"},
            )

        # ── 6. Operation start/end pair (rule LG-11) ──────
        do_use_case(
            LOG,
            user="alice",
            api_key="sk_live_xyz789",  # not actually logged here
            items=["widget-a", "widget-b", "widget-c"],
        )

        # ── 7. Inspect the JSON sink ──────────────────────
        _show_records(log_file, "Records in JSON sink")

        # ── 8. Show the redactor's effect explicitly ──────
        # Every value bound to a secret-bearing key is gone.
        full = log_file.read_text()
        for secret in ("hunter2", "sk_live_xyz789", "tok_secret_42"):
            assert secret not in full, f"redactor failed for {secret!r}"
        print("\n✓ secrets confirmed absent from JSON output:")
        for secret in ("hunter2", "sk_live_xyz789", "tok_secret_42"):
            print(f"    {secret!r}: not in log file")

        ctx.shutdown()


if __name__ == "__main__":
    main()
