#!/usr/bin/env python3
"""
IBKR paper/live runner for intraday MA-slope **confirm_entry_4b**.

**Paper default:** TWS port **7497**, ``client_id=4``.

Workflow (recommend-only first)::

    # 1) TWS paper + API enabled
    # 2) One-shot recommendation (~10:45 ET entry window)
    cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 .venv/bin/python \\
        -m RenTech.live.run_ma_slope_intraday_ibkr --recommend-only

    # 3) Loop during RTH (checks every 60s)
    cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 .venv/bin/python \\
        -m RenTech.live.run_ma_slope_intraday_ibkr --loop --recommend-only

    # 4) Paper auto-trade (after validating recommendations)
    cd /Users/robzingale/trading_bot && PYTHONUNBUFFERED=1 .venv/bin/python \\
        -m RenTech.live.run_ma_slope_intraday_ibkr --loop --live

Kill switch: ``RenTech/live/config/KILL_SWITCH`` with content ``HALT``.
"""

from __future__ import annotations

import argparse
import asyncio
import json
import sys
import time
import uuid
from datetime import date, datetime
from pathlib import Path
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.ibkr_equity_orders import last_price, place_stock_order
from RenTech.live.ibkr_session import connect_ib, disconnect_ib, ensure_positions_loaded, has_pending_orders
from RenTech.live.ma_slope_intraday_live import (
    build_entry_intents,
    build_exit_intents,
    compute_target_weights,
    default_universe,
    in_entry_window,
    in_exit_window,
    load_day_state,
    load_intraday_panels,
    load_sector_map,
    recommendation_payload,
    save_day_state,
)
from RenTech.live.platform_config import load_platform_config
from RenTech.live.portfolio import fetch_portfolio_snapshot
from RenTech.live.risk_gate import evaluate_platform_risk

NY = ZoneInfo("America/New_York")
DEFAULT_CONFIG = _REPO / "RenTech" / "live" / "config" / "live_ma_slope_intraday.json"


async def _run_cycle(
    cfg_path: Path,
    *,
    recommend_only: bool,
    force: bool = False,
) -> dict:
    cfg = load_platform_config(cfg_path)
    rec_only = recommend_only or cfg.risk.recommend_only
    scfg = {}
    for slot in cfg.strategies:
        if slot.id == "ma_slope_intraday":
            scfg = dict(slot.config)
            break

    now_et = datetime.now(NY)
    today = now_et.date()
    run_id = uuid.uuid4().hex[:10]

    top_n = int(scfg.get("top_n", 10))
    max_tickers = int(scfg.get("max_tickers", 500))
    data_source = str(scfg.get("data_source", "alpaca_parquet"))
    nav_pct = float(scfg.get("capital_budget_nav_pct", 0.10))
    entry_after = str(scfg.get("entry_after_et", "10:40"))
    entry_before = str(scfg.get("entry_before_et", "11:05"))
    exit_after = str(scfg.get("exit_after_et", "15:55"))
    exit_before = str(scfg.get("exit_before_et", "16:00"))

    state_path = Path(str(scfg.get("state_path", "RenTech/data/live_state/ma_slope_intraday.json")))
    if not state_path.is_absolute():
        state_path = (_REPO / state_path).resolve()
    rec_out = Path(
        str(scfg.get("recommend_out", "RenTech/data/logs/ma_slope_intraday_ibkr_recommendation.json"))
    )
    if not rec_out.is_absolute():
        rec_out = (_REPO / rec_out).resolve()

    day = load_day_state(state_path, today)
    summary: dict = {
        "run_id": run_id,
        "as_of_et": now_et.isoformat(),
        "recommend_only": rec_only,
        "phase": "idle",
        "orders": [],
    }

    ib = await connect_ib(cfg.broker)
    try:
        await ensure_positions_loaded(ib)
        portfolio = await fetch_portfolio_snapshot(ib)
        risk = evaluate_platform_risk(cfg, portfolio, today, recommend_only=rec_only, ib=ib)
        summary["risk"] = {"action": risk.action.value, "reasons": risk.reasons}
        if risk.action.value == "halt_all":
            summary["phase"] = "halted"
            return summary

        budget = max(0.0, portfolio.net_liquidation_usd * nav_pct)
        summary["capital_budget_usd"] = budget
        symbols = default_universe(max_tickers=max_tickers)

        # --- EXIT (MOC) ---
        if (force or in_exit_window(now_et, exit_after=exit_after, exit_before=exit_before)) and not day.get(
            "exit_done"
        ):
            held_syms = list(day.get("symbols") or [])
            intents = build_exit_intents(held_syms, portfolio.positions)
            summary["phase"] = "exit_moc"
            payload = recommendation_payload(
                phase="exit_moc",
                targets={},
                meta={"symbols": held_syms},
                intents=intents,
                capital_budget_usd=budget,
            )
            rec_out.parent.mkdir(parents=True, exist_ok=True)
            rec_out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")

            if not rec_only and intents:
                if cfg.risk.block_if_pending_orders and has_pending_orders(ib):
                    summary["error"] = "pending orders block exit"
                    return summary
                tickets = []
                for intent in intents:
                    tickets.append(await place_stock_order(ib, intent, recommend_only=False))
                summary["orders"] = tickets
                save_day_state(
                    state_path,
                    today,
                    {**day, "exit_done": True, "exit_run_id": run_id},
                )
            else:
                summary["orders"] = [o.__dict__ for o in intents]
            return summary

        # --- ENTRY (confirm_entry_4b) ---
        if not (force or in_entry_window(now_et, entry_after=entry_after, entry_before=entry_before)):
            summary["phase"] = "outside_entry_window"
            return summary
        if day.get("entry_done"):
            summary["phase"] = "entry_already_done"
            return summary
        if not risk.allow_new_entries:
            summary["phase"] = "entries_blocked"
            return summary

        print(f"[ma_slope_intraday] Loading panels ({data_source}) n={len(symbols)} …", flush=True)
        intra = await load_intraday_panels(
            symbols, data_source=data_source, ib=ib, warmup_sessions=15  # type: ignore[arg-type]
        )
        targets, meta = compute_target_weights(
            intra, load_sector_map(), top_n=top_n, as_of_et=now_et
        )
        summary["signal_meta"] = meta
        summary["targets"] = targets

        if meta.get("status") == "waiting_confirm":
            summary["phase"] = "waiting_confirm"
            return summary
        if not targets:
            summary["phase"] = "flat_signal"
            return summary

        prices: dict[str, float] = {}
        for sym in targets:
            try:
                prices[sym] = await last_price(ib, sym)
            except Exception as exc:
                print(f"  [price] {sym}: {exc}", flush=True)

        intents = build_entry_intents(
            targets, portfolio.positions, capital_budget_usd=budget, prices=prices
        )
        summary["phase"] = "entry"
        payload = recommendation_payload(
            phase="entry",
            targets=targets,
            meta=meta,
            intents=intents,
            capital_budget_usd=budget,
        )
        rec_out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
        print(f"[ma_slope_intraday] Wrote {rec_out}", flush=True)

        if not rec_only and intents:
            if cfg.risk.block_if_pending_orders and has_pending_orders(ib):
                summary["error"] = "pending orders block entry"
                return summary
            tickets = []
            for intent in intents:
                tickets.append(await place_stock_order(ib, intent, recommend_only=False))
            summary["orders"] = tickets
            save_day_state(
                state_path,
                today,
                {
                    "entry_done": True,
                    "exit_done": False,
                    "symbols": sorted(targets),
                    "weights": targets,
                    "entry_run_id": run_id,
                },
            )
        else:
            summary["orders"] = [i.__dict__ for i in intents]

        return summary
    finally:
        await disconnect_ib(ib)


def main() -> None:
    ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    ap.add_argument("--config", type=Path, default=DEFAULT_CONFIG)
    ap.add_argument("--recommend-only", action="store_true", help="Tickets only (default)")
    ap.add_argument("--live", action="store_true", help="Place orders (paper/live per TWS port)")
    ap.add_argument("--loop", action="store_true", help="Poll every 60s during RTH")
    ap.add_argument("--interval-sec", type=int, default=60)
    ap.add_argument("--force", action="store_true", help="Ignore entry/exit time windows (testing)")
    args = ap.parse_args()

    rec_only = True if args.recommend_only else (False if args.live else True)

    async def once() -> dict:
        return await _run_cycle(
            args.config.expanduser().resolve(),
            recommend_only=rec_only,
            force=bool(args.force),
        )

    if not args.loop:
        out = asyncio.run(once())
        print(json.dumps(out, indent=2))
        return

    print(f"[loop] recommend_only={rec_only} interval={args.interval_sec}s", flush=True)
    while True:
        now = datetime.now(NY)
        if now.weekday() >= 5:
            time.sleep(300)
            continue
        t = now.time()
        if t < datetime.strptime("09:25", "%H:%M").time() or t > datetime.strptime("16:05", "%H:%M").time():
            time.sleep(120)
            continue
        try:
            out = asyncio.run(once())
            print(json.dumps(out, indent=2), flush=True)
        except Exception as exc:
            print(f"[loop] error: {exc}", flush=True)
        time.sleep(max(15, int(args.interval_sec)))


if __name__ == "__main__":
    main()
