"""
Generate Pine Script preset block from optimizer winner CSVs.

Reads all 20 optimization_winner_*.csv files from results/winners/ and
rewrites the auto-generated sentinel block inside
strategies/strategy_activation_scores.pine with the current best params
for each asset/TF combo.

After any optimization run, call this script to keep the Pine file current:
    python3 tools/generate_pine_presets.py

The Pine file is updated in-place. Paste it into TradingView — the strategy
auto-selects the correct preset based on the current chart symbol and timeframe.

Usage:
    python3 tools/generate_pine_presets.py [--dry-run]
"""

import argparse
import csv
import os
import re
import sys
from datetime import datetime

# ── Config ────────────────────────────────────────────────────────────────────

WINNERS_DIR = "results/winners"
PINE_FILE   = "strategies/strategy_activation_scores.pine"

SENTINEL_START = "// ── AUTO-GENERATED PRESET VALUES"
SENTINEL_END   = "// ── END AUTO-GENERATED"

# Columns in winner CSVs that are NOT params (metrics / internal bookkeeping)
_METRIC_COLS = {
    "Total P&L %", "Max Drawdown %", "Sharpe Ratio", "Sortino Ratio",
    "Total Trades", "Calmar Ratio", "P&L/DD Ratio", "% In Market",
    "composite_score", "Composite", "verified", "GPU_Score", "CPU_Score",
    "_pnl_dd_percentile", "subperiod_consistent", "WFO_Score", "WFO_Fold_Calmars",
}

# Params emitted as Pine booleans (stored as 0.0/1.0 float in CSV)
_BOOL_PARAMS = {"i_use_long_entry_confirmation", "i_use_long_exit_confirmation", "i_mvrv_suppress_bear"}

# Params emitted as Pine integers
_INT_PARAMS = {"i_regime_window", "i_m3_momentum_period", "i_div_window"}

# Params locked to a single fixed value across all presets (emitted as a plain
# assignment, not a switch).  Value must be a valid Pine literal string.
_LOCKED_PARAMS = {
    "i_m3_momentum_period": "1",   # period=1 is the only meaningful value; see docs
}

# Supported 20 combos (defines preset ordering in dropdown)
_ASSETS = ["COINBASE_BTCUSD", "COINBASE_ETHUSD", "BINANCE_SOLUSD", "BINANCE_LINKUSD"]
_TFS    = ["4H", "6H", "8H", "12H", "1D"]

# Timeframe period strings as returned by Pine's timeframe.period
_TF_PERIOD = {"4H": "240", "6H": "360", "8H": "480", "12H": "720", "1D": "D"}

# Default fallback preset when auto-detection fails
_FALLBACK_PRESET = "COINBASE_BTCUSD 1D"


# ── Helpers ───────────────────────────────────────────────────────────────────

def preset_key(asset: str, tf: str) -> str:
    return f"{asset} {tf}"


def fmt_float(v: float) -> str:
    """Format float: up to 4 sig figs, strip trailing zeros."""
    s = f"{v:.4g}"
    # Ensure there's always a decimal point for Pine float literals
    if "." not in s and "e" not in s:
        s += ".0"
    return s


def fmt_value(name: str, raw) -> str:
    """Return Pine-literal string for a param value."""
    if raw in (None, "", "nan"):
        v = 0.0
    elif isinstance(raw, str) and raw.lower() in ("true", "false"):
        v = 1.0 if raw.lower() == "true" else 0.0
    else:
        v = float(raw)
    if name in _BOOL_PARAMS:
        return "true" if v > 0.5 else "false"
    if name in _INT_PARAMS:
        return str(int(round(v)))
    return fmt_float(v)


def pine_type(name: str) -> str:
    if name in _BOOL_PARAMS:
        return "bool"
    if name in _INT_PARAMS:
        return "int"
    return "float"


# ── Load winner CSVs ──────────────────────────────────────────────────────────

def load_winners() -> dict:
    """
    Returns {preset_key: {param_name: value_str, ...}} for all found winner files.
    Warns for missing files.
    """
    pattern = re.compile(
        r"optimization_winner_activation_scores_([A-Z0-9_]+)_([A-Z0-9HD]+)\.csv$"
    )
    presets = {}
    for fname in sorted(os.listdir(WINNERS_DIR)):
        m = pattern.match(fname)
        if not m:
            continue
        asset, tf = m.group(1), m.group(2)
        if asset not in _ASSETS or tf not in _TFS:
            continue
        key = preset_key(asset, tf)
        fpath = os.path.join(WINNERS_DIR, fname)
        with open(fpath, newline="") as f:
            rows = list(csv.DictReader(f))
        if not rows:
            print(f"  WARNING: {fname} is empty — skipping", file=sys.stderr)
            continue
        row = rows[0]
        presets[key] = {k: v for k, v in row.items() if k not in _METRIC_COLS}

    # Report missing
    for asset in _ASSETS:
        for tf in _TFS:
            k = preset_key(asset, tf)
            if k not in presets:
                print(f"  WARNING: no winner file for {k} — will use fallback values", file=sys.stderr)

    return presets


# ── Get canonical param list ──────────────────────────────────────────────────

def get_param_names(presets: dict) -> list:
    """
    Return ordered param list for the preset block.

    Uses config.WEIGHT_COLS as the authoritative weight list so that newly added
    signals are always emitted even if the fallback winner CSV pre-dates their addition.
    Non-weight params (thresholds, flags) are appended from the union of all winner CSVs.
    """
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    from config import WEIGHT_COLS

    # Collect non-weight param names from any winner CSV (thresholds, flags, etc.)
    non_weight_seen = []
    non_weight_set = set()
    for row in presets.values():
        for k in row:
            if not k.startswith("i_w_") and k not in non_weight_set:
                non_weight_seen.append(k)
                non_weight_set.add(k)

    # Canonical order: non-weight params first (preserving CSV order), then all WEIGHT_COLS
    return non_weight_seen + [w for w in WEIGHT_COLS if w not in non_weight_set]


# ── Generate Pine block ───────────────────────────────────────────────────────

def generate_block(presets: dict, param_names: list) -> str:
    """Return the full auto-generated block string (between sentinels, inclusive)."""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
    ordered_keys = [preset_key(a, tf) for a in _ASSETS for tf in _TFS]
    present_keys = [k for k in ordered_keys if k in presets]

    lines = []
    lines.append(f"{SENTINEL_START} — do not edit manually ───────────────────────────────")
    lines.append(f"// Run tools/generate_pine_presets.py to regenerate from latest winner CSVs.")
    lines.append(f"// Last generated: {timestamp}  |  {len(present_keys)}/20 presets found")
    lines.append("")

    # Auto-detect block — use timeframe.in_seconds(timeframe.period) which normalises
    # any TradingView-internal period representation ("720", "12H", etc.) to a fixed
    # second count, avoiding ambiguity around timeframe.multiplier behaviour.
    lines.append("// ── Preset auto-detection from chart symbol + timeframe ─────────────────────")
    lines.append("_tf_secs  = timeframe.in_seconds(timeframe.period)")
    lines.append("_tf_label = _tf_secs == 86400 ? \"1D\" :")
    lines.append("             _tf_secs == 14400 ? \"4H\" :")
    lines.append("             _tf_secs == 21600 ? \"6H\" :")
    lines.append("             _tf_secs == 28800 ? \"8H\" :")
    lines.append("             _tf_secs == 43200 ? \"12H\" : \"\"")
    lines.append("")

    # Override input with all 20 options listed
    options_str = ", ".join(f'"{k}"' for k in ordered_keys)
    lines.append('i_preset_override = input.string("", "Force preset (blank = auto)",')
    lines.append(f'    options=["", {options_str}],')
    lines.append(f'    group=group_neural_activation_thresholds, display=display.none)')
    lines.append("")
    lines.append("_auto_key = syminfo.prefix + \"_\" + syminfo.ticker + \" \" + _tf_label")
    lines.append('_preset   = i_preset_override != "" ? i_preset_override : _auto_key')
    lines.append("// ─────────────────────────────────────────────────────────────────────────────")
    lines.append("")

    # Locked params: emit as plain assignments (no switch)
    if _LOCKED_PARAMS:
        lines.append("// ── Locked params (single value, all presets) ────────────────────────────────")
        for param, locked_val in _LOCKED_PARAMS.items():
            ptype = pine_type(param)
            lines.append(f"{ptype} {param} = {locked_val}  // locked — do not preset-switch")
        lines.append("")

    # One switch expression per optimisable param
    fallback_row = presets.get(_FALLBACK_PRESET, {})

    for param in param_names:
        if param in _LOCKED_PARAMS:
            continue
        ptype = pine_type(param)
        fallback_val = fmt_value(param, fallback_row.get(param, 0))

        lines.append(f"{ptype} {param} = switch _preset")
        for key in present_keys:
            row = presets[key]
            val = fmt_value(param, row.get(param, fallback_row.get(param, 0)))
            lines.append(f'    "{key}" => {val}')
        lines.append(f"    => {fallback_val}  // fallback: {_FALLBACK_PRESET}")
        lines.append("")

    lines.append(SENTINEL_END + " ─────────────────────────────────────────────────────────────")

    return "\n".join(lines)


# ── Rewrite Pine file ─────────────────────────────────────────────────────────

def _fmt_pine_date(date_str: str) -> str:
    """Convert 'YYYY-MM-DD' to 'D Month YYYY' for Pine's timestamp() function."""
    from datetime import datetime
    dt = datetime.strptime(date_str, "%Y-%m-%d")
    return dt.strftime("%-d %B %Y")


def _sync_pine_dates(content: str) -> tuple[str, list[str]]:
    """
    Replace hardcoded startDate / endDate timestamp literals in the Pine file
    with values from config.SCORE_START and config.TRAIN_END.

    Returns (updated_content, list_of_change_descriptions).
    """
    import re as _re
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    from config import SCORE_START, TRAIN_END

    want_start = _fmt_pine_date(SCORE_START)
    want_end   = _fmt_pine_date(TRAIN_END)

    changes = []

    def _replace(pattern, want, label):
        nonlocal content
        updated = _re.sub(pattern, r'\g<1>' + want + r'\g<3>', content)
        if updated != content:
            changes.append(f"{label}: → \"{want}\"")
            content = updated

    _replace(r'(startDate\s*=\s*input\.time\(timestamp\(")([^"]+)(")', want_start, "startDate")
    _replace(r'(endDate\s*=\s*input\.time\(timestamp\(")([^"]+)(")',   want_end,   "endDate  ")

    return content, changes


def rewrite_pine(new_block: str, dry_run: bool = False) -> bool:
    """
    Locate sentinel start/end in the Pine file and replace the block between them.
    Also syncs startDate/endDate from config.SCORE_START / config.TRAIN_END.
    Returns True if the file was modified (or would be modified in dry-run).
    """
    with open(PINE_FILE, "r", encoding="utf-8") as f:
        content = f.read()

    # Sync date inputs from config.py first
    content, date_changes = _sync_pine_dates(content)
    if date_changes:
        for msg in date_changes:
            print(f"  Date synced from config.py: {msg}")

    # Find sentinel positions
    start_idx = content.find(SENTINEL_START)
    end_idx   = content.find(SENTINEL_END)

    if start_idx == -1:
        print(f"ERROR: sentinel '{SENTINEL_START}' not found in {PINE_FILE}")
        print("  Run P3 first: add the sentinel comments to the Pine file.")
        return False
    if end_idx == -1:
        print(f"ERROR: sentinel '{SENTINEL_END}' not found in {PINE_FILE}")
        return False

    # Include the end-sentinel line in the replacement
    end_line_end = content.find("\n", end_idx)
    if end_line_end == -1:
        end_line_end = len(content)
    else:
        end_line_end += 1  # include the newline

    new_content = content[:start_idx] + new_block + "\n" + content[end_line_end:]

    if dry_run:
        print(f"[DRY RUN] Would rewrite {PINE_FILE}")
        print(f"  Block size: {len(new_block)} chars, {new_block.count(chr(10))} lines")
        return True

    with open(PINE_FILE, "w", encoding="utf-8") as f:
        f.write(new_content)
    return True


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

def main():
    parser = argparse.ArgumentParser(description="Generate Pine Script preset block from winner CSVs")
    parser.add_argument("--dry-run", action="store_true",
                        help="Print what would be done without writing files")
    args = parser.parse_args()

    print(f"Reading winner files from {WINNERS_DIR}/...")
    presets = load_winners()
    if not presets:
        print("ERROR: no winner files found. Run the optimizer first.")
        sys.exit(1)

    param_names = get_param_names(presets)
    print(f"Found {len(presets)} presets, {len(param_names)} params each")

    block = generate_block(presets, param_names)

    if args.dry_run:
        print("\n── Generated block preview (first 50 lines) ──────────────")
        for line in block.split("\n")[:50]:
            print(line)
        print("  ...")
        rewrite_pine(block, dry_run=True)
    else:
        if rewrite_pine(block, dry_run=False):
            print(f"✓  {PINE_FILE} updated with {len(presets)} presets")
            print(f"   Params: {len(param_names)}  |  Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
            print(f"   Next: open {PINE_FILE}, Select All, Copy, paste into TradingView")
        else:
            sys.exit(1)


if __name__ == "__main__":
    # Support running from project root or tools/ directory
    if not os.path.isdir(WINNERS_DIR):
        os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    main()
