"""Probe additivity from peer-relative surprise features on top of idle-alpha.

This is a standalone research tool. It does not modify shared runtime code.

The probe compares a baseline experiment config against variants that add
peer-relative surprise filters to the existing post-allocation idle-alpha
earnings engines.

Example:
    python -m apps.tools.peer_relative_idle_alpha_probe \
        --config configs/experiments/return_max_long_v17.1_idle_alpha_extended.json \
        --start 2022-01-01 \
        --end 2026-03-31 \
        --period-start y2026=2026-01-01
"""
from __future__ import annotations

import argparse
import datetime as dt
import json
from collections import Counter, defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from apps.backtester.run import BacktestRunner, _build_merged_snapshot_store, load_manifest, resolve_config
from libs.backtest.artifacts import _resolve_trade_sleeve
from libs.common.logging import configure_logging


DEFAULT_CONFIG = "configs/experiments/return_max_long_v17.1_idle_alpha_extended.json"
TARGET_EVENT_TYPE = "earnings_release"
TARGET_SCOPE_BULLISH_ENGINE_ID = "next_open_long_bullish_raised_strong"


@dataclass(frozen=True)
class VariantSpec:
    name: str
    scope: str
    peer_sector_event_count_365d_min: float | None = None
    peer_relative_surprise_pct_365d_min: float | None = None
    peer_relative_sue_hist_mean_4q_365d_min: float | None = None


def _parse_date(value: str, *, is_end: bool = False) -> dt.date:
    parts = value.split("-")
    if len(parts) == 1 and len(value) == 4 and value.isdigit():
        year = int(value)
        return dt.date(year, 12, 31) if is_end else dt.date(year, 1, 1)
    if len(parts) == 2 and all(part.isdigit() for part in parts):
        year = int(parts[0])
        month = int(parts[1])
        if is_end:
            next_month = dt.date(year + (month // 12), (month % 12) + 1, 1)
            return next_month - dt.timedelta(days=1)
        return dt.date(year, month, 1)
    return dt.date.fromisoformat(value)


def _parse_period_start(value: str) -> tuple[str, dt.date]:
    if "=" in value:
        label, raw_date = value.split("=", 1)
    else:
        label, raw_date = value, value
    return label, _parse_date(raw_date)


def _period_stats(curve: list[Any], start_date: dt.date) -> dict[str, Any] | None:
    points = [(state.date, float(state.equity)) for state in curve if state.date >= start_date]
    if not points:
        return None
    start_equity = points[0][1]
    peak = start_equity
    max_dd_pct = 0.0
    for _, equity in points:
        peak = max(peak, equity)
        if peak > 0:
            max_dd_pct = max(max_dd_pct, (peak - equity) / peak * 100.0)
    end_equity = points[-1][1]
    return {
        "start_date": points[0][0].isoformat(),
        "end_date": points[-1][0].isoformat(),
        "return_pct": (end_equity / start_equity - 1.0) * 100.0 if start_equity else 0.0,
        "max_dd_pct": max_dd_pct,
    }


def _clear_peer_filters(engine: Any) -> None:
    engine.peer_sector_event_count_365d_min = None
    engine.peer_sector_event_count_365d_max = None
    engine.peer_relative_surprise_pct_365d_min = None
    engine.peer_relative_surprise_pct_365d_max = None
    engine.peer_relative_sue_hist_mean_4q_365d_min = None
    engine.peer_relative_sue_hist_mean_4q_365d_max = None


def _is_idle_earnings_engine(engine: Any) -> bool:
    event_types = {str(event_type) for event_type in (engine.event_types or [])}
    return bool(getattr(engine, "post_allocation_idle_only", False)) and TARGET_EVENT_TYPE in event_types


def _target_engine_ids(config: Any, scope: str) -> set[str]:
    idle_earnings = [engine.engine_id for engine in config.strategy_engines if _is_idle_earnings_engine(engine)]
    if scope == "bullish_only":
        return {engine_id for engine_id in idle_earnings if engine_id == TARGET_SCOPE_BULLISH_ENGINE_ID}
    return set(idle_earnings)


def _apply_variant(config: Any, variant: VariantSpec) -> None:
    target_engine_ids = _target_engine_ids(config, variant.scope)
    for engine in config.strategy_engines:
        _clear_peer_filters(engine)
        if engine.engine_id not in target_engine_ids:
            continue
        if variant.peer_sector_event_count_365d_min is not None:
            engine.peer_sector_event_count_365d_min = variant.peer_sector_event_count_365d_min
        if variant.peer_relative_surprise_pct_365d_min is not None:
            engine.peer_relative_surprise_pct_365d_min = variant.peer_relative_surprise_pct_365d_min
        if variant.peer_relative_sue_hist_mean_4q_365d_min is not None:
            engine.peer_relative_sue_hist_mean_4q_365d_min = variant.peer_relative_sue_hist_mean_4q_365d_min


def _build_variants() -> list[VariantSpec]:
    variants = [VariantSpec(name="baseline", scope="all_idle_earnings")]

    # Scope 1: apply to both idle earnings engines.
    variants.extend(
        [
            VariantSpec(name="all_count3", scope="all_idle_earnings", peer_sector_event_count_365d_min=3.0),
            VariantSpec(name="all_count4", scope="all_idle_earnings", peer_sector_event_count_365d_min=4.0),
            VariantSpec(name="all_surprise8", scope="all_idle_earnings", peer_relative_surprise_pct_365d_min=8.0),
            VariantSpec(name="all_surprise10", scope="all_idle_earnings", peer_relative_surprise_pct_365d_min=10.0),
            VariantSpec(name="all_surprise12", scope="all_idle_earnings", peer_relative_surprise_pct_365d_min=12.0),
            VariantSpec(
                name="all_count3_surprise8_sue0",
                scope="all_idle_earnings",
                peer_sector_event_count_365d_min=3.0,
                peer_relative_surprise_pct_365d_min=8.0,
                peer_relative_sue_hist_mean_4q_365d_min=0.0,
            ),
            VariantSpec(
                name="all_count4_surprise10_sue0",
                scope="all_idle_earnings",
                peer_sector_event_count_365d_min=4.0,
                peer_relative_surprise_pct_365d_min=10.0,
                peer_relative_sue_hist_mean_4q_365d_min=0.0,
            ),
        ]
    )

    # Scope 2: only the bullish idle earnings engine.
    variants.extend(
        [
            VariantSpec(name="bullish_count3", scope="bullish_only", peer_sector_event_count_365d_min=3.0),
            VariantSpec(name="bullish_count4", scope="bullish_only", peer_sector_event_count_365d_min=4.0),
            VariantSpec(name="bullish_surprise8", scope="bullish_only", peer_relative_surprise_pct_365d_min=8.0),
            VariantSpec(name="bullish_surprise10", scope="bullish_only", peer_relative_surprise_pct_365d_min=10.0),
            VariantSpec(name="bullish_surprise12", scope="bullish_only", peer_relative_surprise_pct_365d_min=12.0),
            VariantSpec(
                name="bullish_count3_surprise8_sue0",
                scope="bullish_only",
                peer_sector_event_count_365d_min=3.0,
                peer_relative_surprise_pct_365d_min=8.0,
                peer_relative_sue_hist_mean_4q_365d_min=0.0,
            ),
            VariantSpec(
                name="bullish_count4_surprise10_sue0",
                scope="bullish_only",
                peer_sector_event_count_365d_min=4.0,
                peer_relative_surprise_pct_365d_min=10.0,
                peer_relative_sue_hist_mean_4q_365d_min=0.0,
            ),
        ]
    )
    return variants


def _trade_sleeve_summary(trades: list[Any], candidate_map: dict[str, Any]) -> dict[str, Any]:
    by_sleeve: Counter[str] = Counter()
    by_engine: Counter[str] = Counter()
    sleeve_pnl: defaultdict[str, float] = defaultdict(float)
    idle_trades = 0
    idle_pnl = 0.0
    idle_wins = 0

    for trade in trades:
        sleeve = _resolve_trade_sleeve(trade, candidate_map)
        by_sleeve[sleeve] += 1
        by_engine[str(trade.engine_id)] += 1
        sleeve_pnl[sleeve] += float(trade.net_pnl)
        if sleeve == "idle_alpha":
            idle_trades += 1
            idle_pnl += float(trade.net_pnl)
            if float(trade.net_pnl) > 0:
                idle_wins += 1

    return {
        "by_sleeve": dict(sorted(by_sleeve.items(), key=lambda kv: (-kv[1], kv[0]))),
        "by_engine": dict(sorted(by_engine.items(), key=lambda kv: (-kv[1], kv[0]))),
        "sleeve_pnl": {k: round(v, 2) for k, v in sleeve_pnl.items()},
        "idle_alpha_trade_count": idle_trades,
        "idle_alpha_net_pnl": round(idle_pnl, 2),
        "idle_alpha_win_rate": round(idle_wins / idle_trades, 4) if idle_trades else None,
    }


def main() -> None:
    parser = argparse.ArgumentParser(description="Probe peer-relative surprise additivity for idle-alpha")
    parser.add_argument(
        "--config",
        default=DEFAULT_CONFIG,
        help=f"Experiment manifest JSON path (default: {DEFAULT_CONFIG})",
    )
    parser.add_argument("--start", required=True, help="Start date (YYYY, YYYY-MM, YYYY-MM-DD)")
    parser.add_argument("--end", required=True, help="End date (YYYY, YYYY-MM, YYYY-MM-DD)")
    parser.add_argument(
        "--period-start",
        action="append",
        default=[],
        help="Sub-period start. Format: label=YYYY-MM-DD (repeatable).",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Emit machine-readable JSON only.",
    )
    parser.add_argument(
        "--capital",
        type=float,
        default=10_000.0,
        help="Initial equity for the probe (default: 10000).",
    )
    args = parser.parse_args()

    configure_logging("WARNING")

    start_date = _parse_date(args.start)
    end_date = _parse_date(args.end, is_end=True)
    period_starts = [_parse_period_start(value) for value in args.period_start]
    variants = _build_variants()

    manifest = load_manifest(args.config)
    base_config = resolve_config(manifest)
    store = _build_merged_snapshot_store(
        manifest,
        base_config,
        snapshot_dir_override=None,
    ).slice_by_date_range(start_date, end_date)

    rows: list[dict[str, Any]] = []
    for variant in variants:
        config = resolve_config(manifest)
        _apply_variant(config, variant)

        runner = BacktestRunner(
            manifest=manifest,
            config=config,
            store=store,
            initial_equity=args.capital,
            split_name="peer_relative_surprise_probe",
        )
        result = runner.run(output_root=None)
        sleeve_summary = _trade_sleeve_summary(runner._closed_trades, runner._candidate_map)

        row: dict[str, Any] = {
            "variant": variant.name,
            "scope": variant.scope,
            "peer_sector_event_count_365d_min": variant.peer_sector_event_count_365d_min,
            "peer_relative_surprise_pct_365d_min": variant.peer_relative_surprise_pct_365d_min,
            "peer_relative_sue_hist_mean_4q_365d_min": variant.peer_relative_sue_hist_mean_4q_365d_min,
            "full_return_pct": round(result.metrics.total_return_pct or 0.0, 2),
            "full_dd_pct": round(result.metrics.max_drawdown_pct or 0.0, 2),
            "full_sharpe": round(result.metrics.sharpe_ratio or 0.0, 3),
            "trade_count": result.metrics.trade_count,
            **sleeve_summary,
            "last_date": runner._equity_curve[-1].date.isoformat() if runner._equity_curve else None,
        }
        for label, period_start in period_starts:
            stats = _period_stats(runner._equity_curve, period_start)
            row[label] = {
                "return_pct": round(stats["return_pct"], 2) if stats else None,
                "max_dd_pct": round(stats["max_dd_pct"], 2) if stats else None,
                "start_date": stats["start_date"] if stats else None,
                "end_date": stats["end_date"] if stats else None,
            }
        rows.append(row)

    baseline = next((row for row in rows if row["variant"] == "baseline"), None)
    if baseline is not None:
        for row in rows:
            row["delta_return_vs_baseline"] = round(
                row["full_return_pct"] - baseline["full_return_pct"],
                2,
            )
            row["delta_dd_vs_baseline"] = round(
                row["full_dd_pct"] - baseline["full_dd_pct"],
                2,
            )
            row["delta_idle_alpha_trade_count_vs_baseline"] = (
                row["idle_alpha_trade_count"] - baseline["idle_alpha_trade_count"]
            )
            row["delta_idle_alpha_net_pnl_vs_baseline"] = round(
                row["idle_alpha_net_pnl"] - baseline["idle_alpha_net_pnl"],
                2,
            )

    if args.json:
        print(json.dumps(rows, ensure_ascii=False, indent=2))
        return

    period_labels = [label for label, _ in period_starts]
    header = [
        "variant",
        "scope",
        "full_return",
        "full_dd",
        "full_sharpe",
        "trades",
        "idle_trades",
        "idle_pnl",
        "delta_return",
        "delta_dd",
        "delta_idle_trades",
        "delta_idle_pnl",
        "last_date",
    ]
    for label in period_labels:
        header.extend([f"{label}_return", f"{label}_dd"])
    print("\t".join(header))
    for row in rows:
        fields = [
            row["variant"],
            row["scope"],
            f"{row['full_return_pct']:.2f}",
            f"{row['full_dd_pct']:.2f}",
            f"{row['full_sharpe']:.3f}",
            str(row["trade_count"]),
            str(row["idle_alpha_trade_count"]),
            f"{row['idle_alpha_net_pnl']:.2f}",
            f"{row['delta_return_vs_baseline']:.2f}" if baseline else "-",
            f"{row['delta_dd_vs_baseline']:.2f}" if baseline else "-",
            str(row["delta_idle_alpha_trade_count_vs_baseline"]) if baseline else "-",
            f"{row['delta_idle_alpha_net_pnl_vs_baseline']:.2f}" if baseline else "-",
            str(row["last_date"] or "-"),
        ]
        for label in period_labels:
            stats = row.get(label) or {}
            fields.extend(
                [
                    "-" if stats.get("return_pct") is None else f"{stats['return_pct']:.2f}",
                    "-" if stats.get("max_dd_pct") is None else f"{stats['max_dd_pct']:.2f}",
                ]
            )
        print("\t".join(fields))

    if baseline is not None:
        best = max(rows, key=lambda row: (row["delta_return_vs_baseline"], -row["delta_dd_vs_baseline"]))
        best_additive = [
            row
            for row in rows
            if row["variant"] != "baseline"
            and row["delta_return_vs_baseline"] > 0
            and row["delta_dd_vs_baseline"] <= 0
        ]
        if best_additive:
            best_additive_row = max(
                best_additive,
                key=lambda row: (row["delta_return_vs_baseline"], -row["delta_dd_vs_baseline"]),
            )
            print()
            print(
                "BEST_ADDITIVE",
                best_additive_row["variant"],
                f"return_delta={best_additive_row['delta_return_vs_baseline']:.2f}",
                f"dd_delta={best_additive_row['delta_dd_vs_baseline']:.2f}",
                f"idle_delta={best_additive_row['delta_idle_alpha_net_pnl_vs_baseline']:.2f}",
            )
        else:
            print()
            print(
                "BEST_OVERALL",
                best["variant"],
                f"return_delta={best['delta_return_vs_baseline']:.2f}",
                f"dd_delta={best['delta_dd_vs_baseline']:.2f}",
                f"idle_delta={best['delta_idle_alpha_net_pnl_vs_baseline']:.2f}",
            )


if __name__ == "__main__":
    main()
