#!/usr/bin/env python3
"""
Batch promote — Custom Lab GOLD por ticker.

Itera por una lista de tickers que han sacado tier GOLD en el ultimo Custom Lab
run (en 1h con `box_prevday_hammer_long`) y hace POST /api/promote para cada
uno via el servidor Leonex. Util para evitar pulsar 12 botones a mano.

USO:
    python tools/batch_promote_custom_gold.py
    python tools/batch_promote_custom_gold.py --server https://leonex.tudominio.com
    python tools/batch_promote_custom_gold.py --strategy box_prevday_hammer_long --dry-run
    python tools/batch_promote_custom_gold.py --tickers CEG,CMI,ABNB

NOTAS IMPORTANTES:
- Promueve la estrategia a `asset_strategies` para que el Strategy Tracker la
  siga en forward (out-of-sample, paper-traded performance accumulation).
- El bridge intraday actual NO opera estrategias custom todavia (solo opera
  strategies con formato trigger|filter|exit). Eso significa que la promocion
  registra la estrategia para tracking pero NO genera ordenes Alpaca todavia.
  Si quieres operacion automatica, dimelo y extiendo el bridge.
- El servidor tiene un _refresh_lock global: si hay un job corriendo (refresh,
  intraday bridge...) el promote devuelve 409 y este script reintenta con
  backoff exponencial hasta 3 veces.
"""

from __future__ import annotations

import argparse
import json
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

# Lista por defecto: los 12 tickers que aparecieron GOLD en el ultimo Custom
# Lab run de box_prevday_hammer_long en 1h (segun el dashboard del usuario).
DEFAULT_GOLD_TICKERS = [
    "CEG", "CMI", "ABNB", "EW", "EXPE", "LRCX",
    "GLW", "REG", "ALGN", "GM", "BBY", "MU",
]
DEFAULT_STRATEGY = "box_prevday_hammer_long"
DEFAULT_SERVER = "http://localhost:8765"


def promote_one(server: str, ticker: str, strategy: str,
                timeout_s: int = 120) -> dict:
    """Llama POST /api/promote?ticker=X&strategy=Y. Devuelve dict con
    {status, payload, http_code, error}. Maneja 409 (lock busy) con retry."""
    url = f"{server.rstrip('/')}/api/promote?" + urllib.parse.urlencode({
        "ticker": ticker,
        "strategy": strategy,
    })
    req = urllib.request.Request(url, method="POST")

    max_retries = 3
    delay = 5.0
    last_err = None
    for attempt in range(1, max_retries + 1):
        try:
            with urllib.request.urlopen(req, timeout=timeout_s) as r:
                body = r.read().decode("utf-8", errors="replace")
                try:
                    payload = json.loads(body)
                except Exception:
                    payload = {"raw": body}
                return {
                    "status": "ok" if r.status == 200 else "non_200",
                    "http_code": r.status,
                    "payload": payload,
                    "attempts": attempt,
                }
        except urllib.error.HTTPError as e:
            body = e.read().decode("utf-8", errors="replace")
            try:
                payload = json.loads(body)
            except Exception:
                payload = {"raw": body}
            if e.code == 409 and attempt < max_retries:
                holder = (payload.get("lock_status") or {}).get("holder", "?")
                age = (payload.get("lock_status") or {}).get("age_seconds", "?")
                print(f"    [WAIT] 409 lock busy (holder={holder}, age={age}s) "
                      f"— retry {attempt}/{max_retries} en {delay:.0f}s")
                time.sleep(delay)
                delay *= 2.0
                continue
            return {
                "status": "http_error",
                "http_code": e.code,
                "payload": payload,
                "attempts": attempt,
            }
        except urllib.error.URLError as e:
            last_err = str(e)
            if attempt < max_retries:
                print(f"    [WAIT] URL error ({e}) — retry {attempt}/{max_retries} en {delay:.0f}s")
                time.sleep(delay)
                delay *= 2.0
                continue
            return {"status": "connection_error", "error": last_err,
                    "attempts": attempt}
        except Exception as e:
            return {"status": "exception", "error": repr(e),
                    "attempts": attempt}

    return {"status": "retries_exhausted", "error": last_err}


def main() -> int:
    parser = argparse.ArgumentParser(description="Batch promote Custom Lab GOLD")
    parser.add_argument("--server", default=DEFAULT_SERVER,
                        help=f"Base URL del servidor Leonex (default {DEFAULT_SERVER})")
    parser.add_argument("--strategy", default=DEFAULT_STRATEGY,
                        help=f"Nombre de la estrategia (default {DEFAULT_STRATEGY})")
    parser.add_argument("--tickers", default=",".join(DEFAULT_GOLD_TICKERS),
                        help="Lista de tickers separados por coma")
    parser.add_argument("--pause", type=float, default=2.0,
                        help="Pausa en segundos entre llamadas (default 2.0)")
    parser.add_argument("--dry-run", action="store_true",
                        help="Solo imprime lo que haria, no llama al servidor")
    args = parser.parse_args()

    tickers = [t.strip().upper() for t in args.tickers.split(",") if t.strip()]
    if not tickers:
        print("[ERROR] Sin tickers — usa --tickers AAPL,MSFT,...")
        return 2

    print("=" * 72)
    print(f"Batch promote — {len(tickers)} tickers")
    print(f"Strategy : {args.strategy}")
    print(f"Server   : {args.server}")
    print(f"Pause    : {args.pause}s entre llamadas")
    print(f"Mode     : {'DRY-RUN' if args.dry_run else 'EXECUTE'}")
    print("=" * 72)
    print()
    print("RECORDATORIO: el bridge intraday todavia NO opera estrategias custom")
    print("(solo opera trigger|filter|exit). Esta promocion las registra para el")
    print("Strategy Tracker (forward paper). Para operacion en Alpaca, hay que")
    print("extender el bridge a custom strategies — pidemelo si lo quieres.")
    print()

    results: list[dict] = []
    for i, t in enumerate(tickers, 1):
        prefix = f"[{i}/{len(tickers)}] {t:<6} {args.strategy}"
        if args.dry_run:
            print(f"{prefix} → DRY-RUN (no se llama al servidor)")
            results.append({"ticker": t, "status": "dry_run"})
            continue
        print(f"{prefix} → POST /api/promote ...")
        r = promote_one(args.server, t, args.strategy)
        results.append({"ticker": t, **r})
        st = r.get("status")
        if st == "ok":
            print(f"    [OK] HTTP 200 — registrado en asset_strategies"
                  f" (intentos={r['attempts']})")
        elif st == "http_error":
            err = (r.get("payload") or {}).get("error", "?")
            stderr_tail = (r.get("payload") or {}).get("stderr_tail")
            print(f"    [ERR] HTTP {r.get('http_code')} error={err}")
            if stderr_tail:
                for line in stderr_tail[-3:]:
                    print(f"           {line}")
        else:
            print(f"    [FAIL] {st}: {r.get('error') or r.get('payload')}")
        if i < len(tickers) and args.pause > 0:
            time.sleep(args.pause)

    # Resumen final
    print()
    print("=" * 72)
    print("RESUMEN")
    print("=" * 72)
    n_ok = sum(1 for r in results if r.get("status") == "ok")
    n_fail = len(results) - n_ok
    print(f"OK   : {n_ok}/{len(results)}")
    print(f"FAIL : {n_fail}/{len(results)}")
    if n_fail:
        print()
        print("Failed tickers:")
        for r in results:
            if r.get("status") != "ok" and r.get("status") != "dry_run":
                print(f"  {r['ticker']:<6} ({r.get('status')})")
    print()
    print("Verifica los promovidos en el dashboard → Strategy Tracker.")
    return 0 if n_fail == 0 else 1


if __name__ == "__main__":
    sys.exit(main())
