#!/usr/bin/env python3
"""Tier 2 behavioural exit check (Plan 5 Task 4).

Consumes the aggregated metrics JSON produced by
`scripts/aggregate-multi-seed-metrics.py` (Plan 5 Task 1B.2).

Pass criteria (per spec §2 Tier 2 — behavioural):
  1. `val_trades_per_bar` mean ≥ 0.005 on stable epochs (model trades).
  2. `val_active_frac` mean > 0.2 on stable epochs (model is not always Hold).
  3. Direction-argmax entropy > 0.8 × log(4) ≈ 1.109 on stable epochs.
     Preferred source: a `val_dir_entropy` aggregate metric. Fallback:
     derive from `val_dir_dist_*` (per-direction probability) if present.
     If neither is in the aggregate, FAIL with an explanatory message so
     the missing metric gets added to HEALTH_DIAG in a follow-up run.

Exits 0 on PASS, 1 on FAIL. Each check prints a `PASS:` / `FAIL:` line.
"""
from __future__ import annotations

import argparse
import json
import math
import sys
from statistics import mean


# 0.8 * log(4) ≈ 1.10903549
DIR_ENTROPY_FLOOR = 0.8 * math.log(4.0)


def _stable(series, warmup_end):
    return [x for x in series if x["epoch"] >= warmup_end]


def check_trades_per_bar(agg, warmup_end):
    series = agg.get("val_trades_per_bar")
    if not series:
        return False, (
            "val_trades_per_bar missing from aggregate — required for Tier 2"
        )
    stable = _stable(series, warmup_end)
    if not stable:
        return False, (
            f"No stable epochs for val_trades_per_bar past warmup_end={warmup_end}"
        )
    m = mean(x["mean"] for x in stable)
    if m < 0.005:
        return False, f"val_trades_per_bar mean = {m:.5f} < 0.005"
    return True, f"val_trades_per_bar mean = {m:.5f} >= 0.005"


def check_active_frac(agg, warmup_end):
    series = agg.get("val_active_frac")
    if not series:
        return False, (
            "val_active_frac missing from aggregate — required for Tier 2"
        )
    stable = _stable(series, warmup_end)
    if not stable:
        return False, (
            f"No stable epochs for val_active_frac past warmup_end={warmup_end}"
        )
    m = mean(x["mean"] for x in stable)
    if m <= 0.2:
        return False, f"val_active_frac mean = {m:.4f} <= 0.2"
    return True, f"val_active_frac mean = {m:.4f} > 0.2"


def _entropy_from_dist(probs):
    """Shannon entropy in nats; ignores non-positive probabilities."""
    return -sum(p * math.log(p) for p in probs if p > 0)


def check_dir_entropy(agg, warmup_end):
    """Direction argmax entropy > 0.8 × log(4).

    Prefers a pre-computed `val_dir_entropy` aggregate. Falls back to
    deriving entropy from per-direction probability mass (`val_dir_dist_short`
    / `val_dir_dist_hold` / `val_dir_dist_long` / `val_dir_dist_flat` —
    naming kept loose to match whatever HEALTH_DIAG ends up emitting).
    """
    direct = agg.get("val_dir_entropy")
    if direct:
        stable = _stable(direct, warmup_end)
        if not stable:
            return False, (
                f"No stable epochs for val_dir_entropy past warmup_end={warmup_end}"
            )
        m = mean(x["mean"] for x in stable)
        if m <= DIR_ENTROPY_FLOOR:
            return False, (
                f"val_dir_entropy mean = {m:.4f} <= {DIR_ENTROPY_FLOOR:.4f} "
                f"(0.8 × log 4)"
            )
        return True, (
            f"val_dir_entropy mean = {m:.4f} > {DIR_ENTROPY_FLOOR:.4f} "
            f"(0.8 × log 4)"
        )

    # Fallback: derive from per-direction distribution.
    dist_keys = [
        "val_dir_dist_short",
        "val_dir_dist_hold",
        "val_dir_dist_long",
        "val_dir_dist_flat",
    ]
    dist_series = [agg.get(k) for k in dist_keys]
    if all(s for s in dist_series):
        # Align by epoch on stable epochs.
        stables = [_stable(s, warmup_end) for s in dist_series]
        if any(not s for s in stables):
            return False, (
                f"No stable epochs for val_dir_dist_* past warmup_end={warmup_end}"
            )
        # Average per-direction mean across stable epochs, then compute entropy.
        means = [mean(x["mean"] for x in s) for s in stables]
        total = sum(means)
        if total <= 0:
            return False, "val_dir_dist_* sums to <= 0; cannot compute entropy"
        probs = [m / total for m in means]
        ent = _entropy_from_dist(probs)
        if ent <= DIR_ENTROPY_FLOOR:
            return False, (
                f"derived dir entropy = {ent:.4f} <= {DIR_ENTROPY_FLOOR:.4f}"
            )
        return True, (
            f"derived dir entropy = {ent:.4f} > {DIR_ENTROPY_FLOOR:.4f}"
        )

    return False, (
        "neither val_dir_entropy nor val_dir_dist_* present in aggregate — "
        "add a direction-distribution metric to HEALTH_DIAG to enable "
        "Tier 2 entropy check"
    )


def main():
    ap = argparse.ArgumentParser(
        description="Tier 2 behavioural exit check (Plan 5 Task 4).",
    )
    ap.add_argument(
        "metrics_json",
        help="Aggregated metrics JSON (output of aggregate-multi-seed-metrics.py).",
    )
    ap.add_argument(
        "--warmup-end",
        type=int,
        default=15,
        help="Epoch at which warmup ends (stable epochs are > this); default 15.",
    )
    args = ap.parse_args()

    try:
        with open(args.metrics_json) as fh:
            data = json.load(fh)
    except (OSError, json.JSONDecodeError) as exc:
        print(f"FAIL: cannot read {args.metrics_json}: {exc}")
        sys.exit(1)

    agg = data.get("aggregates")
    if not isinstance(agg, dict):
        print("FAIL: missing or malformed `aggregates` block in metrics JSON")
        sys.exit(1)

    checks = [
        check_trades_per_bar(agg, args.warmup_end),
        check_active_frac(agg, args.warmup_end),
        check_dir_entropy(agg, args.warmup_end),
    ]

    all_pass = all(ok for ok, _ in checks)
    for ok, msg in checks:
        print(("PASS" if ok else "FAIL") + ": " + msg)
    sys.exit(0 if all_pass else 1)


if __name__ == "__main__":
    main()
