"""Standalone cash parking backtest — QQQ/SGOV switching as its own strategy.

No event trades. 100% of capital goes to parking.
Uses the same gate logic as the main backtester.

Usage:
    python -m apps.tools.parking_only_backtest --preset vm_24_m20 --capital 10000 --start 2022 --end 2026
    python -m apps.tools.parking_only_backtest --preset vt_24_t13 --symbol qqqm --capital 10000 --start 2022 --end 2026
    python -m apps.tools.parking_only_backtest --compare --capital 10000 --start 2022 --end 2026
"""

import argparse
import asyncio
import datetime as dt
import math
import sys
from dataclasses import dataclass, field


@dataclass
class ParkingResult:
    preset: str
    symbol: str
    start: dt.date
    end: dt.date
    capital: float
    final_value: float
    total_return_pct: float
    max_drawdown_pct: float
    cagr_pct: float
    sharpe: float
    switches: int  # number of QQQ<->SGOV transitions
    days_in_equity: int
    days_in_sgov: int
    annual_returns: dict = field(default_factory=dict)


def run_parking_backtest(
    macro_data: dict[dt.date, dict],
    preset_name: str,
    capital: float,
    start: dt.date,
    end: dt.date,
    sgov_rate: float = 0.05,
) -> ParkingResult:
    """Simulate parking-only strategy using precomputed macro data."""
    from libs.backtest.domain import RiskConfig

    # Apply preset to get gate parameters
    risk = RiskConfig()
    risk.cash_parking_preset = preset_name
    risk.apply_parking_preset()

    park_symbol = risk.cash_parking_symbol
    if park_symbol == "sgov":
        # Pure SGOV — just accrue interest
        dates = sorted(d for d in macro_data if start <= d <= end)
        days = len(dates)
        final = capital * (1 + sgov_rate) ** (days / 252)
        years = days / 252
        return ParkingResult(
            preset=preset_name, symbol="sgov", start=start, end=end,
            capital=capital, final_value=final,
            total_return_pct=(final / capital - 1) * 100,
            max_drawdown_pct=0.0,
            cagr_pct=((final / capital) ** (1 / max(years, 0.01)) - 1) * 100,
            sharpe=0.0, switches=0, days_in_equity=0, days_in_sgov=days,
        )

    # State
    cash = capital
    shares = 0
    avg_price = 0.0
    current_sym = "cash"  # "cash", parking symbol, or "sgov"
    in_sgov = False  # hysteresis state
    sgov_value = 0.0
    peak_equity = capital
    max_dd = 0.0
    switches = 0
    days_equity = 0
    days_sgov = 0
    daily_returns: list[float] = []
    prev_equity = capital
    annual_eq: dict[int, list[float]] = {}

    dates = sorted(d for d in macro_data if start <= d <= end)

    for date in dates:
        macro = macro_data.get(date, {})
        if not macro:
            continue

        # --- Evaluate gate (simplified version of _evaluate_parking_target) ---
        target = _evaluate_gate(macro, risk, park_symbol, in_sgov)
        if target == "sgov":
            if not in_sgov:
                in_sgov = True
        elif target and target != "sgov":
            if in_sgov:
                in_sgov = False

        # Resolve actual target
        if in_sgov:
            target = "sgov"
        else:
            target = park_symbol

        # --- Execute transitions ---
        equity_close = macro.get(f"{park_symbol}_close")
        if equity_close is None and park_symbol == "qqqm":
            equity_close = macro.get("qqq_close")  # fallback to QQQ if QQQM unavailable

        if target == "sgov" and current_sym != "sgov":
            # Sell equity → SGOV
            if shares > 0 and equity_close:
                cash += shares * equity_close
                shares = 0
            sgov_value = cash
            cash = 0
            current_sym = "sgov"
            switches += 1
        elif target != "sgov" and current_sym == "sgov":
            # Sell SGOV → buy equity
            cash = sgov_value
            sgov_value = 0
            if equity_close and equity_close > 0:
                shares = int(cash / equity_close)
                cash -= shares * equity_close
                avg_price = equity_close
            current_sym = park_symbol
            switches += 1
        elif target != "sgov" and current_sym == "cash":
            # Initial buy
            if equity_close and equity_close > 0:
                shares = int(cash / equity_close)
                cash -= shares * equity_close
                avg_price = equity_close
            current_sym = park_symbol

        # --- Accrue SGOV interest ---
        if current_sym == "sgov":
            daily_rate = (1 + sgov_rate) ** (1 / 252) - 1
            sgov_value *= (1 + daily_rate)
            days_sgov += 1
        else:
            days_equity += 1

        # --- Compute equity ---
        if current_sym == "sgov":
            total_equity = sgov_value + cash
        elif equity_close:
            total_equity = shares * equity_close + cash
        else:
            total_equity = prev_equity  # no price data, hold

        # Track drawdown
        if total_equity > peak_equity:
            peak_equity = total_equity
        dd = (total_equity - peak_equity) / peak_equity
        if dd < max_dd:
            max_dd = dd

        # Track daily return
        if prev_equity > 0:
            daily_ret = total_equity / prev_equity - 1
            daily_returns.append(daily_ret)

        # Track annual
        year = date.year
        annual_eq.setdefault(year, []).append(total_equity)

        prev_equity = total_equity

    # Compute metrics
    final_value = prev_equity
    total_return_pct = (final_value / capital - 1) * 100
    years = len(dates) / 252
    cagr = ((final_value / capital) ** (1 / max(years, 0.01)) - 1) * 100

    # Sharpe
    if daily_returns:
        mean_r = sum(daily_returns) / len(daily_returns)
        var_r = sum((r - mean_r) ** 2 for r in daily_returns) / len(daily_returns)
        std_r = math.sqrt(var_r) if var_r > 0 else 1e-10
        sharpe = (mean_r / std_r) * math.sqrt(252)
    else:
        sharpe = 0.0

    # Annual returns
    annual_rets = {}
    for year, eqs in sorted(annual_eq.items()):
        if len(eqs) >= 2:
            annual_rets[year] = (eqs[-1] / eqs[0] - 1) * 100

    return ParkingResult(
        preset=preset_name, symbol=park_symbol, start=start, end=end,
        capital=capital, final_value=final_value,
        total_return_pct=total_return_pct, max_drawdown_pct=max_dd * 100,
        cagr_pct=cagr, sharpe=sharpe, switches=switches,
        days_in_equity=days_equity, days_in_sgov=days_sgov,
        annual_returns=annual_rets,
    )


def _evaluate_gate(
    macro: dict, risk, park_symbol: str, in_sgov: bool
) -> str | None:
    """Simplified gate evaluation (mirrors _evaluate_parking_target logic)."""
    gate_mode = risk.cash_parking_gate_mode
    prefix = park_symbol if park_symbol in ("spy", "qqq") else "qqq"

    if gate_mode == "volatility":
        vol_lb = risk.cash_parking_gate_vol_lookback
        vol = macro.get(f"qqq_vol_{vol_lb}")
        threshold = risk.cash_parking_gate_vol_threshold
        if vol is not None and vol >= threshold:
            return "sgov"

        # Entropy
        ent_thr = risk.cash_parking_entropy_threshold
        if ent_thr > 0:
            ent_lb = risk.cash_parking_entropy_lookback
            entropy = macro.get(f"{prefix}_entropy_{ent_lb}")
            if entropy is not None and entropy > ent_thr and not in_sgov:
                return "sgov"
            if in_sgov and entropy is not None and entropy <= ent_thr * 0.8:
                return park_symbol  # recover

        # VRP
        vrp_thr = risk.cash_parking_vrp_threshold
        if vrp_thr > 0:
            vix = macro.get("VIXCLS")
            vol_vrp = macro.get(f"qqq_vol_{vol_lb}")
            if vix is not None and vol_vrp is not None:
                vrp = vix - (vol_vrp * 100)
                if vrp > vrp_thr and not in_sgov:
                    return "sgov"
                if in_sgov and vrp <= vrp_thr * 0.6:
                    return park_symbol

        # Temperature
        temp_thr = risk.cash_parking_temperature_threshold
        if temp_thr > 0:
            vol_short = macro.get("qqq_vol_15")
            vol_long = macro.get("qqq_vol_50")
            if vol_short and vol_long and vol_long > 0:
                temp = vol_short / vol_long
                if temp > temp_thr and not in_sgov:
                    return "sgov"
                if in_sgov and temp <= temp_thr * 0.7:
                    return park_symbol

        # Hurst
        hurst_thr = risk.cash_parking_hurst_threshold
        if hurst_thr > 0:
            hurst = macro.get(f"{prefix}_hurst_60")
            if hurst is not None and hurst < hurst_thr and not in_sgov:
                return "sgov"
            if in_sgov and hurst is not None and hurst >= hurst_thr + 0.05:
                return park_symbol

        # Momentum trend check (asymmetric re-entry)
        if risk.cash_parking_require_trend and risk.cash_parking_trend_mode == "momentum":
            period = risk.cash_parking_trend_sma_period
            reentry_pct = risk.cash_parking_trend_reentry_pct
            mom = macro.get(f"{prefix}_mom_{period}")
            if in_sgov:
                mom_ok = mom is not None and mom > reentry_pct
                vix_max = risk.cash_parking_vix_reentry_max
                vix_ok = True
                if vix_max > 0:
                    vix = macro.get("VIXCLS")
                    vix_ok = vix is not None and vix < vix_max
                if mom_ok and vix_ok:
                    return park_symbol
                return "sgov"
            else:
                if mom is not None and mom <= 0:
                    return "sgov"
                # Entropy check within momentum
                ent_thr = risk.cash_parking_entropy_threshold
                if ent_thr > 0:
                    ent_lb = risk.cash_parking_entropy_lookback
                    entropy = macro.get(f"{prefix}_entropy_{ent_lb}")
                    if entropy is not None and entropy > ent_thr:
                        return "sgov"

        return park_symbol

    # Composite mode
    if gate_mode == "composite":
        score = _compute_risk_score(macro)
        exit_thr = risk.cash_parking_composite_exit_score
        enter_thr = risk.cash_parking_composite_enter_score
        if in_sgov:
            if score <= enter_thr:
                return park_symbol
            return "sgov"
        else:
            if score >= exit_thr:
                return "sgov"
            return park_symbol

    return park_symbol


def _compute_risk_score(macro: dict) -> int:
    """Compute composite risk score (same as run.py)."""
    score = 0
    vix = macro.get("VIXCLS")
    if vix is not None:
        if vix > 30: score += 45
        elif vix > 25: score += 35
        elif vix > 20: score += 15
        elif vix > 17: score += 5

    vix_chg = macro.get("vix_change_5d")
    if vix_chg is not None:
        if vix_chg > 8: score += 25
        elif vix_chg > 5: score += 15
        elif vix_chg > 3: score += 8

    hy = macro.get("BAMLH0A0HYM2")
    if hy is not None:
        if hy > 6.0: score += 30
        elif hy > 5.0: score += 20
        elif hy > 4.0: score += 8

    vol = macro.get("qqq_vol_20")
    if vol is not None:
        if vol > 0.30: score += 20
        elif vol > 0.24: score += 10
        elif vol > 0.20: score += 3

    mom = macro.get("qqq_mom_20")
    if mom is not None:
        if mom < -0.05: score += 15
        elif mom < -0.02: score += 10
        elif mom < 0: score += 5

    spy_mom = macro.get("spy_mom_20")
    if spy_mom is not None and mom is not None:
        if spy_mom < 0 and mom < 0: score += 8

    if vix is not None and vol is not None:
        vrp = vix - (vol * 100)
        if vrp > 12: score += 20
        elif vrp > 8: score += 10

    vol_s = macro.get("qqq_vol_15")
    vol_l = macro.get("qqq_vol_50")
    if vol_s and vol_l and vol_l > 0:
        temp = vol_s / vol_l
        if temp > 1.5: score += 25
        elif temp > 1.3: score += 15

    hurst = macro.get("qqq_hurst_60")
    if hurst is not None:
        if hurst < 0.40: score += 15
        elif hurst < 0.45: score += 8

    kurt = macro.get("qqq_kurtosis_20")
    if kurt is not None:
        if kurt > 4.0: score += 15
        elif kurt > 3.0: score += 8

    ac = macro.get("qqq_autocorr_20")
    if ac is not None:
        if ac < -0.2: score += 12
        elif ac < -0.1: score += 6

    corr = macro.get("spy_qqq_corr_20")
    if corr is not None:
        if corr < 0.75: score += 15
        elif corr < 0.80: score += 8

    return min(score, 100)


def load_macro_data(config_path: str, start: dt.date, end: dt.date) -> dict:
    """Load macro data directly from Oracle API + FRED DB."""
    return asyncio.run(_load_macro_async(start, end))


async def _load_macro_async(start: dt.date, end: dt.date) -> dict:
    """Fetch SPY/QQQ/QQQM bars from Oracle API and compute indicators."""
    import os
    from libs.backtest.snapshot_store import SnapshotStore

    oracle_url = os.environ.get("ORACLE_URL") or os.environ.get("STOCK_ORACLE_URL", "http://localhost:8000")
    macro = await SnapshotStore._fetch_spy_macro(
        date_range=(start, end),
        oracle_url=oracle_url,
    )
    # Also load FRED macro data (VIX, HY spread, etc.)
    db_dsn = os.environ.get("DB_DSN") or os.environ.get("POSTGRES_DSN", "")
    if db_dsn:
        fred_macro = await SnapshotStore._fetch_macro(
            date_range=(start, end),
            db_dsn=db_dsn,
        )
        for d, vals in fred_macro.items():
            if d in macro:
                macro[d].update(vals)
            else:
                macro[d] = vals
    return macro


def print_result(r: ParkingResult, buy_hold: ParkingResult | None = None) -> None:
    """Pretty print a single result."""
    print(f"  {r.preset:<25s} {r.symbol:<6s}  "
          f"+{r.total_return_pct:>7.1f}%  DD {r.max_drawdown_pct:>6.2f}%  "
          f"CAGR {r.cagr_pct:>5.1f}%  Sharpe {r.sharpe:>5.2f}  "
          f"Switches {r.switches:>3d}  "
          f"Eq/SGOV {r.days_in_equity}/{r.days_in_sgov}d")
    if r.annual_returns:
        years_str = "  Annual: " + ", ".join(
            f"{y}: {ret:+.1f}%" for y, ret in sorted(r.annual_returns.items())
        )
        print(years_str)


def main():
    parser = argparse.ArgumentParser(description="Standalone parking-only backtest")
    parser.add_argument("--preset", type=str, help="Parking preset name")
    parser.add_argument("--config", type=str, default="configs/experiments/return_max_long_v10.99.json",
                        help="Config file (used only to load macro data from its snapshot)")
    parser.add_argument("--symbol", type=str, help="Override parking symbol (qqq, qqqm, spy)")
    parser.add_argument("--capital", type=float, default=10000)
    parser.add_argument("--start", type=int, default=2022, help="Start year")
    parser.add_argument("--end", type=int, default=2026, help="End year")
    parser.add_argument("--compare", action="store_true", help="Compare all key presets")
    args = parser.parse_args()

    start = dt.date(args.start, 1, 1)
    end = dt.date(args.end, 12, 31)
    today = dt.date.today()
    if end > today:
        end = today

    print(f"Loading macro data {start} → {end} (Oracle API direct)...")
    macro = load_macro_data(args.config, start, end)
    print(f"  {len(macro)} trading days loaded\n")

    if args.compare:
        presets = [
            "qqq_no_gate", "sgov",
            "vol_20_24", "vt_24_t13",
            "vm_24_m20", "vmh_24_m20_h50",
            "ve_10_10", "vme_24_e14", "vmeh_24_e14_h50",
            "composite_v2",
        ]

        print(f"Parking-Only Strategy Comparison (${ args.capital:,.0f}, {args.start}-{args.end})")
        print("=" * 110)

        # Buy-and-hold QQQ baseline
        bh = run_parking_backtest(macro, "qqq_no_gate", args.capital, start, end)
        print(f"\n  {'Preset':<25s} {'Sym':<6s}  {'Return':>9s}  {'MaxDD':>8s}  "
              f"{'CAGR':>7s}  {'Sharpe':>7s}  {'Sw':>5s}  {'Eq/SGOV':>12s}")
        print("  " + "-" * 100)

        results = []
        for p in presets:
            r = run_parking_backtest(macro, p, args.capital, start, end)
            results.append(r)
            print_result(r)

        # Summary
        print("\n" + "=" * 110)
        best_ret = max(results, key=lambda r: r.total_return_pct)
        best_dd = min(results, key=lambda r: abs(r.max_drawdown_pct) if r.max_drawdown_pct != 0 else 999)
        best_sharpe = max(results, key=lambda r: r.sharpe)
        print(f"  Best Return:  {best_ret.preset} (+{best_ret.total_return_pct:.1f}%)")
        print(f"  Best DD:      {best_dd.preset} (DD {best_dd.max_drawdown_pct:.2f}%)")
        print(f"  Best Sharpe:  {best_sharpe.preset} (Sharpe {best_sharpe.sharpe:.2f})")

    elif args.preset:
        r = run_parking_backtest(macro, args.preset, args.capital, start, end)
        print(f"Parking-Only: {args.preset}")
        print("=" * 80)
        print_result(r)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()
