#!/usr/bin/env python3
"""
Low-DD overlap VRP Theta backtest + **VXX sleeve** merge (IV overlays off by default).

Writes:
  - VRP closed trades as JSONL + CSV with ``legs_json`` (merge input + human-readable log)
  - VXX bear-call + long-call JSONLs flattened to CSV (full histories from the standard logs)
  - Merged daily equity CSV: VRP-only, VXX-only sleeve, VRP+VXX (no put/straddle/RR scaling)

Example (full history, overlap slice 2, VXX = 2.4%% of account)::

    cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 .venv/bin/python \\
      RenTech/strategy_stack/run_vrp_low_dd_vxx_bundle.py \\
      --overlap-slice-contracts 2 --capital-vxx-pct 0.024 --no-progress

Use ``--portfolio-merge-json`` to take **only** ``capital_vxx`` / bear-call split from an optimizer
output; IV overlay budgets are still forced to **0** for this bundle.
"""
from __future__ import annotations

import argparse
import csv
import json
import math
import sys
from pathlib import Path

_REPO_ROOT = Path(__file__).resolve().parents[2]
_LOGS = _REPO_ROOT / "RenTech" / "data" / "logs"
_THETA_DIR = _REPO_ROOT / "RenTech" / "data" / "theta_chunks"

if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

import pandas as pd

from RenTech.core.theta_chunks_loader import ThetaChunksLoader, theta_chunks_date_bounds
from RenTech.strategy_stack.export_theta_full_trade_log import _write_csv
from RenTech.strategy_stack.portfolio_vrp_plus_vxx import (
    _metrics_block,
    execute_portfolio_merge,
    merge_capital_from_allocation_json,
)
from RenTech.strategy_stack.vrp_backtester import ClosedTrade, VRPBacktester, load_spy_vix_from_yfinance, normalize_spy_df, trading_days_intersecting_spy
from RenTech.strategy_stack.vrp_strategy_config import (
    DEFAULT_STRATEGY_CONFIG_PATH,
    apply_strategy_params_to_vrp_backtester_module,
    load_strategy_config_file,
)


def _write_vrp_jsonl(path: Path, trades: list[ClosedTrade]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)

    def _row(t: ClosedTrade) -> dict:
        return {
            "entry_date": pd.Timestamp(t.entry_date).isoformat(),
            "exit_date": pd.Timestamp(t.exit_date).isoformat(),
            "pnl_usd": float(t.pnl_usd),
            "exit_reason": str(t.exit_reason),
            "regime": str(t.regime),
            "days_in_trade": int(t.days_in_trade),
            "qty": int(t.qty),
            "initial_net_premium": float(t.initial_net_premium),
            "max_margin": float(t.max_margin),
            "legs_json": str(getattr(t, "legs_json", "") or ""),
        }

    with path.open("w", encoding="utf-8") as f:
        for tr in trades:
            f.write(json.dumps(_row(tr)) + "\n")


def _jsonl_to_csv(path_in: Path, path_out: Path) -> int:
    """Flatten JSONL objects to CSV; returns row count (excluding header)."""
    if not path_in.is_file():
        path_out.parent.mkdir(parents=True, exist_ok=True)
        path_out.write_text("", encoding="utf-8")
        return 0
    rows: list[dict[str, object]] = []
    with path_in.open(encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            rows.append(json.loads(line))
    if not rows:
        path_out.parent.mkdir(parents=True, exist_ok=True)
        path_out.write_text("", encoding="utf-8")
        return 0
    keys = sorted({k for r in rows for k in r})
    path_out.parent.mkdir(parents=True, exist_ok=True)
    with path_out.open("w", newline="", encoding="utf-8") as fo:
        w = csv.DictWriter(fo, fieldnames=keys, extrasaction="ignore")
        w.writeheader()
        for r in rows:
            w.writerow({k: r.get(k, "") for k in keys})
    return len(rows)


def main() -> None:
    ap = argparse.ArgumentParser(description="Low-DD overlap VRP + VXX bundle export and merge.")
    ap.add_argument("--theta-dir", type=Path, default=_THETA_DIR)
    ap.add_argument("--capital", type=float, default=100_000.0)
    ap.add_argument("--overlap-slice-contracts", type=int, default=2, metavar="N")
    ap.add_argument("--start", type=str, default="", help="YYYY-MM-DD inclusive floor (optional).")
    ap.add_argument("--end", type=str, default="", help="YYYY-MM-DD inclusive cap (optional).")
    ap.add_argument("--max-days", type=int, default=0, help="If >0, cap sessions (smoke test).")
    ap.add_argument("--no-progress", action="store_true")
    ap.add_argument(
        "--capital-vxx-pct",
        type=float,
        default=None,
        metavar="FRAC",
        help="VXX total risk budget = FRAC × --capital (e.g. 0.024). Ignored if --portfolio-merge-json supplies VXX.",
    )
    ap.add_argument("--capital-vxx", type=float, default=15_000.0, help="VXX budget USD if --capital-vxx-pct unset.")
    ap.add_argument("--vxx-bear-pct", type=float, default=90.0)
    ap.add_argument("--vxx-call-pct", type=float, default=10.0)
    ap.add_argument(
        "--portfolio-merge-json",
        type=Path,
        default=None,
        help="Optimizer JSON: use its VXX budget + split; IV overlays still forced to 0.",
    )
    ap.add_argument("--vxx-bear-trades", type=Path, default=_LOGS / "vxx_portfolio_bear_call.jsonl")
    ap.add_argument("--vxx-call-trades", type=Path, default=_LOGS / "vxx_portfolio_long_call.jsonl")
    ap.add_argument(
        "--out-prefix",
        type=str,
        default="",
        help="Filename prefix under RenTech/data/logs/ (default: vrp_low_dd_ov{N}_vxxbundle).",
    )
    args = ap.parse_args()

    sl = max(1, int(args.overlap_slice_contracts))
    prefix = str(args.out_prefix).strip() or f"vrp_low_dd_ov{sl}_vxxbundle"
    cap = float(args.capital)

    theta_dir = args.theta_dir.expanduser().resolve()
    d0, d1 = theta_chunks_date_bounds(theta_dir)
    yf_start = (d0 - pd.Timedelta(days=400)).strftime("%Y-%m-%d")
    yf_end = (d1 + pd.Timedelta(days=14)).strftime("%Y-%m-%d")
    spy_wide = normalize_spy_df(load_spy_vix_from_yfinance(yf_start, yf_end))
    ld = ThetaChunksLoader(theta_dir, spy_df=spy_wide)
    days = trading_days_intersecting_spy(ld, spy_wide.index, d0, d1)
    if str(args.start).strip():
        t0 = pd.Timestamp(str(args.start).strip())
        days = [d for d in days if d >= t0]
    if str(args.end).strip():
        t1 = pd.Timestamp(str(args.end).strip())
        days = [d for d in days if d <= t1]
    if int(args.max_days) > 0:
        days = days[: int(args.max_days)]
    if not days:
        raise SystemExit("No trading days after filters.")

    cfg = load_strategy_config_file(DEFAULT_STRATEGY_CONFIG_PATH)
    apply_strategy_params_to_vrp_backtester_module(cfg.strategy_params)

    bt = VRPBacktester(
        ld,
        initial_capital=cap,
        spy_df=spy_wide,
        vol_risk_scaling=False,
        r2_crossover_filters=True,
        dd_risk_scaling=True,
        sleeve_risk_fractions=dict(cfg.sleeve_risk_fractions),
        overlap_portfolio=True,
        overlap_slice_contracts=sl,
    )
    bt.run_backtest(trading_days=days, show_progress=not bool(args.no_progress))

    vrp_jsonl = (_LOGS / f"{prefix}_vrp_trades.jsonl").resolve()
    vrp_csv = (_LOGS / f"{prefix}_trade_log_with_legs.csv").resolve()
    eq_csv = (_LOGS / f"{prefix}_vrp_vxx_equity.csv").resolve()
    vxx_bear_csv = (_LOGS / f"{prefix}_vxx_bear_call_trades.csv").resolve()
    vxx_long_csv = (_LOGS / f"{prefix}_vxx_long_call_trades.csv").resolve()
    manifest = (_LOGS / f"{prefix}_manifest.txt").resolve()

    _write_vrp_jsonl(vrp_jsonl, bt.trade_log)
    _write_csv(vrp_csv, bt.trade_log)
    nb = _jsonl_to_csv(args.vxx_bear_trades.expanduser().resolve(), vxx_bear_csv)
    nc = _jsonl_to_csv(args.vxx_call_trades.expanduser().resolve(), vxx_long_csv)

    cap_vxx = float(args.capital_vxx)
    vxx_bear_eff = float(args.vxx_bear_pct)
    vxx_call_eff = float(args.vxx_call_pct)
    if args.portfolio_merge_json is not None:
        mj = args.portfolio_merge_json.expanduser().resolve()
        if not mj.is_file():
            raise SystemExit(f"--portfolio-merge-json not found: {mj}")
        alloc = merge_capital_from_allocation_json(mj, total_capital=cap)
        cap_vxx = float(alloc.get("capital_vxx", cap_vxx))
        vxx_bear_eff = float(alloc.get("vxx_bear_pct", vxx_bear_eff))
        vxx_call_eff = float(alloc.get("vxx_call_pct", vxx_call_eff))
    elif args.capital_vxx_pct is not None:
        cap_vxx = cap * float(args.capital_vxx_pct)

    cmd = (
        f"{Path(sys.argv[0]).name} --overlap-slice-contracts {sl} --capital {cap:g} "
        f"--capital-vxx {cap_vxx:g} --vxx-bear-pct {vxx_bear_eff:g} --vxx-call-pct {vxx_call_eff:g}"
    )
    if args.portfolio_merge_json is not None:
        cmd += f" --portfolio-merge-json {args.portfolio_merge_json}"

    frame = execute_portfolio_merge(
        vrp_trades=vrp_jsonl,
        total_portfolio_capital=cap,
        capital_put=0.0,
        capital_straddle=0.0,
        capital_risk_reversal=0.0,
        capital_vxx=cap_vxx,
        vxx_bear_pct=vxx_bear_eff,
        vxx_call_pct=vxx_call_eff,
        vxx_bear_trades=args.vxx_bear_trades.expanduser().resolve(),
        vxx_call_trades=args.vxx_call_trades.expanduser().resolve(),
        out_csv=eq_csv,
        print_report=True,
        print_vxx_sweep=False,
    )

    m_vrp = _metrics_block(frame["eq_vrp_only"], "VRP only")
    m_vxx = _metrics_block(frame["eq_vxx_sleeve"], "VXX sleeve only")
    m_combo = _metrics_block(frame["eq_full_portfolio"], "VRP + VXX (no IV overlays)")

    manifest.parent.mkdir(parents=True, exist_ok=True)
    lines = [
        f"command: {cmd}",
        f"vrp_jsonl: {vrp_jsonl}",
        f"vrp_trade_csv: {vrp_csv}",
        f"vxx_bear_csv_rows: {nb}  source: {args.vxx_bear_trades}",
        f"vxx_long_csv_rows: {nc}  source: {args.vxx_call_trades}",
        f"equity_csv: {eq_csv}",
        f"window: {pd.Timestamp(days[0]).date()} → {pd.Timestamp(days[-1]).date()}  n_sessions={len(days)}",
        f"capital_vxx_usd: {cap_vxx:,.2f}  bear/call%: {vxx_bear_eff:g}/{vxx_call_eff:g}",
        "",
        "metrics (daily equity series, merge calendar):",
        f"  VRP only: return%={m_vrp['return_pct']:.2f} maxDD%={m_vrp['max_dd_pct']:.2f} CAGR%={m_vrp['cagr_pct']}",
        f"  VXX sleeve: return%={m_vxx['return_pct']:.2f} maxDD%={m_vxx['max_dd_pct']:.2f} CAGR%={m_vxx['cagr_pct']}",
        f"  VRP+VXX: return%={m_combo['return_pct']:.2f} maxDD%={m_combo['max_dd_pct']:.2f} CAGR%={m_combo['cagr_pct']}",
        "",
        f"vrp_closed_trades: {len(bt.trade_log)}",
    ]
    manifest.write_text("\n".join(lines) + "\n", encoding="utf-8")

    print(f"\nWrote manifest: {manifest}")


if __name__ == "__main__":
    main()
