from __future__ import annotations

import json
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Any

from config import BotConfig, PROJECT_ROOT

PRESETS_FILE = PROJECT_ROOT / "strategy_presets.json"
CONFIG_FIELDS = set(BotConfig.__dataclass_fields__.keys())


@dataclass(frozen=True)
class StrategyPreset:
    strategy_id: str
    label: str
    source: str
    parameters: dict[str, Any]
    tags: tuple[str, ...] = ()


def _as_preset(row: dict[str, Any]) -> StrategyPreset:
    return StrategyPreset(
        strategy_id=str(row["strategy_id"]),
        label=str(row.get("label") or row["strategy_id"]),
        source=str(row.get("source") or "unknown"),
        parameters=dict(row.get("parameters") or {}),
        tags=tuple(str(tag) for tag in row.get("tags", ())),
    )


def build_candidate_presets() -> list[StrategyPreset]:
    """Hand-picked paper-only challengers derived from V59-V61 lessons.

    These are deliberately conservative: trade size is capped at current paper size,
    hard stops stay bounded, and every preset remains suitable for paper/backtest
    tournaments before any live-risk discussion.
    """
    rows = [
        {
            "strategy_id": "candidate_v62_anti_chop_context",
            "label": "Candidate V62 Anti-Chop Context",
            "source": "candidate:context-filter",
            "tags": ("candidate", "context-aware", "anti-chop"),
            "parameters": {
                "max_total_trades": 3,
                "default_leverage": 5,
                "trade_size_usd": 50.0,
                "min_volume_24h": 15_000_000,
                "cooldown_minutes": 90,
                "flash_crash_trigger_pct": 5.0,
                "atr_sl_multiplier": 1.8,
                "break_even_activation_pct": 0.45,
                "v_shape_activation_pct": 1.4,
                "v_shape_trail_dist_pct": 0.65,
                "dead_fish_time_limit_mins": 20,
                "max_hard_stop_pct": 3.0,
            },
        },
        {
            "strategy_id": "candidate_v62_scalp_fast_rebound",
            "label": "Candidate V62 Scalp Fast Rebound",
            "source": "candidate:fast-rebound",
            "tags": ("candidate", "scalp", "fast-rebound"),
            "parameters": {
                "max_total_trades": 2,
                "default_leverage": 5,
                "trade_size_usd": 40.0,
                "min_volume_24h": 20_000_000,
                "cooldown_minutes": 45,
                "flash_crash_trigger_pct": 3.8,
                "atr_sl_multiplier": 1.2,
                "break_even_activation_pct": 0.25,
                "v_shape_activation_pct": 0.9,
                "v_shape_trail_dist_pct": 0.45,
                "dead_fish_time_limit_mins": 8,
                "max_hard_stop_pct": 2.2,
            },
        },
        {
            "strategy_id": "candidate_v62_deep_capitulation",
            "label": "Candidate V62 Deep Capitulation",
            "source": "candidate:deep-dip",
            "tags": ("candidate", "strict-trigger", "deep-dip"),
            "parameters": {
                "max_total_trades": 2,
                "default_leverage": 5,
                "trade_size_usd": 50.0,
                "min_volume_24h": 10_000_000,
                "cooldown_minutes": 120,
                "flash_crash_trigger_pct": 6.5,
                "atr_sl_multiplier": 2.0,
                "break_even_activation_pct": 0.6,
                "v_shape_activation_pct": 1.8,
                "v_shape_trail_dist_pct": 0.9,
                "dead_fish_time_limit_mins": 30,
                "max_hard_stop_pct": 4.0,
            },
        },
        {
            "strategy_id": "candidate_v62_loose_trend_rebound",
            "label": "Candidate V62 Loose Trend Rebound",
            "source": "candidate:trend-rebound",
            "tags": ("candidate", "trend-up", "looser-stop"),
            "parameters": {
                "max_total_trades": 3,
                "default_leverage": 5,
                "trade_size_usd": 50.0,
                "min_volume_24h": 25_000_000,
                "cooldown_minutes": 60,
                "flash_crash_trigger_pct": 4.2,
                "atr_sl_multiplier": 2.2,
                "break_even_activation_pct": 0.6,
                "v_shape_activation_pct": 1.6,
                "v_shape_trail_dist_pct": 0.8,
                "dead_fish_time_limit_mins": 35,
                "max_hard_stop_pct": 4.5,
            },
        },
        {
            "strategy_id": "candidate_v62_micro_size_explorer",
            "label": "Candidate V62 Micro Size Explorer",
            "source": "candidate:sample-builder",
            "tags": ("candidate", "sample-builder", "micro-size"),
            "parameters": {
                "max_total_trades": 5,
                "default_leverage": 3,
                "trade_size_usd": 20.0,
                "min_volume_24h": 8_000_000,
                "cooldown_minutes": 30,
                "flash_crash_trigger_pct": 3.5,
                "atr_sl_multiplier": 1.4,
                "break_even_activation_pct": 0.3,
                "v_shape_activation_pct": 1.0,
                "v_shape_trail_dist_pct": 0.5,
                "dead_fish_time_limit_mins": 12,
                "max_hard_stop_pct": 2.5,
            },
        },
        {
            "strategy_id": "candidate_v62_quality_only",
            "label": "Candidate V62 Quality Only",
            "source": "candidate:high-liquidity",
            "tags": ("candidate", "quality", "high-liquidity"),
            "parameters": {
                "max_total_trades": 2,
                "default_leverage": 4,
                "trade_size_usd": 50.0,
                "min_volume_24h": 50_000_000,
                "cooldown_minutes": 120,
                "flash_crash_trigger_pct": 5.5,
                "atr_sl_multiplier": 1.6,
                "break_even_activation_pct": 0.4,
                "v_shape_activation_pct": 1.25,
                "v_shape_trail_dist_pct": 0.6,
                "dead_fish_time_limit_mins": 18,
                "max_hard_stop_pct": 3.0,
            },
        },
        {
            "strategy_id": "candidate_v63_loose_fast_decay",
            "label": "Candidate V63 Loose Fast Decay",
            "source": "candidate:parameter-sweep:2026-05-29",
            "tags": ("candidate", "sweep-winner", "loose-rebound", "fast-decay"),
            "parameters": {
                "max_total_trades": 3,
                "default_leverage": 5,
                "trade_size_usd": 50.0,
                "min_volume_24h": 25_000_000,
                "cooldown_minutes": 60,
                "flash_crash_trigger_pct": 4.0,
                "atr_sl_multiplier": 2.2,
                "break_even_activation_pct": 0.6,
                "v_shape_activation_pct": 1.6,
                "v_shape_trail_dist_pct": 0.5,
                "dead_fish_time_limit_mins": 25,
                "max_hard_stop_pct": 4.5,
            },
        },
        {
            "strategy_id": "candidate_v64_squeeze_survival_strict",
            "label": "Candidate V64 Squeeze Survival Strict",
            "source": "candidate:pre-handover-lessons:squeeze-breakout",
            "tags": ("candidate", "paper-only", "pre-handover-lessons", "squeeze-breakout", "tight-survival"),
            "parameters": {
                "max_total_trades": 2,
                "default_leverage": 5,
                "trade_size_usd": 40.0,
                "min_volume_24h": 20_000_000,
                "cooldown_minutes": 90,
                "flash_crash_trigger_pct": 4.8,
                "atr_sl_multiplier": 1.4,
                "break_even_activation_pct": 0.4,
                "v_shape_activation_pct": 1.2,
                "v_shape_trail_dist_pct": 0.45,
                "dead_fish_time_limit_mins": 12,
                "max_hard_stop_pct": 2.6,
            },
        },
        {
            "strategy_id": "candidate_v64_squeeze_survival_wide",
            "label": "Candidate V64 Squeeze Survival Wide",
            "source": "candidate:pre-handover-lessons:squeeze-breakout",
            "tags": ("candidate", "paper-only", "pre-handover-lessons", "squeeze-breakout", "survival"),
            "parameters": {
                "max_total_trades": 3,
                "default_leverage": 5,
                "trade_size_usd": 50.0,
                "min_volume_24h": 15_000_000,
                "cooldown_minutes": 75,
                "flash_crash_trigger_pct": 4.2,
                "atr_sl_multiplier": 1.6,
                "break_even_activation_pct": 0.55,
                "v_shape_activation_pct": 1.4,
                "v_shape_trail_dist_pct": 0.65,
                "dead_fish_time_limit_mins": 15,
                "max_hard_stop_pct": 3.0,
            },
        },
        {
            "strategy_id": "candidate_v65_signal_sampler_conservative",
            "label": "Candidate V65 Signal Sampler Conservative",
            "source": "candidate:research-sampler:near-miss",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "data-collection"),
            "parameters": {
                "max_total_trades": 2,
                "default_leverage": 3,
                "trade_size_usd": 20.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.5,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 10.0,
                "max_position_notional_pct": 45.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 8_000_000,
                "cooldown_minutes": 45,
                "flash_crash_trigger_pct": 3.0,
                "crash_roe_trigger_pct": 9.0,
                "atr_sl_multiplier": 1.0,
                "break_even_activation_pct": 0.35,
                "v_shape_activation_pct": 1.0,
                "v_shape_trail_dist_pct": 0.4,
                "dead_fish_time_limit_mins": 10,
                "max_hard_stop_pct": 2.0,
                "max_scan_coins": 30,
            },
        },
        {
            "strategy_id": "candidate_v65_signal_sampler_aggressive",
            "label": "Candidate V65 Signal Sampler Aggressive",
            "source": "candidate:research-sampler:near-miss",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "data-collection", "aggressive-sampler"),
            "parameters": {
                "max_total_trades": 2,
                "default_leverage": 3,
                "trade_size_usd": 10.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.5,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 10.0,
                "max_position_notional_pct": 45.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 6_000_000,
                "cooldown_minutes": 30,
                "flash_crash_trigger_pct": 2.5,
                "crash_roe_trigger_pct": 7.5,
                "atr_sl_multiplier": 0.9,
                "break_even_activation_pct": 0.25,
                "v_shape_activation_pct": 0.8,
                "v_shape_trail_dist_pct": 0.35,
                "dead_fish_time_limit_mins": 8,
                "max_hard_stop_pct": 2.0,
                "max_scan_coins": 30,
            },
        },
        {
            "strategy_id": "candidate_v66_squeeze_breakout_sampler",
            "label": "Candidate V66 Squeeze Breakout Sampler",
            "source": "candidate:strategy-lab:grid_squeeze_w2p5_b1p2",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "squeeze-breakout", "non-drop"),
            "parameters": {
                "strategy_family": "volatility_squeeze_breakout",
                "max_total_trades": 2,
                "default_leverage": 3,
                "trade_size_usd": 15.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.75,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 20.0,
                "max_position_notional_pct": 60.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 10_000_000,
                "cooldown_minutes": 45,
                "squeeze_lookback_ticks": 24,
                "squeeze_max_band_width_pct": 2.5,
                "squeeze_breakout_pct": 1.2,
                "atr_sl_multiplier": 1.0,
                "break_even_activation_pct": 0.35,
                "v_shape_activation_pct": 1.0,
                "v_shape_trail_dist_pct": 0.45,
                "dead_fish_time_limit_mins": 10,
                "max_hard_stop_pct": 2.0,
                "max_scan_coins": 30,
            },
        },
        {
            "strategy_id": "candidate_v67_trend_pullback_sma_vwap",
            "label": "Candidate V67 Trend Pullback SMA/VWAP",
            "source": "candidate:indicator-expansion:trend-pullback",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "indicator", "trend-pullback", "sma", "vwap"),
            "parameters": {
                "strategy_family": "trend_pullback_sma_vwap",
                "max_total_trades": 2,
                "default_leverage": 3,
                "trade_size_usd": 15.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.5,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 12.0,
                "max_position_notional_pct": 45.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 15_000_000,
                "cooldown_minutes": 60,
                "trend_pullback_min_rsi": 35.0,
                "trend_pullback_max_rsi": 70.0,
                "trend_pullback_max_distance_pct": 1.5,
                "atr_sl_multiplier": 1.2,
                "break_even_activation_pct": 0.45,
                "v_shape_activation_pct": 1.2,
                "v_shape_trail_dist_pct": 0.5,
                "dead_fish_time_limit_mins": 45,
                "max_hard_stop_pct": 2.0,
                "max_scan_coins": 20,
            },
        },
        {
            "strategy_id": "candidate_v68_bollinger_rsi_mean_reversion",
            "label": "Candidate V68 Bollinger RSI Mean Reversion",
            "source": "candidate:indicator-expansion:mean-reversion",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "indicator", "bollinger", "rsi", "mean-reversion"),
            "parameters": {
                "strategy_family": "bollinger_rsi_mean_reversion",
                "max_total_trades": 2,
                "default_leverage": 3,
                "trade_size_usd": 12.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.5,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 10.0,
                "max_position_notional_pct": 40.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 15_000_000,
                "cooldown_minutes": 45,
                "mean_reversion_max_rsi": 35.0,
                "mean_reversion_band_tolerance_pct": 0.3,
                "atr_sl_multiplier": 1.1,
                "break_even_activation_pct": 0.35,
                "v_shape_activation_pct": 0.9,
                "v_shape_trail_dist_pct": 0.4,
                "dead_fish_time_limit_mins": 30,
                "max_hard_stop_pct": 2.0,
                "max_scan_coins": 20,
            },
        },
        {
            "strategy_id": "candidate_v69_squeeze_breakout_confirmed",
            "label": "Candidate V69 Confirmed Squeeze Breakout",
            "source": "candidate:indicator-expansion:confirmed-squeeze",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "indicator", "squeeze-breakout", "confirmed", "non-drop"),
            "parameters": {
                "strategy_family": "confirmed_squeeze_breakout",
                "max_total_trades": 2,
                "default_leverage": 3,
                "trade_size_usd": 15.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.5,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 12.0,
                "max_position_notional_pct": 45.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 15_000_000,
                "cooldown_minutes": 60,
                "squeeze_lookback_ticks": 24,
                "squeeze_max_band_width_pct": 2.2,
                "squeeze_breakout_pct": 1.0,
                "atr_sl_multiplier": 1.0,
                "break_even_activation_pct": 0.35,
                "v_shape_activation_pct": 1.0,
                "v_shape_trail_dist_pct": 0.45,
                "dead_fish_time_limit_mins": 20,
                "max_hard_stop_pct": 2.0,
                "max_scan_coins": 20,
            },
        },
        {
            "strategy_id": "candidate_v73_multi_day_trend_investment",
            "label": "Candidate V73 Multi-Day Trend Investment",
            "source": "candidate:indicator-expansion:multi-day-trend",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "indicator", "multi-day", "trend-investment"),
            "parameters": {
                "strategy_family": "multi_day_trend_investment",
                "max_total_trades": 1,
                "default_leverage": 1,
                "trade_size_usd": 20.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.5,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 12.0,
                "max_position_notional_pct": 35.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 25_000_000,
                "cooldown_minutes": 240,
                "multi_day_min_trend_pct": 2.0,
                "multi_day_max_pullback_distance_pct": 3.0,
                "atr_sl_multiplier": 2.0,
                "break_even_activation_pct": 1.2,
                "v_shape_activation_pct": 3.0,
                "v_shape_trail_dist_pct": 1.0,
                "dead_fish_time_limit_mins": 1440,
                "max_hard_stop_pct": 4.0,
                "max_scan_coins": 8,
            },
        },
        {
            "strategy_id": "candidate_v74_relative_strength_breadth_guard",
            "label": "Candidate V74 Relative Strength Breadth Guard",
            "source": "candidate:strategy-lab:grid_rs_l24_t4_m3",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "relative-strength", "breadth-guard", "non-drop"),
            "parameters": {
                "strategy_family": "relative_strength_rotation",
                "max_total_trades": 2,
                "default_leverage": 2,
                "trade_size_usd": 12.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.4,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 8.0,
                "max_position_notional_pct": 30.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 15_000_000,
                "cooldown_minutes": 180,
                "relative_strength_lookback_ticks": 24,
                "relative_strength_top_n": 4,
                "relative_strength_min_momentum_pct": 3.0,
                "relative_strength_min_positive_candidates": 3,
                "atr_sl_multiplier": 1.2,
                "break_even_activation_pct": 0.6,
                "v_shape_activation_pct": 1.8,
                "v_shape_trail_dist_pct": 0.7,
                "dead_fish_time_limit_mins": 180,
                "max_hard_stop_pct": 2.5,
                "max_scan_coins": 24,
            },
        },
        {
            "strategy_id": "candidate_v75_hybrid_survival_squeeze",
            "label": "Candidate V75 Hybrid Survival Squeeze",
            "source": "candidate:paper-learnings:v60-v66-v69",
            "tags": ("candidate", "paper-only", "research-sampler", "not-champion", "hybrid", "survival", "squeeze-breakout", "quality-whitelist"),
            "parameters": {
                "strategy_family": "hybrid_survival_squeeze",
                "max_total_trades": 2,
                "default_leverage": 3,
                "trade_size_usd": 12.0,
                "wallet_equity_usdc": 500.0,
                "risk_per_trade_pct": 0.45,
                "position_sizing_mode": "risk",
                "max_position_margin_pct": 10.0,
                "max_position_notional_pct": 40.0,
                "min_order_notional_usd": 12.0,
                "min_volume_24h": 12_000_000,
                "cooldown_minutes": 75,
                "allowed_coins": ("WLD", "SUI", "ENA", "BTC", "ETH", "LINK", "BCH"),
                "flash_crash_trigger_pct": 4.0,
                "crash_roe_trigger_pct": 8.0,
                "squeeze_lookback_ticks": 24,
                "squeeze_max_band_width_pct": 2.2,
                "squeeze_breakout_pct": 1.0,
                "atr_sl_multiplier": 1.1,
                "break_even_activation_pct": 0.35,
                "v_shape_activation_pct": 1.0,
                "v_shape_trail_dist_pct": 0.45,
                "dead_fish_time_limit_mins": 15,
                "max_hard_stop_pct": 2.0,
                "max_scan_coins": 12,
            },
        },
    ]
    return [_as_preset(row) for row in rows]


def load_strategy_presets(path: str | Path = PRESETS_FILE, *, include_candidates: bool = True) -> dict[str, StrategyPreset]:
    preset_path = Path(path)
    raw = json.loads(preset_path.read_text(encoding="utf-8")) if preset_path.exists() else {"presets": []}
    presets = [_as_preset(row) for row in raw.get("presets", [])]
    if include_candidates:
        presets.extend(build_candidate_presets())
    by_id: dict[str, StrategyPreset] = {}
    for preset in presets:
        if preset.strategy_id in by_id:
            raise ValueError(f"Duplicate strategy_id: {preset.strategy_id}")
        by_id[preset.strategy_id] = preset
    return by_id


def preset_to_config(preset: StrategyPreset, base: BotConfig | None = None, *, runtime_dir: Path | None = None) -> BotConfig:
    cfg = base or BotConfig.from_file()
    overrides = {key: value for key, value in preset.parameters.items() if key in CONFIG_FIELDS}
    if runtime_dir is not None:
        overrides["runtime_dir"] = Path(runtime_dir).expanduser().resolve()
    return replace(cfg, **overrides)


def main() -> int:
    presets = load_strategy_presets()
    print(f"Strategy presets: {len(presets)}")
    for preset in presets.values():
        print(f"- {preset.strategy_id}: {preset.label} [{', '.join(preset.tags)}]")
    return 0


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