#!/usr/bin/env python3
"""Engine ablation analysis: measure each primary engine's marginal contribution
to composite performance (primary + IA + parking).

Runs N+1 backtests (baseline + leave-one-out for each enabled engine) with
presets attached and reports which engines help vs harm composite returns.

Usage:
    # Leave-one-out analysis
    python -m apps.tools.run_engine_ablation \\
        --manifest configs/experiments/return_max_long_v7.70.json \\
        --parking qqqm_low_dd \\
        --idle-alpha micro_event_alpha_plus_event_plus \\
        --output-root /tmp/ablation_v7.70

    # Greedy pruning (iteratively removes worst composite-contributor)
    python -m apps.tools.run_engine_ablation \\
        --manifest configs/experiments/return_max_long_v7.70.json \\
        --parking qqqm_low_dd --idle-alpha micro_event_alpha_plus_event_plus \\
        --greedy-prune --output-root /tmp/ablation_v7.70

    # Risk budget sweep (test 0.5x/1.0x/1.5x for each engine's risk budget)
    python -m apps.tools.run_engine_ablation \\
        --manifest configs/experiments/return_max_long_v7.70.json \\
        --parking qqqm_low_dd --idle-alpha micro_event_alpha_plus_event_plus \\
        --budget-sweep --output-root /tmp/ablation_v7.70
"""
from __future__ import annotations

import argparse
import copy
import json
import subprocess
import sys
import tempfile
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_SNAPSHOT_DIR = ""


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Engine ablation analysis with composite scoring."
    )
    parser.add_argument("--manifest", required=True, help="Base experiment manifest path.")
    parser.add_argument("--parking", default="qqqm_low_dd", help="Cash parking preset.")
    parser.add_argument("--idle-alpha", default="micro_event_alpha_plus_event_plus", help="IA sleeve preset.")
    parser.add_argument("--snapshot-dir", default=DEFAULT_SNAPSHOT_DIR)
    parser.add_argument("--output-root", required=True, help="Root directory for run artifacts.")
    parser.add_argument("--split", default="all", help="Backtest split (default: 'all' = full period merging train+valid+test).")
    parser.add_argument(
        "--greedy-prune", action="store_true",
        help="After LOO, iteratively remove worst engine until no improvement possible.",
    )
    parser.add_argument(
        "--budget-sweep", action="store_true",
        help="Instead of removing engines, test 0.5x/1.5x risk budget for each engine.",
    )
    parser.add_argument(
        "--no-presets", action="store_true",
        help="Run without IA/parking presets (primary-only ablation).",
    )
    parser.add_argument(
        "--idle-alpha-dedup", default=None, choices=["skip", "rename"],
        help="IA dedup mode: 'rename' injects IA engines even when engine_id conflicts with primary (adds __ia_sleeve suffix).",
    )
    parser.add_argument("--form4-sleeve", default=None, help="Form4 insider sleeve preset name.")
    parser.add_argument("--ownership-sleeve", default=None, help="Ownership 13D sleeve preset name.")
    return parser.parse_args()


def _load_manifest(manifest_path: str) -> dict:
    return json.loads(Path(manifest_path).read_text())


def _get_enabled_engines(manifest: dict) -> list[dict]:
    return [e for e in manifest.get("strategy_engines", []) if e.get("enabled", True) is not False]


def _run_backtest(
    manifest_dict: dict,
    parking: str | None,
    idle_alpha: str | None,
    split: str | None,
    output_root: Path,
    run_label: str,
    snapshot_dir: str,
    idle_alpha_dedup: str | None = None,
    form4_sleeve: str | None = None,
    ownership_sleeve: str | None = None,
) -> dict[str, object]:
    """Write a temp manifest JSON and run the backtest, returning key metrics."""
    run_dir = output_root / run_label
    run_dir.mkdir(parents=True, exist_ok=True)

    with tempfile.NamedTemporaryFile(
        mode="w", suffix=".json", dir=run_dir, delete=False
    ) as f:
        json.dump(manifest_dict, f, indent=2)
        temp_manifest = f.name

    cmd = [
        sys.executable, "-m", "apps.backtester.run",
        "--manifest", temp_manifest,
        "--output-root", str(run_dir),
    ]
    if snapshot_dir:
        cmd += ["--snapshot-dir", snapshot_dir]
    if split:
        cmd += ["--split", split]
    if parking:
        cmd += ["--parking", parking]
    if idle_alpha:
        cmd += ["--idle-alpha", idle_alpha]
    if idle_alpha_dedup:
        cmd += ["--idle-alpha-dedup", idle_alpha_dedup]
    if form4_sleeve:
        cmd += ["--form4-sleeve", form4_sleeve]
    if ownership_sleeve:
        cmd += ["--ownership-sleeve", ownership_sleeve]

    print(f"  [{run_label}]...", end=" ", flush=True)
    result = subprocess.run(
        cmd,
        cwd=str(REPO_ROOT),
        stdout=subprocess.PIPE,
        stderr=subprocess.STDOUT,
        text=True,
        check=False,
    )
    if result.returncode != 0:
        print("FAILED")
        lines = (result.stdout or "").splitlines()
        for line in lines[-4:]:
            print(f"    {line}")
        return {"error": True, "run_label": run_label, "total_return_pct": None, "composite_amplification": None}

    # Find latest run dir
    bt_dirs = sorted(run_dir.glob("bt_*"), key=lambda p: p.stat().st_mtime)
    if not bt_dirs:
        print("NO_RUN_DIR")
        return {"error": True, "run_label": run_label, "total_return_pct": None}
    latest = bt_dirs[-1]

    metrics_path = latest / "metrics" / "metrics_summary.json"
    sleeve_path = latest / "metrics" / "sleeve_decomposition.json"
    summary = json.loads(metrics_path.read_text()) if metrics_path.exists() else {}
    sleeve = json.loads(sleeve_path.read_text()) if sleeve_path.exists() else {}

    ret = summary.get("total_return_pct")
    sharpe = summary.get("sharpe_ratio")
    dd = summary.get("max_drawdown_pct")
    amp = sleeve.get("composite_amplification")
    idle = sleeve.get("avg_idle_fraction_pct")
    print(f"CW={ret:.2f}% Sharpe={sharpe:.2f} DD={dd:.2f}% Idle={idle:.1f}%" if ret is not None else "no metrics")

    return {
        "run_label": run_label,
        "total_return_pct": ret,
        "sharpe_ratio": sharpe,
        "max_drawdown_pct": dd,
        "composite_amplification": amp,
        "avg_idle_fraction_pct": idle,
        "trade_count": summary.get("trade_count", 0),
        "error": False,
    }


def run_loo_ablation(
    base_manifest: dict,
    parking: str | None,
    idle_alpha: str | None,
    split: str | None,
    output_root: Path,
    snapshot_dir: str,
    idle_alpha_dedup: str | None = None,
    form4_sleeve: str | None = None,
    ownership_sleeve: str | None = None,
) -> tuple[dict, list[dict]]:
    """Run baseline + leave-one-out for each enabled engine. Returns (baseline, loo_results)."""
    engines = _get_enabled_engines(base_manifest)
    print(f"\n=== Leave-One-Out Ablation: {len(engines)} enabled engines ===")
    print(f"Total runs: {len(engines) + 1}\n")

    print("Running baseline...")
    baseline = _run_backtest(
        base_manifest, parking, idle_alpha, split,
        output_root / "loo", "baseline", snapshot_dir, idle_alpha_dedup,
        form4_sleeve, ownership_sleeve
    )

    loo_results: list[dict] = []
    for i, engine in enumerate(engines):
        engine_id = engine.get("engine_id", f"engine_{i}")
        # Build manifest with this engine removed
        mod = copy.deepcopy(base_manifest)
        mod["strategy_engines"] = [
            e for e in mod["strategy_engines"]
            if e.get("engine_id") != engine_id
        ]
        mod["experiment_name"] = f"{base_manifest.get('experiment_name', 'ablation')}_loo_{engine_id}"

        result = _run_backtest(
            mod, parking, idle_alpha, split,
            output_root / "loo", f"loo_{engine_id}", snapshot_dir, idle_alpha_dedup,
            form4_sleeve, ownership_sleeve
        )
        base_ret = baseline.get("total_return_pct")
        loo_ret = result.get("total_return_pct")
        delta = (loo_ret - base_ret) if (loo_ret is not None and base_ret is not None) else None
        result["engine_id"] = engine_id
        result["delta_return_pct"] = delta
        result["engine_risk_budget_pct"] = engine.get("engine_risk_budget_pct")
        loo_results.append(result)

    return baseline, loo_results


def run_budget_sweep(
    base_manifest: dict,
    parking: str | None,
    idle_alpha: str | None,
    split: str | None,
    output_root: Path,
    snapshot_dir: str,
    idle_alpha_dedup: str | None = None,
    form4_sleeve: str | None = None,
    ownership_sleeve: str | None = None,
) -> tuple[dict, list[dict]]:
    """Test 0.5x and 1.5x risk budget for each engine vs baseline."""
    engines = _get_enabled_engines(base_manifest)
    print(f"\n=== Budget Sweep: {len(engines)} engines × 2 levels ===")
    print(f"Total additional runs: {len(engines) * 2}\n")

    print("Running baseline...")
    baseline = _run_backtest(
        base_manifest, parking, idle_alpha, split,
        output_root / "budget_sweep", "baseline", snapshot_dir, idle_alpha_dedup,
        form4_sleeve, ownership_sleeve
    )

    sweep_results: list[dict] = []
    for i, engine in enumerate(engines):
        engine_id = engine.get("engine_id", f"engine_{i}")
        orig_budget = engine.get("engine_risk_budget_pct", 1.0)
        for scale, label in [(0.5, "half"), (1.5, "boost")]:
            mod = copy.deepcopy(base_manifest)
            for e in mod["strategy_engines"]:
                if e.get("engine_id") == engine_id:
                    e["engine_risk_budget_pct"] = orig_budget * scale
            mod["experiment_name"] = f"sweep_{engine_id}_{label}"

            result = _run_backtest(
                mod, parking, idle_alpha, split,
                output_root / "budget_sweep", f"{engine_id}_{label}", snapshot_dir, idle_alpha_dedup,
                form4_sleeve, ownership_sleeve
            )
            base_ret = baseline.get("total_return_pct")
            sweep_ret = result.get("total_return_pct")
            delta = (sweep_ret - base_ret) if (sweep_ret is not None and base_ret is not None) else None
            result["engine_id"] = engine_id
            result["scale"] = scale
            result["scale_label"] = label
            result["delta_return_pct"] = delta
            result["orig_budget"] = orig_budget
            result["new_budget"] = orig_budget * scale
            sweep_results.append(result)

    return baseline, sweep_results


def run_greedy_prune(
    base_manifest: dict,
    parking: str | None,
    idle_alpha: str | None,
    split: str | None,
    output_root: Path,
    snapshot_dir: str,
    idle_alpha_dedup: str | None = None,
    form4_sleeve: str | None = None,
    ownership_sleeve: str | None = None,
) -> list[str]:
    """Iteratively remove the engine with highest positive delta (least hurts or improves composite).
    Returns list of removed engine_ids in removal order."""
    current_manifest = copy.deepcopy(base_manifest)
    removed: list[str] = []
    round_num = 0

    print("\n=== Greedy Pruning ===")
    while True:
        round_num += 1
        print(f"\n--- Round {round_num} ---")
        baseline, loo_results = run_loo_ablation(
            current_manifest, parking, idle_alpha, split,
            output_root / f"greedy_round_{round_num}", snapshot_dir, idle_alpha_dedup,
            form4_sleeve, ownership_sleeve
        )
        valid = [r for r in loo_results if not r.get("error") and r.get("delta_return_pct") is not None]
        if not valid:
            print("No valid LOO results, stopping.")
            break
        # Find engine whose removal gives highest delta (most beneficial or least harmful to remove)
        best = max(valid, key=lambda r: r["delta_return_pct"])
        if best["delta_return_pct"] <= 0:
            print(f"Best removal delta={best['delta_return_pct']:.2f}pp (non-positive). Stopping.")
            break
        engine_id = best["engine_id"]
        delta = best["delta_return_pct"]
        print(f"Removing '{engine_id}' (delta={delta:+.2f}pp)")
        removed.append(engine_id)
        current_manifest["strategy_engines"] = [
            e for e in current_manifest["strategy_engines"]
            if e.get("engine_id") != engine_id
        ]

    print(f"\nGreedy pruning complete. Removed {len(removed)} engines: {removed}")
    # Save the pruned manifest
    pruned_name = base_manifest.get("experiment_name", "ablation") + "_pruned"
    current_manifest["experiment_name"] = pruned_name
    pruned_path = output_root / f"{pruned_name}.json"
    pruned_path.write_text(json.dumps(current_manifest, indent=2))
    print(f"Pruned manifest saved to: {pruned_path}")
    return removed


def _fmt(v: object, decimals: int = 2) -> str:
    if v is None:
        return "N/A"
    try:
        return f"{float(v):.{decimals}f}"  # type: ignore[arg-type]
    except (TypeError, ValueError):
        return str(v)


def print_loo_table(baseline: dict, loo_results: list[dict]) -> None:
    base_ret = baseline.get("total_return_pct")
    print(f"\n{'Engine':<55} {'Without%':>9} {'Baseline%':>10} {'Δpp':>7} {'Action':>8}")
    print("-" * 95)
    sorted_loo = sorted(
        loo_results,
        key=lambda r: (r.get("delta_return_pct") is None, r.get("delta_return_pct") or 0.0),
        reverse=True,
    )
    for r in sorted_loo:
        engine_id = str(r.get("engine_id", "?"))[:54]
        without = _fmt(r.get("total_return_pct"))
        delta = r.get("delta_return_pct")
        delta_str = f"{delta:+.2f}" if delta is not None else "N/A"
        action = ""
        if delta is not None:
            action = "REMOVE!" if delta > 0 else "KEEP"
        print(f"{engine_id:<55} {without:>9} {_fmt(base_ret):>10} {delta_str:>7} {action:>8}")
    print("-" * 95)
    print(f"{'BASELINE':<55} {_fmt(base_ret):>9}")


def print_budget_sweep_table(baseline: dict, sweep_results: list[dict]) -> None:
    base_ret = baseline.get("total_return_pct")
    print(f"\n{'Engine':<45} {'Scale':>6} {'Budget':>7} {'CW%':>8} {'Δpp':>7}")
    print("-" * 80)
    for r in sweep_results:
        engine_id = str(r.get("engine_id", "?"))[:44]
        scale = r.get("scale_label", "?")
        budget = _fmt(r.get("new_budget"), decimals=4)
        cw = _fmt(r.get("total_return_pct"))
        delta = r.get("delta_return_pct")
        delta_str = f"{delta:+.2f}" if delta is not None else "N/A"
        print(f"{engine_id:<45} {scale:>6} {budget:>7} {cw:>8} {delta_str:>7}")
    print("-" * 80)
    print(f"{'BASELINE':>58} {_fmt(base_ret):>8}")


def main() -> None:
    args = parse_args()
    output_root = Path(args.output_root)
    output_root.mkdir(parents=True, exist_ok=True)

    base_manifest = _load_manifest(args.manifest)
    exp_name = base_manifest.get("experiment_name", Path(args.manifest).stem)
    parking = None if args.no_presets else args.parking
    idle_alpha = None if args.no_presets else getattr(args, "idle_alpha")

    print(f"Engine Ablation: {exp_name}")
    print(f"Presets: parking={parking}, idle_alpha={idle_alpha}")
    engines = _get_enabled_engines(base_manifest)
    print(f"Enabled engines: {len(engines)}")

    idle_alpha_dedup = None if args.no_presets else getattr(args, "idle_alpha_dedup", None)
    form4_sleeve = None if args.no_presets else getattr(args, "form4_sleeve", None)
    ownership_sleeve = None if args.no_presets else getattr(args, "ownership_sleeve", None)

    if args.greedy_prune:
        removed = run_greedy_prune(
            base_manifest, parking, idle_alpha, args.split,
            output_root, args.snapshot_dir, idle_alpha_dedup,
            form4_sleeve, ownership_sleeve
        )
        print(f"\nFinal removed engines: {removed}")
    elif args.budget_sweep:
        baseline, sweep_results = run_budget_sweep(
            base_manifest, parking, idle_alpha, args.split,
            output_root, args.snapshot_dir, idle_alpha_dedup,
            form4_sleeve, ownership_sleeve
        )
        print_budget_sweep_table(baseline, sweep_results)
        # Save results
        out = output_root / "budget_sweep_results.json"
        out.write_text(json.dumps({"baseline": baseline, "sweep": sweep_results}, indent=2))
        print(f"\nResults saved to: {out}")
    else:
        # Default: LOO ablation
        baseline, loo_results = run_loo_ablation(
            base_manifest, parking, idle_alpha, args.split,
            output_root, args.snapshot_dir, idle_alpha_dedup,
            form4_sleeve, ownership_sleeve
        )
        print_loo_table(baseline, loo_results)
        # Save results
        out = output_root / "loo_results.json"
        out.write_text(json.dumps({"baseline": baseline, "loo": loo_results}, indent=2))
        print(f"\nResults saved to: {out}")


if __name__ == "__main__":
    main()
