"""``mgf-fed bench`` — microbench numbers for hot paths.

Closes the strategic-review §3.3 finding: the library claims
"fast" without numbers. This subcommand prints microbench numbers
for the load-bearing primitives so consumers (and CI) can see
regressions instantly.

Measurements (runs in <5 seconds total; numbers are
nanoseconds-per-call):

  - ``current_context()`` — the contextvar lookup that runs on
    every log call's identity-resolution path.
  - ``bootstrap()`` cold + warm — full lifecycle wiring, with vs
    without re-entry overhead.
  - ``operation_span()`` open/close — OTel span allocation when
    OTel is configured.
  - one log call (sync) — JSON formatter + redaction roundtrip,
    standard StreamHandler.
  - one log call (async) — same record path, but through
    :class:`AsyncJsonHandler` (queue put — drain happens off-CPU).
  - one redaction pass — the redactor's hot path on a typical
    record dict.

Usage:

.. code-block:: bash

    $ mgf-fed bench
    mgf-common 0.7.0 — microbench (n iterations per row)

    operation                                 ns/call    iterations
    ─────────────────────────────────────────────────────────────────
    current_context()                              28      1_000_000
    bootstrap (cold)                          124_561              1
    bootstrap (warm)                            5_223          1_000
    operation_span open+close                     180        100_000
    log call (sync, JSON)                       4_812         10_000
    log call (async, JSON)                        612         10_000
    redaction pass (10-key dict)                  890         10_000
    ─────────────────────────────────────────────────────────────────
    total walltime: 1.34 s

Track regressions in ``docs/ops/benchmarks.md`` — every
release re-runs this and we record the numbers.
"""

from __future__ import annotations

import io
import logging
import os
import time
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    import argparse


def add_bench_args(parser: argparse.ArgumentParser) -> None:
    """Register the ``bench`` subcommand's arguments.

    No required arguments — the subcommand is a fixed-shape report.
    ``bench rebaseline`` re-records the relative perf baselines (PF-04/05).
    """
    parser.add_argument(
        "subaction",
        nargs="?",
        choices=["rebaseline"],
        default=None,
        help=(
            "`rebaseline` re-records the perf baselines in "
            "tests/perf/baselines.json (PF-04) — runs the perf suite with "
            "MGF_PERF_REBASELINE=1. Commit the result in a SEPARATE PR whose "
            "body explains WHY the baseline moved."
        ),
    )
    parser.add_argument(
        "--metric",
        default=None,
        help="With `rebaseline`: re-record only this metric (pytest -k match).",
    )
    parser.add_argument(
        "--quick",
        action="store_true",
        help=(
            "Run a faster, lower-fidelity pass (fewer iterations). "
            "Default is a 5-second-budget run; --quick targets ~1s."
        ),
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help=(
            "Print the results as JSON instead of the human-readable "
            "table — useful for machine consumption (CI dashboards)."
        ),
    )


def _run_rebaseline(metric: str | None) -> int:
    """Re-record perf baselines by running tests/perf under MGF_PERF_REBASELINE
    (PF-04/05). Available only from a source checkout."""
    import subprocess
    import sys
    from pathlib import Path

    perf_dir = Path(__file__).resolve().parents[4] / "tests" / "perf"
    if not perf_dir.is_dir():
        print(
            "bench rebaseline: tests/perf/ not found — this command runs only "
            "from an mgf-common source checkout (it re-records the library's "
            "own baselines).",
            file=sys.stderr,
        )
        return 2
    argv = [
        sys.executable,
        "-m",
        "pytest",
        str(perf_dir),
        "-m",
        "perf",
        "-o",
        "addopts=",
        "-q",
    ]
    if metric:
        argv += ["-k", metric]
    env = {**os.environ, "MGF_PERF_REBASELINE": "1"}
    target = metric or "ALL metrics"
    print(
        f"Re-recording perf baselines ({target}) into tests/perf/baselines.json…\n"
        "PF-04: commit the result in a SEPARATE PR explaining WHY it moved.\n"
    )
    try:
        return subprocess.run(  # SC-07: bounded
            argv,
            env=env,
            check=False,
            timeout=int(os.environ.get("MGF_FED_BENCH_TIMEOUT_S", "1200")),
        ).returncode
    except subprocess.TimeoutExpired:
        print("perf rebaseline timed out", flush=True)
        return 1


def run_bench(args: argparse.Namespace) -> int:
    """Execute the benchmark suite + print the report."""
    if getattr(args, "subaction", None) == "rebaseline":
        return _run_rebaseline(getattr(args, "metric", None))

    import warnings

    from mgf.common import UnconfiguredWarning

    iterations_scale = 0.2 if getattr(args, "quick", False) else 1.0

    # Silence the once-per-process UnconfiguredWarning. The first
    # bench measures current_context() which the bootstrap-not-yet-run
    # warning catches; that's expected — we trigger bootstrap on the
    # next row.
    warnings.simplefilter("ignore", UnconfiguredWarning)

    # Silence library audit logging during measurement so the table
    # output isn't interleaved with `bootstrap called again` lines
    # (the bench *intentionally* re-enters bootstrap to measure warm
    # cost; it would be wrong for those audits to show up). bootstrap()
    # itself re-wires logging on every call, so per-logger filter is
    # the only stable suppression — we re-disable the lifecycle logger
    # before each row that triggers a bootstrap event.
    lifecycle_logger = logging.getLogger("mgf.common._lifecycle")
    lifecycle_logger.disabled = True

    try:
        rows: list[_BenchRow] = []
        walltime_start = time.perf_counter()

        rows.append(_bench_current_context(iterations_scale))
        rows.append(_bench_bootstrap_cold())
        lifecycle_logger.disabled = True  # bootstrap re-wired; re-disable.
        rows.append(_bench_bootstrap_warm(iterations_scale))
        lifecycle_logger.disabled = True
        rows.append(_bench_operation_span(iterations_scale))
        rows.append(_bench_log_sync(iterations_scale))
        rows.append(_bench_log_async(iterations_scale))
        rows.append(_bench_redaction(iterations_scale))

        walltime = time.perf_counter() - walltime_start
    finally:
        lifecycle_logger.disabled = False

    if getattr(args, "json", False):
        _print_json(rows, walltime)
    else:
        _print_table(rows, walltime)

    return 0


# ---------------------------------------------------------------------------
# Bench row dataclass-light. Keep dependency-free.
# ---------------------------------------------------------------------------


class _BenchRow:
    __slots__ = ("iterations", "name", "ns_per_call")

    def __init__(self, name: str, iterations: int, ns_per_call: float) -> None:
        self.name = name
        self.iterations = iterations
        self.ns_per_call = ns_per_call


def _measure(name: str, iterations: int, fn: Any) -> _BenchRow:
    """Run ``fn`` ``iterations`` times; return ns/call."""
    # Warmup — Python's import / cache effects shouldn't pollute.
    for _ in range(min(100, iterations)):
        fn()
    start = time.perf_counter_ns()
    for _ in range(iterations):
        fn()
    elapsed = time.perf_counter_ns() - start
    return _BenchRow(name, iterations, elapsed / iterations)


# ---------------------------------------------------------------------------
# Individual benchmarks. Each returns one _BenchRow.
# ---------------------------------------------------------------------------


def _bench_current_context(scale: float) -> _BenchRow:
    from mgf.common import current_context

    iterations = max(1, int(1_000_000 * scale))
    return _measure("current_context()", iterations, current_context)


def _bench_bootstrap_cold() -> _BenchRow:
    """Cold = no prior bootstrap state. Single call."""
    from mgf.common import bootstrap, reset_context

    # reset_context() clears the process-wide context (genuinely cold) and
    # restores the prior on exit; shut the bench's own context down inside so it
    # doesn't leak. Subsequent warm benches self-warm on their first call.
    with reset_context():
        start = time.perf_counter_ns()
        ctx = bootstrap(app_name="bench-app", app_version="0.0.1")
        elapsed = time.perf_counter_ns() - start
        ctx.shutdown()

    return _BenchRow("bootstrap (cold)", 1, float(elapsed))


def _bench_bootstrap_warm(scale: float) -> _BenchRow:
    """Warm = bootstrap re-entry. Re-using the existing context."""
    from mgf.common import bootstrap

    iterations = max(10, int(1_000 * scale))

    def _do() -> None:
        bootstrap(app_name="bench-app", app_version="0.0.1")

    return _measure("bootstrap (warm)", iterations, _do)


def _bench_operation_span(scale: float) -> _BenchRow:
    from mgf.common.observability import operation_span

    iterations = max(1_000, int(100_000 * scale))

    def _do() -> None:
        with operation_span("bench"):
            pass

    return _measure("operation_span open+close", iterations, _do)


def _bench_log_sync(scale: float) -> _BenchRow:
    """One log call through the JSON formatter (sync handler)."""
    from mgf.common.observability import JsonFormatter, Redactor

    iterations = max(1_000, int(10_000 * scale))

    sink = io.StringIO()
    handler = logging.StreamHandler(sink)
    handler.setFormatter(JsonFormatter(redactor=Redactor.default()))
    logger = logging.getLogger("mgf-bench-sync")
    logger.handlers = [handler]
    logger.setLevel(logging.INFO)
    logger.propagate = False

    def _do() -> None:
        logger.info("benchmark log line", extra={"key": "value"})

    return _measure("log call (sync, JSON)", iterations, _do)


def _bench_log_async(scale: float) -> _BenchRow:
    """One log call through AsyncJsonHandler (queue put, async drain)."""
    from mgf.common.observability import (
        AsyncJsonHandler,
        JsonFormatter,
        Redactor,
    )

    iterations = max(1_000, int(10_000 * scale))

    sink = io.StringIO()
    target = logging.StreamHandler(sink)
    target.setFormatter(JsonFormatter(redactor=Redactor.default()))
    handler = AsyncJsonHandler(target=target, max_queue_size=iterations + 1000)

    logger = logging.getLogger("mgf-bench-async")
    logger.handlers = [handler]
    logger.setLevel(logging.INFO)
    logger.propagate = False

    def _do() -> None:
        logger.info("benchmark log line", extra={"key": "value"})

    try:
        return _measure("log call (async, JSON)", iterations, _do)
    finally:
        handler.close()


def _bench_redaction(scale: float) -> _BenchRow:
    """One redaction pass on a typical 10-key record."""
    from mgf.common.observability import Redactor

    iterations = max(1_000, int(10_000 * scale))
    redactor = Redactor.default()
    record_template = {
        "user_id": "alice",
        "request_id": "abc-123",
        "url": "https://example.com/api",
        "method": "POST",
        "duration_ms": 42,
        "password": "supersecret",  # will be redacted
        "token": "Bearer xyz",  # will be redacted
        "ip": "10.0.0.1",
        "trace_id": "0123456789abcdef",
        "service": "mgf-bench",
    }

    def _do() -> None:
        # Redactor.redact mutates in-place; pass a fresh copy each call.
        record = dict(record_template)
        redactor.redact(record)

    return _measure("redaction pass (10-key dict)", iterations, _do)


# ---------------------------------------------------------------------------
# Output: human-readable table + JSON.
# ---------------------------------------------------------------------------


def _print_table(rows: list[_BenchRow], walltime: float) -> None:
    from mgf.common import __version__

    print(
        f"mgf-common {__version__} — microbench (n iterations per row; ns = nanoseconds)",
    )
    print()
    print(f"{'operation':<35}{'ns/call':>12}{'iterations':>15}")
    print("─" * 62)
    for row in rows:
        print(
            f"{row.name:<35}{_format_ns(row.ns_per_call):>12}{row.iterations:>15_}",
        )
    print("─" * 62)
    print(f"total walltime: {walltime:.2f} s")
    print()
    print(
        "Track regressions in docs/ops/benchmarks.md.",
    )


def _print_json(rows: list[_BenchRow], walltime: float) -> None:
    import json

    from mgf.common import __version__

    payload = {
        "version": __version__,
        "platform": os.uname().sysname.lower() if hasattr(os, "uname") else "",
        "walltime_s": walltime,
        "rows": [
            {
                "name": r.name,
                "ns_per_call": r.ns_per_call,
                "iterations": r.iterations,
            }
            for r in rows
        ],
    }
    print(json.dumps(payload, indent=2))


def _format_ns(ns: float) -> str:
    """Format nanoseconds with thousands separators."""
    if ns < 1000:
        return f"{ns:.0f}"
    return f"{ns:_.0f}"


__all__ = ["add_bench_args", "run_bench"]
