"""``python -m headroom.cli.toin_publish`` — emit ``recommendations.toml``.

PR-B5 retired TOIN's request-time hint API. Recommendations now flow
through this offline CLI: we walk the on-disk TOIN store, aggregate one
row per ``(auth_mode, model_family, structure_hash)`` slice that has at
least ``--min-observations`` recorded compression events, and write the
result as TOML the Rust proxy loads at startup
(``$HEADROOM_RECOMMENDATIONS_PATH`` / ``./recommendations.toml``).

# TOML schema

::

    [[recommendation]]
    auth_mode = "payg"
    model_family = "claude-3-5"
    structure_hash = "deadbeef..."
    strategy_hint = "smart_crusher"
    confidence = 0.87
    observations = 142

# Why a CLI, not a library hook

Per-request mutation is exactly the dangerous coupling PR-B5 retired.
Publishing happens at deploy boundaries — never inside a request. The
deploy pipeline runs ``python -m headroom.cli.toin_publish ...`` after
draining the TOIN store from production, ships the resulting TOML
alongside the Rust binary, and the proxy reads it once at startup.

# Output stability

Rows are sorted by ``(auth_mode, model_family, structure_hash)`` so the
file diffs cleanly across publishes. Strategies ship as the
ToolPattern's learned ``optimal_strategy`` (or the dominant entry of
``strategy_success_rates``). Confidence is the pattern's existing
confidence score — bounded ``[0.0, 0.95]`` by the confidence calculator.
"""

from __future__ import annotations

import argparse
import logging
import sys
from collections.abc import Iterable
from pathlib import Path
from typing import Any, Final

from headroom.telemetry.toin import (
    DEFAULT_MIN_OBSERVATIONS_TO_PUBLISH,
    PatternKey,
    ToolIntelligenceNetwork,
    ToolPattern,
    get_toin,
)

logger = logging.getLogger(__name__)

# Header annotated into every emitted file so ops can grep for "where
# did this TOML come from?" without having to remember the publish
# command. Newlines preserved as literal "\n" inside the string.
TOML_HEADER: Final[str] = (
    "# Auto-generated by `python -m headroom.cli.toin_publish`.\n"
    "# DO NOT EDIT BY HAND — re-run the publish CLI to regenerate.\n"
    "# Loaded by the Rust proxy at startup; see\n"
    "# `crates/headroom-core/src/transforms/recommendations.rs`.\n\n"
)


def _select_strategy(pattern: ToolPattern) -> str:
    """Pick the strategy hint to publish for a pattern.

    Priority:
    1. ``optimal_strategy`` if the pattern has explicitly recorded one
       and it isn't the placeholder ``"default"``.
    2. The strategy with the highest success rate, if any have been
       recorded.
    3. ``"default"`` — meaning "no opinion; the proxy uses its static
       default strategy."
    """
    if pattern.optimal_strategy and pattern.optimal_strategy != "default":
        return pattern.optimal_strategy
    if pattern.strategy_success_rates:
        return max(pattern.strategy_success_rates.items(), key=lambda kv: kv[1])[0]
    return "default"


def _toml_escape(s: str) -> str:
    """Escape a string for inclusion as a basic TOML string literal."""
    # TOML basic strings need backslash + quote escapes only; the values
    # we publish (auth_mode, model_family, hex hashes, identifier
    # strategies) never contain control characters in practice, but we
    # encode defensively per RFC.
    return s.replace("\\", "\\\\").replace('"', '\\"')


def _format_row(
    *,
    auth_mode: str,
    model_family: str,
    structure_hash: str,
    strategy_hint: str,
    confidence: float,
    observations: int,
) -> str:
    """Render one ``[[recommendation]]`` block."""
    return (
        "[[recommendation]]\n"
        f'auth_mode = "{_toml_escape(auth_mode)}"\n'
        f'model_family = "{_toml_escape(model_family)}"\n'
        f'structure_hash = "{_toml_escape(structure_hash)}"\n'
        f'strategy_hint = "{_toml_escape(strategy_hint)}"\n'
        f"confidence = {confidence:.4f}\n"
        f"observations = {observations}\n"
    )


def _eligible_rows(
    patterns: Iterable[tuple[PatternKey, ToolPattern]],
    min_observations: int,
) -> list[dict[str, Any]]:
    """Filter and sort pattern slices into deterministic publish rows.

    Observations source: ``ToolPattern.total_compressions`` is the
    canonical "how many events fed this slice" counter — incremented
    once per ``record_compression`` call. The legacy ``observations``
    field counted ``get_recommendation`` invocations (retired in PR-B5)
    and now stays at zero, so we use ``total_compressions`` instead.
    """
    rows: list[dict[str, Any]] = []
    for key, pattern in patterns:
        observations = pattern.total_compressions
        if observations < min_observations:
            continue
        auth_mode, model_family, sig_hash = key
        rows.append(
            {
                "auth_mode": auth_mode,
                "model_family": model_family,
                "structure_hash": sig_hash,
                "strategy_hint": _select_strategy(pattern),
                "confidence": float(pattern.confidence),
                "observations": observations,
            }
        )

    rows.sort(
        key=lambda r: (r["auth_mode"], r["model_family"], r["structure_hash"]),
    )
    return rows


def publish(
    *,
    output_path: Path,
    min_observations: int = DEFAULT_MIN_OBSERVATIONS_TO_PUBLISH,
    toin: ToolIntelligenceNetwork | None = None,
) -> int:
    """Aggregate the live TOIN store and write ``recommendations.toml``.

    Args:
        output_path: Destination TOML path. Parent directories must
            already exist.
        min_observations: Minimum ``total_compressions`` per slice
            before we emit a row. Anything below this is noise.
        toin: Optional pre-built TOIN handle (tests pass an isolated
            instance). Defaults to the global singleton.

    Returns:
        The number of recommendation rows written.
    """
    handle = toin if toin is not None else get_toin()
    rows = _eligible_rows(handle.iter_patterns(), min_observations=min_observations)

    body = TOML_HEADER + "\n".join(_format_row(**row) for row in rows)
    if rows:
        # Trailing newline keeps POSIX tools happy and round-trips
        # cleanly through `tomllib.loads`.
        body += "\n"

    output_path.write_text(body, encoding="utf-8")
    logger.info(
        "TOIN publish complete",
        extra={
            "event": "toin_publish",
            "path": str(output_path),
            "rows": len(rows),
            "min_observations": min_observations,
        },
    )
    return len(rows)


def _build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="python -m headroom.cli.toin_publish",
        description=(
            "Aggregate the on-disk TOIN store and emit recommendations.toml "
            "for the Rust proxy to load at startup."
        ),
    )
    parser.add_argument(
        "--output",
        "-o",
        type=Path,
        default=Path("recommendations.toml"),
        help="Output TOML path (default: ./recommendations.toml).",
    )
    parser.add_argument(
        "--min-observations",
        type=int,
        default=DEFAULT_MIN_OBSERVATIONS_TO_PUBLISH,
        help=(
            "Minimum total_compressions per slice before a row is emitted "
            f"(default: {DEFAULT_MIN_OBSERVATIONS_TO_PUBLISH})."
        ),
    )
    parser.add_argument(
        "--verbose",
        "-v",
        action="store_true",
        help="Enable info-level logging on stderr.",
    )
    return parser


def main(argv: list[str] | None = None) -> int:
    """Entry point. Returns the exit code for ``__main__``."""
    parser = _build_parser()
    args = parser.parse_args(argv)

    if args.verbose:
        logging.basicConfig(level=logging.INFO, stream=sys.stderr)

    if args.min_observations < 1:
        parser.error("--min-observations must be >= 1")

    output: Path = args.output
    output.parent.mkdir(parents=True, exist_ok=True)

    rows = publish(output_path=output, min_observations=args.min_observations)
    print(f"wrote {rows} recommendation rows to {output}", file=sys.stderr)
    return 0


if __name__ == "__main__":  # pragma: no cover - module entrypoint
    raise SystemExit(main())
