"""Multi-sleeve composite intraday backtester.

Runs N independent strategy sleeves, each with proportional capital allocation,
then combines results into a unified portfolio report. Directly ports the PEAD
composed_gld multi-sleeve approach to intraday ORB strategies.

Usage:
    python -m apps.intraday_bt.composite \\
        --sleeve configs/intraday/strategies/orb_gainers_v16.yaml:0.60 \\
        --sleeve configs/intraday/strategies/orb_compression.yaml:0.25 \\
        --sleeve configs/intraday/strategies/orb_stocks_in_play_v17a.yaml:0.15 \\
        --total-capital 10000 \\
        --days 200
"""
from __future__ import annotations

import argparse
import asyncio
import json
import math
import statistics
import uuid
from collections import defaultdict
from datetime import datetime
from pathlib import Path

from apps.intraday_bt.run import load_config, run as run_sleeve
from libs.intraday.domain import BacktestParams, IntradayConfig, ORBStrategyParams


# ── Merging ────────────────────────────────────────────────────────────────


def _merge_day_results(
    sleeve_results: list[tuple[list, str, float]],
) -> tuple[dict[str, float], dict[str, list], list[str]]:
    """Combine day-level PnL and trades from all sleeves.

    Returns:
        daily_pnl: {date: combined dollar PnL}
        daily_trades: {date: list of all sleeve trades}
        sorted dates
    """
    combined_pnl: dict[str, float] = defaultdict(float)
    combined_trades: dict[str, list] = defaultdict(list)
    all_dates: set[str] = set()

    for day_results, _name, _capital in sleeve_results:
        for dr in day_results:
            all_dates.add(dr.date)
            combined_pnl[dr.date] += dr.daily_pnl
            combined_trades[dr.date].extend(dr.trades)

    return dict(combined_pnl), dict(combined_trades), sorted(all_dates)


# ── Metrics ────────────────────────────────────────────────────────────────


def _compute_composite_metrics(
    daily_pnl: dict[str, float],
    daily_trades: dict[str, list],
    dates: list[str],
    total_capital: float,
    run_id: str = "",
) -> dict:
    """Compute portfolio-level metrics from merged sleeve results."""
    if not dates:
        return {}

    # Equity curve (each sleeve already compounds internally)
    equity = total_capital
    equity_curve: list[float] = [equity]
    daily_returns: list[float] = []

    for date in dates:
        pnl = daily_pnl.get(date, 0.0)
        daily_ret = pnl / equity if equity > 0 else 0.0
        daily_returns.append(daily_ret)
        equity += pnl
        equity_curve.append(equity)

    final_equity = equity
    total_return = (final_equity - total_capital) / total_capital

    # Annualized return (252 trading days per year)
    n_days = len(dates)
    annualized = (1 + total_return) ** (252 / n_days) - 1 if n_days > 0 else 0.0

    # Sharpe (annualized daily Sharpe)
    if len(daily_returns) > 1:
        mean_ret = statistics.mean(daily_returns)
        std_ret = statistics.stdev(daily_returns)
        sharpe = (mean_ret / std_ret * math.sqrt(252)) if std_ret > 0 else 0.0
    else:
        sharpe = 0.0

    # Sortino (downside deviation)
    downside = [r for r in daily_returns if r < 0]
    if downside:
        downside_std = math.sqrt(sum(r ** 2 for r in downside) / len(daily_returns))
        mean_ret_s = statistics.mean(daily_returns)
        sortino = (mean_ret_s / downside_std * math.sqrt(252)) if downside_std > 0 else 0.0
    else:
        sortino = float("inf")

    # Max drawdown
    peak = total_capital
    max_dd = 0.0
    for eq in equity_curve:
        if eq > peak:
            peak = eq
        dd = (eq - peak) / peak
        if dd < max_dd:
            max_dd = dd

    # Calmar
    calmar = (annualized / abs(max_dd)) if max_dd != 0 else float("inf")

    # Trade stats (combined)
    all_trades = [t for date in dates for t in daily_trades.get(date, [])]
    wins = [t for t in all_trades if t.pnl > 0]
    losses = [t for t in all_trades if t.pnl <= 0]
    win_rate = len(wins) / len(all_trades) if all_trades else 0.0
    gross_profit = sum(t.pnl for t in wins)
    gross_loss = abs(sum(t.pnl for t in losses))
    profit_factor = gross_profit / gross_loss if gross_loss > 0 else float("inf")
    days_with_trades = sum(1 for date in dates if daily_trades.get(date))

    return {
        "run_id": run_id or str(uuid.uuid4())[:8],
        "start_date": dates[0],
        "end_date": dates[-1],
        "trading_days": n_days,
        "days_with_trades": days_with_trades,
        "total_return_pct": total_return,
        "annualized_return_pct": annualized,
        "sharpe_ratio": sharpe,
        "sortino_ratio": sortino,
        "calmar_ratio": calmar,
        "max_drawdown_pct": max_dd,
        "total_trades": len(all_trades),
        "win_rate": win_rate,
        "profit_factor": profit_factor,
        "initial_capital": total_capital,
        "final_equity": final_equity,
    }


# ── Sleeve runner ──────────────────────────────────────────────────────────


def _build_sleeve_config(
    base_config: IntradayConfig,
    capital: float,
    days: int | None,
) -> IntradayConfig:
    """Clone config with adjusted initial_capital and optional lookback override."""
    orb = base_config.orb_strategy
    if orb is None:
        raise ValueError("Composite only supports orb strategy_mode sleeves.")

    orb_dict = orb.model_dump()
    orb_dict["initial_capital"] = capital
    new_orb = ORBStrategyParams(**orb_dict)

    bt_dict = base_config.backtest.model_dump()
    if days is not None:
        bt_dict["lookback_trading_days"] = days

    return IntradayConfig(
        strategy_mode=base_config.strategy_mode,
        strategy=base_config.strategy,
        orb_strategy=new_orb,
        universe=base_config.universe,
        backtest=BacktestParams(**bt_dict),
        cache=base_config.cache,
        output=base_config.output,
    )


# ── Reporting ──────────────────────────────────────────────────────────────


def _format_composite_summary(
    metrics: dict,
    sleeve_summaries: list[dict],
    total_capital: float,
) -> str:
    lines = []
    lines.append("\n" + "=" * 60)
    lines.append("  COMPOSITE PORTFOLIO RESULTS")
    lines.append("=" * 60)
    lines.append(f"  Period:       {metrics['start_date']} → {metrics['end_date']}")
    lines.append(f"  Capital:      ${total_capital:,.0f}")
    lines.append(f"  Final equity: ${metrics['final_equity']:,.2f}")
    lines.append("")
    lines.append(f"  Total return: {metrics['total_return_pct']*100:+.2f}%")
    lines.append(f"  Ann. return:  {metrics['annualized_return_pct']*100:+.2f}%")
    lines.append(f"  Sharpe:       {metrics['sharpe_ratio']:.3f}")
    lines.append(f"  Sortino:      {metrics['sortino_ratio']:.3f}")
    lines.append(f"  Max DD:       {metrics['max_drawdown_pct']*100:.2f}%")
    lines.append(f"  Calmar:       {metrics['calmar_ratio']:.2f}")
    lines.append(f"  Trades:       {metrics['total_trades']}")
    lines.append(f"  Win rate:     {metrics['win_rate']*100:.1f}%")
    lines.append(f"  Profit factor: {metrics['profit_factor']:.3f}")
    lines.append("")
    lines.append("  Sleeve breakdown:")
    for s in sleeve_summaries:
        lines.append(
            f"    {s['name']:40s} {s['weight']:4.0%} | "
            f"{s['return_pct']*100:+6.2f}% | Sharpe {s['sharpe']:.2f} | "
            f"DD {s['max_dd']*100:.2f}%"
        )
    lines.append("=" * 60)
    return "\n".join(lines)


# ── Main ───────────────────────────────────────────────────────────────────


async def run_composite(
    sleeve_specs: list[tuple[str, float]],
    total_capital: float = 10_000.0,
    days: int | None = None,
) -> dict:
    """Run all sleeves sequentially and return composite metrics."""
    # Normalize weights
    total_w = sum(w for _, w in sleeve_specs)
    sleeve_specs = [(p, w / total_w) for p, w in sleeve_specs]

    sleeve_results: list[tuple[list, str, float]] = []
    sleeve_summaries: list[dict] = []

    for config_path, weight in sleeve_specs:
        capital = total_capital * weight
        name = Path(config_path).stem
        print(f"\n{'='*60}")
        print(f"  Sleeve: {name}  ({weight:.0%} — ${capital:,.0f})")
        print("=" * 60)

        base_config = load_config(config_path)
        sleeve_config = _build_sleeve_config(base_config, capital, days)

        day_results, metrics, _, _ = await run_sleeve(sleeve_config)
        sleeve_results.append((day_results, name, capital))

        sleeve_summaries.append({
            "name": name,
            "weight": weight,
            "return_pct": metrics.total_return_pct or 0.0,
            "sharpe": metrics.sharpe_ratio or 0.0,
            "max_dd": metrics.max_drawdown_pct or 0.0,
            "trades": metrics.total_trades,
        })
        print(f"  → {name}: {(metrics.total_return_pct or 0)*100:+.2f}% | "
              f"Sharpe {metrics.sharpe_ratio:.2f} | DD {(metrics.max_drawdown_pct or 0)*100:.2f}%")

    # Merge and compute
    daily_pnl, daily_trades, dates = _merge_day_results(sleeve_results)
    run_id = str(uuid.uuid4())[:8]
    composite_metrics = _compute_composite_metrics(
        daily_pnl, daily_trades, dates, total_capital, run_id=run_id
    )

    print(_format_composite_summary(composite_metrics, sleeve_summaries, total_capital))

    # Save results
    out_dir = Path("runs/intraday_orb")
    out_dir.mkdir(parents=True, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    out_file = out_dir / f"composite_{ts}_{run_id}.json"
    out_file.write_text(json.dumps({
        "run_id": run_id,
        "generated_at": datetime.now().isoformat(),
        "total_capital": total_capital,
        "sleeves": sleeve_summaries,
        "metrics": composite_metrics,
    }, indent=2, default=str))
    print(f"\nResults saved to: {out_file}")

    return composite_metrics


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Composite multi-sleeve intraday backtester")
    parser.add_argument(
        "--sleeve",
        dest="sleeves",
        action="append",
        default=[],
        metavar="CONFIG:WEIGHT",
        help="Sleeve config path and weight, e.g. orb_v16.yaml:0.60 (can repeat)",
    )
    parser.add_argument("--total-capital", type=float, default=10_000.0, dest="total_capital")
    parser.add_argument("--days", type=int, default=None)
    return parser.parse_args()


async def main_async() -> None:
    args = parse_args()

    if not args.sleeves:
        print("Error: at least one --sleeve CONFIG:WEIGHT is required.")
        return

    sleeve_specs: list[tuple[str, float]] = []
    for spec in args.sleeves:
        if ":" not in spec:
            print(f"Error: sleeve spec must be 'path:weight', got: {spec!r}")
            return
        path, weight_str = spec.rsplit(":", 1)
        sleeve_specs.append((path, float(weight_str)))

    await run_composite(sleeve_specs, total_capital=args.total_capital, days=args.days)


def main() -> None:
    asyncio.run(main_async())


if __name__ == "__main__":
    main()
