"""
Live signal path for intraday MA-slope **confirm_entry_4b**.

Default data: local Alpaca RTH parquets (matches backtest). Optional IB 5m bars.
"""

from __future__ import annotations

import json
from dataclasses import asdict, replace
from datetime import date, datetime, time
from pathlib import Path
from typing import Any, Literal
from zoneinfo import ZoneInfo

import pandas as pd

from RenTech.live.ibkr_equity_orders import EquityOrderIntent, shares_for_notional, stock_position_qty
from RenTech.strategy_stack.alpaca_minute_loader import (
    DEFAULT_ALPACA_RTH_DIR,
    list_parquet_symbols,
    load_equity_panels,
)
from RenTech.strategy_stack.ma_slope_intraday_enhanced import (
    EnhancedIntradayEngine,
    baseline_enhanced_config,
)
from RenTech.strategy_stack.run_johansen_triplet_sp500 import load_sp500_sectors

NY = ZoneInfo("America/New_York")

DataSource = Literal["alpaca_parquet", "ibkr"]


def confirm_entry_config():
    return replace(
        baseline_enhanced_config(),
        hold_mode="confirm_entry",
        confirm_lag_bars=4,
    )


def default_universe(*, max_tickers: int = 500) -> list[str]:
    syms = list_parquet_symbols(DEFAULT_ALPACA_RTH_DIR)[: int(max_tickers)]
    if "SPY" not in syms:
        syms.insert(0, "SPY")
    return syms


def load_sector_map() -> dict[str, str]:
    sec = load_sp500_sectors()
    return dict(zip(sec["ticker"].astype(str).str.upper(), sec["sector"].astype(str)))


async def load_intraday_panels(
    symbols: list[str],
    *,
    data_source: DataSource,
    ib: Any | None,
    warmup_sessions: int = 15,
    ib_duration: str = "10 D",
) -> dict[str, pd.DataFrame]:
    if data_source == "alpaca_parquet":
        end = datetime.now(NY).strftime("%Y-%m-%d")
        intra, _ = load_equity_panels(
            symbols,
            data_dir=DEFAULT_ALPACA_RTH_DIR,
            bar_minutes=5,
            start=None,
            end=end,
            warmup_sessions=warmup_sessions,
            verbose=True,
        )
        return intra
    if ib is None:
        raise ValueError("ib connection required for data_source='ibkr'")
    from RenTech.live.ibkr_intraday_bars import fetch_intraday_panel_ibkr

    return await fetch_intraday_panel_ibkr(ib, symbols, duration_str=ib_duration)


def compute_target_weights(
    intra: dict[str, pd.DataFrame],
    sector_map: dict[str, str],
    *,
    top_n: int,
    as_of_et: datetime | None = None,
) -> tuple[dict[str, float], dict[str, Any]]:
    """Return equal-weight targets for confirm_entry_4b at *as_of_et* (default now)."""
    now = as_of_et or datetime.now(NY)
    if now.tzinfo is None:
        now = now.replace(tzinfo=NY)
    else:
        now = now.astimezone(NY)

    cfg = confirm_entry_config()
    eng = EnhancedIntradayEngine(config=cfg)
    panels = eng.build_panels(intra, sector_map)
    tw = eng.target_weights_enhanced(panels, top_n=top_n)

    idx = pd.to_datetime(tw.index).tz_localize(None)
    today = pd.Timestamp(now.date())
    tw = tw.copy()
    tw.index = idx
    tw_today = tw.loc[idx.normalize() == today]
    if tw_today.empty:
        return {}, {"status": "no_session_bars", "as_of_et": now.isoformat()}

    # Use latest bar with exposure (post-confirm hold row).
    gross = tw_today.sum(axis=1)
    active = tw_today.loc[gross > 1e-9]
    if active.empty:
        return {}, {"status": "flat", "as_of_et": now.isoformat(), "n_bars_today": len(tw_today)}

    # Only act once confirm window has passed (entry_bar + confirm_lag on 5m grid).
    entry_bar = int(cfg.entry_bar)
    lag = int(cfg.confirm_lag_bars)
    confirm_bar = entry_bar + lag
    bar_in_session = EnhancedIntradayEngine._session_bar_index(pd.DatetimeIndex(tw_today.index))
    max_bar = int(bar_in_session.max()) if len(bar_in_session) else -1
    meta = {
        "status": "ok",
        "as_of_et": now.isoformat(),
        "confirm_bar": confirm_bar,
        "max_bar_seen": max_bar,
        "n_bars_today": len(tw_today),
    }
    if max_bar < confirm_bar:
        meta["status"] = "waiting_confirm"
        return {}, meta

    row = active.iloc[-1]
    targets = {str(c).upper(): float(row[c]) for c in row.index if float(row[c]) > 1e-9}
    meta["symbols"] = sorted(targets)
    meta["gross"] = float(sum(targets.values()))
    return targets, meta


def in_entry_window(now_et: datetime, *, entry_after: str, entry_before: str) -> bool:
    t = now_et.timetz() if now_et.tzinfo else now_et.time()
    lo = time.fromisoformat(entry_after)
    hi = time.fromisoformat(entry_before)
    return lo <= t.replace(tzinfo=None) <= hi


def in_exit_window(now_et: datetime, *, exit_after: str, exit_before: str) -> bool:
    return in_entry_window(now_et, entry_after=exit_after, entry_before=exit_before)


def load_day_state(state_path: Path, today: date) -> dict:
    if not state_path.is_file():
        return {}
    try:
        data = json.loads(state_path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}
    if str(data.get("date")) != today.isoformat():
        return {}
    return data


def save_day_state(state_path: Path, today: date, payload: dict) -> None:
    state_path.parent.mkdir(parents=True, exist_ok=True)
    body = {"date": today.isoformat(), **payload}
    tmp = state_path.with_suffix(".json.tmp")
    tmp.write_text(json.dumps(body, indent=2) + "\n", encoding="utf-8")
    tmp.replace(state_path)


def build_entry_intents(
    targets: dict[str, float],
    portfolio_positions: tuple,
    *,
    capital_budget_usd: float,
    prices: dict[str, float],
) -> list[EquityOrderIntent]:
    intents: list[EquityOrderIntent] = []
    for sym, w in sorted(targets.items()):
        target_notional = float(capital_budget_usd) * float(w)
        held = stock_position_qty(portfolio_positions, sym)
        px = float(prices.get(sym, 0.0))
        if px <= 0:
            continue
        target_shares = shares_for_notional(target_notional, px)
        delta = target_shares - int(round(held))
        if delta <= 0:
            continue
        intents.append(
            EquityOrderIntent(
                symbol=sym,
                action="BUY",
                shares=int(delta),
                notional_usd=delta * px,
                order_type="MKT",
                reason="confirm_entry_4b",
            )
        )
    return intents


def build_exit_intents(
    state_symbols: list[str],
    portfolio_positions: tuple,
) -> list[EquityOrderIntent]:
    intents: list[EquityOrderIntent] = []
    for sym in state_symbols:
        held = int(round(stock_position_qty(portfolio_positions, sym)))
        if held <= 0:
            continue
        intents.append(
            EquityOrderIntent(
                symbol=sym,
                action="SELL",
                shares=held,
                notional_usd=0.0,
                order_type="MOC",
                reason="hold_to_moc",
            )
        )
    return intents


def recommendation_payload(
    *,
    phase: str,
    targets: dict[str, float],
    meta: dict[str, Any],
    intents: list[EquityOrderIntent],
    capital_budget_usd: float,
) -> dict:
    return {
        "phase": phase,
        "strategy": "ma_slope_intraday_confirm_entry_4b",
        "capital_budget_usd": capital_budget_usd,
        "targets": targets,
        "signal_meta": meta,
        "orders": [asdict(i) for i in intents],
    }
