#!/usr/bin/env python3
"""
Build JSON snapshot for the **stock-only Best Ideas + IBKR** command center.

Example::

    cd /Users/robzingale/trading_bot
    PYTHONUNBUFFERED=1 .venv/bin/python -m RenTech.live.run_stock_only_signals
    PYTHONUNBUFFERED=1 .venv/bin/python -m RenTech.monitor.build_stock_command_center_snapshot --live-signals

    # Offline (Yahoo signals, no TWS)
    PYTHONUNBUFFERED=1 .venv/bin/python -m RenTech.live.run_stock_only_signals --offline
    PYTHONUNBUFFERED=1 .venv/bin/python -m RenTech.monitor.build_stock_command_center_snapshot --live-signals

Output: ``RenTech/data/logs/stock_command_center_snapshot.json``
"""

from __future__ import annotations

import argparse
import asyncio
import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo

_REPO = Path(__file__).resolve().parents[2]
if str(_REPO) not in sys.path:
    sys.path.insert(0, str(_REPO))

from RenTech.live.stock_book_utils import STOCK_ONLY_WEIGHTS, STOCK_SLEEVE_LABELS  # noqa: E402
from RenTech.monitor.build_command_center_snapshot import (  # noqa: E402
    _fetch_ibkr_portfolio,
    _ib_option_legs,
    _ib_stock_mv,
    _load_fund_portfolio_summary,
    _read_json,
)

NY = ZoneInfo("America/New_York")
LOGS = _REPO / "RenTech" / "data" / "logs"
DEFAULT_OUT = LOGS / "stock_command_center_snapshot.json"
LIVE_SIGNALS_PATH = LOGS / "live_stock_only_signals.json"

FUND_DAILY_DEFAULT = (
    LOGS
    / "stock_only_ma_slope_fund15_2011_plus_stock_only_plus_sp500_dip_plus_tactical_aw_plus_tsmom_plus_johansen_etf_plus_ma_slope_topn_plus_ma_slope_inverse_plus_vol_edge_plus_fund_plus_nav_q_mtm_daily.csv"
)
FUND_META_DEFAULT = FUND_DAILY_DEFAULT.with_name(
    FUND_DAILY_DEFAULT.name.replace("_daily.csv", "_meta.json")
)
FUND_YEARLY_DEFAULT = FUND_DAILY_DEFAULT.with_name(
    FUND_DAILY_DEFAULT.name.replace("_daily.csv", "_yearly.csv")
)


def _build_signal_explain(live: dict[str, Any] | None, portfolio: dict[str, Any]) -> dict[str, Any]:
    fund_nav = float(live.get("fund_nav_usd") if live else portfolio.get("fund_nav_usd", 100_000))
    fund_scale = float(live.get("fund_scale", 1.5) if live else 1.5)
    weights = (live or {}).get("fund_weights") or portfolio.get("fund_weights") or STOCK_ONLY_WEIGHTS
    sig = (live or {}).get("signals") or {}
    ib_mv = _ib_stock_mv(((live or {}).get("ibkr") or {}).get("portfolio"))

    sleeves_out: list[dict[str, Any]] = []
    for key, label in STOCK_SLEEVE_LABELS.items():
        w = float(weights.get(key, 0))
        sl = sig.get(key, {})
        targets = sl.get("targets") or {}
        budget = fund_nav * w * fund_scale
        positions = []
        for sym, tw in sorted(targets.items(), key=lambda x: -abs(x[1])):
            positions.append(
                {
                    "symbol": sym,
                    "target_weight": tw,
                    "target_notional_usd": round(budget * abs(tw), 2),
                    "ib_market_value_usd": round(ib_mv.get(sym, 0), 2),
                }
            )
        rules = {
            "tactical_aw": [
                "SPY/TLT/IEF/GLD/DBC baseline weights; gate each ETF on SMA200 + 12-1m momentum.",
                "Rebalance when gates flip; remainder cash.",
            ],
            "equity_dip": [
                "Prior session −3% drop, SMA200 up, ATR%/close > 3%; limit entry at close−0.9×ATR.",
                "Exits: >10d, prior high, or +0.5×ATR profit; max 10 names.",
            ],
            "vol_edge": [
                "eVRP + VIX/VIX3M term structure → SVIX/SVXY short vol or VIXY long vol.",
                "Rebalance when weight drift > 2%.",
            ],
            "ma_slope_topn": [
                "Monthly top-10 S&P 500 by dual EMA slope; equal weight; 2×ATR stops intra-month.",
            ],
            "johansen_etf": [
                "Six ETF triplets, Johansen cointegration, equal-weight combine; causal refit.",
            ],
            "tsmom": [
                "8-asset 3/6/12m momentum blend; vol-normalized; monthly rebalance; long and short.",
            ],
            "ma_slope_inverse": [
                "Long SH when SPY dual-slope bearish OR below SMA200; exit when regime clears.",
            ],
        }.get(key, [])
        sleeves_out.append(
            {
                "key": key,
                "label": label,
                "weight": w,
                "sleeve_budget_usd": round(budget, 2),
                "rules_summary": rules,
                "source": sl.get("source", live.get("data_source") if live else "backtest"),
                "meta": {k: v for k, v in sl.items() if k != "targets"},
                "targets": targets,
                "positions": positions,
            }
        )

    return {
        "blurb": "Stock-only Best Ideas book · fund_scale sizing · recommend-only (no auto orders)",
        "fund_nav_usd": fund_nav,
        "fund_scale": fund_scale,
        "sleeves": sleeves_out,
    }


def build_stock_snapshot(
    *,
    use_ibkr: bool = False,
    live_signals: bool = False,
    live_signals_from_file: bool = True,
    live_signals_path: Path | None = None,
    fund_daily: Path | None = None,
    fund_meta: Path | None = None,
    fund_yearly: Path | None = None,
    fund_scale: float = 1.5,
) -> dict[str, Any]:
    now = datetime.now(NY)
    live_payload: dict[str, Any] | None = None
    path = live_signals_path or LIVE_SIGNALS_PATH

    if live_signals:
        from RenTech.live.stock_only_signals import run_stock_only_signals_async, write_stock_only_signals

        live_payload = asyncio.run(
            run_stock_only_signals_async(fund_scale=fund_scale, offline=not use_ibkr)
        )
        write_stock_only_signals(path, live_payload)
    elif live_signals_from_file:
        data = _read_json(path)
        if isinstance(data, dict):
            live_payload = data

    ib_portfolio, ib_err = (None, None)
    if live_payload and (live_payload.get("ibkr") or {}).get("portfolio"):
        ib_portfolio = live_payload["ibkr"]["portfolio"]
    elif use_ibkr:
        ib_portfolio, ib_err = asyncio.run(_fetch_ibkr_portfolio())

    daily_path = fund_daily or FUND_DAILY_DEFAULT
    meta_path = fund_meta or (FUND_META_DEFAULT if FUND_META_DEFAULT.is_file() else None)
    yearly_path = fund_yearly or (FUND_YEARLY_DEFAULT if FUND_YEARLY_DEFAULT.is_file() else None)

    portfolio = _load_fund_portfolio_summary(daily_path, meta_path, yearly_path)
    if portfolio.get("status") != "ok":
        portfolio = {"status": "missing", "path": str(daily_path), "fund_weights": STOCK_ONLY_WEIGHTS}

    for key, label in STOCK_SLEEVE_LABELS.items():
        if key not in (portfolio.get("sleeves") or {}):
            w = float(STOCK_ONLY_WEIGHTS.get(key, 0))
            portfolio.setdefault("sleeves", {})[key] = {
                "label": label,
                "weight": w,
            }

    kill_switch = (_REPO / "RenTech/live/config/KILL_SWITCH").is_file()
    actions = list((live_payload or {}).get("actions") or [])

    return {
        "generated_at": now.isoformat(),
        "book": "stock_only_best_ideas",
        "data_source": (live_payload or {}).get("data_source", "backtest_csv"),
        "fund_scale": fund_scale,
        "fund_weights": (live_payload or {}).get("fund_weights") or STOCK_ONLY_WEIGHTS,
        "portfolio": portfolio,
        "fund": {
            "backtest": portfolio,
            "live_nav_usd": (live_payload or {}).get("fund_nav_usd"),
            "sleeve_budgets_usd": (live_payload or {}).get("sleeve_budgets_usd"),
            "reference_note": "Backtest KPIs from combine CSV; live targets from run_stock_only_signals",
        },
        "ibkr": {
            "connected": ib_portfolio is not None,
            "error": ib_err,
            "portfolio": ib_portfolio,
            "option_positions": _ib_option_legs(ib_portfolio),
        },
        "signals": (live_payload or {}).get("signals") or {},
        "actions": actions,
        "signal_explain": _build_signal_explain(live_payload, portfolio),
        "risk": {
            "kill_switch_active": kill_switch,
            "recommend_only": True,
        },
        "commands": {
            "refresh_signals_ibkr": (
                "cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 "
                ".venv/bin/python -m RenTech.live.run_stock_only_signals --fund-scale "
                f"{fund_scale:.1f}"
            ),
            "refresh_signals_offline": (
                "cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 "
                ".venv/bin/python -m RenTech.live.run_stock_only_signals --offline "
                f"--fund-scale {fund_scale:.1f}"
            ),
            "rebuild_snapshot": (
                "cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 "
                ".venv/bin/python -m RenTech.monitor.build_stock_command_center_snapshot --live-signals"
            ),
            "serve_ui": "cd /Users/robzingale/trading_bot && python -m http.server 8080",
        },
    }


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--out", type=Path, default=DEFAULT_OUT)
    ap.add_argument("--ibkr", action="store_true", help="Connect to TWS when refreshing live signals")
    ap.add_argument(
        "--live-signals",
        action="store_true",
        help="Run live signal refresh (reads JSON by default if omitted)",
    )
    ap.add_argument("--no-live-file", action="store_true", help="Do not read existing live_stock_only_signals.json")
    ap.add_argument("--fund-scale", type=float, default=1.5)
    ap.add_argument("--fund-daily", type=Path, default=None)
    ap.add_argument("--fund-meta", type=Path, default=None)
    ap.add_argument("--fund-yearly", type=Path, default=None)
    args = ap.parse_args()

    snap = build_stock_snapshot(
        use_ibkr=bool(args.ibkr),
        live_signals=bool(args.live_signals),
        live_signals_from_file=not args.no_live_file,
        fund_daily=args.fund_daily,
        fund_meta=args.fund_meta,
        fund_yearly=args.fund_yearly,
        fund_scale=float(args.fund_scale),
    )
    out = args.out.expanduser().resolve()
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(json.dumps(snap, indent=2, default=str) + "\n", encoding="utf-8")
    print(f"Wrote {out}")
    print(
        f"  IBKR connected: {snap['ibkr']['connected']}  "
        f"actions: {len(snap.get('actions', []))}  "
        f"live NAV: {snap['fund'].get('live_nav_usd')}"
    )
    if snap["ibkr"].get("error"):
        print(f"  IBKR note: {snap['ibkr']['error']}")


if __name__ == "__main__":
    main()
