"""Research-only SOPR forced-exit ablation for promoted BTC MLP replays.

Entries, MLP weights, promoted thresholds, and Pine code remain unchanged. A
long position is closed at a decision-bar close when SOPR is at or below a
discovery-calibrated low-tail cutoff.
"""
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

import numpy as np
import pandas as pd

REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO))

from config import OOS_START, SCORE_START, WFO_FOLDS
from strategies.strategy_mlp_scores import calculate_metrics, generate_signals
from tools.check_mlp_parity import load_params
from tools.run_mlp_extreme_bar_event_study import DISCOVERY_END, VALIDATION_START, parse_chart_time
from tools.run_mlp_sopr_entry_ablation import calibrate_sopr_cutoff


def _metrics(signals: pd.DataFrame, start: str, end: str | None = None) -> dict[str, float | int]:
    window = signals if end is None else signals[signals["time"] <= pd.Timestamp(end)].copy()
    result = calculate_metrics(window, score_start=start, min_trades=0)
    return {key: float(value) if isinstance(value, (float, np.floating)) else int(value) for key, value in result.items()}


def _exit_diagnostics(signals: pd.DataFrame, forced_exit: pd.Series, start: str, end: str | None) -> dict[str, float | int | None]:
    mask = signals["time"] >= pd.Timestamp(start)
    if end is not None:
        mask &= signals["time"] <= pd.Timestamp(end)
    # The state at the start of t determines whether a forced close can fire.
    forceable = signals["position"].shift(1, fill_value=0).eq(1) & forced_exit & mask
    next_return = signals["close"].shift(-1).div(signals["close"]).sub(1.0)
    values = next_return[forceable].dropna()
    return {
        "baseline_forceable_exits": int(forceable.sum()),
        "next_bar_observations": int(len(values)),
        "next_bar_mean_return_pct": float(values.mean() * 100) if len(values) else None,
        "next_bar_positive_rate": float((values > 0).mean()) if len(values) else None,
    }


def run(data_path: Path, winner_path: Path, *, feature_tail: float = 0.10) -> dict[str, object]:
    if not 0 < feature_tail < 0.5:
        raise ValueError("feature_tail must be between 0 and 0.5")
    data = pd.read_csv(data_path)
    data.columns = data.columns.str.lower().str.strip()
    if "sopr_norm" not in data.columns:
        raise ValueError("data has no sopr_norm column")
    data["time"] = parse_chart_time(data["time"])
    data = data.sort_values("time").reset_index(drop=True)
    params = load_params(str(winner_path))
    if not params.get("mlp_weights_file"):
        raise ValueError(f"winner has no mlp_weights_file: {winner_path}")

    cutoff = calibrate_sopr_cutoff(data, DISCOVERY_END, feature_tail)
    forced_exit = data["sopr_norm"].le(cutoff).fillna(False)
    baseline = generate_signals(data.copy(), **params)
    gated = generate_signals(data.copy(), forced_exit=forced_exit.to_numpy(dtype=bool), **params)
    all_false = generate_signals(data.copy(), forced_exit=np.zeros(len(data), dtype=bool), **params)
    for column in ("position", "execute_entry", "execute_exit"):
        if not baseline[column].equals(all_false[column]):
            raise AssertionError(f"all-false forced-exit mask changed baseline {column}")

    windows: dict[str, tuple[str, str | None]] = {
        "discovery": (SCORE_START, str(DISCOVERY_END)),
        "validation": (str(VALIDATION_START), str(pd.Timestamp(OOS_START) - pd.Timedelta(nanoseconds=1))),
        "oos": (OOS_START, None),
    }
    summaries = {
        name: {
            "baseline": _metrics(baseline, start, end),
            "sopr_forced_exit": _metrics(gated, start, end),
            "exit_diagnostics": _exit_diagnostics(baseline, forced_exit, start, end),
        }
        for name, (start, end) in windows.items()
    }
    folds: dict[str, dict[str, object]] = {}
    for is_end, start, end in WFO_FOLDS:
        fold_cutoff = calibrate_sopr_cutoff(data, is_end, feature_tail)
        fold_forced_exit = data["sopr_norm"].le(fold_cutoff).fillna(False)
        fold_gated = generate_signals(data.copy(), forced_exit=fold_forced_exit.to_numpy(dtype=bool), **params)
        folds[end] = {
            "cutoff": fold_cutoff,
            "baseline": _metrics(baseline, start, end),
            "sopr_forced_exit": _metrics(fold_gated, start, end),
            "exit_diagnostics": _exit_diagnostics(baseline, fold_forced_exit, start, end),
        }
    return {
        "data_path": str(data_path),
        "winner_path": str(winner_path),
        "definition": {
            "policy": "when already long at the start of a decision bar, force exit if sopr_norm <= discovery cutoff",
            "feature_tail": feature_tail,
            "sopr_cutoff": cutoff,
            "unchanged": ["MLP weights", "all promoted parameters", "entry logic", "Pine code"],
            "costs": "strategy metrics include the repository's 0.5% entry and 0.5% exit commission assumptions",
            "wfo_calibration": "each fold recalibrates its fixed 10th-percentile cutoff using only history before that fold's OOS start",
            "warning": "this is a one-policy ablation, not a threshold sweep or a promotable candidate",
        },
        "windows": summaries,
        "wfo_folds": folds,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--data", type=Path, required=True)
    parser.add_argument("--winner", type=Path, required=True)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()
    result = run(args.data, args.winner)
    text = json.dumps(result, indent=2)
    print(text)
    if args.output:
        args.output.parent.mkdir(parents=True, exist_ok=True)
        args.output.write_text(text + "\n")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
