"""
CrackingMarkets dip live helpers: state, sizing, entry/exit intents.

Used by ``StockBookIbkrStrategy`` (recommend-only by default).
"""

from __future__ import annotations

import json
import math
from datetime import date, datetime
from pathlib import Path
from typing import Any

import pandas as pd

from RenTech.live.ibkr_equity_orders import EquityOrderIntent, shares_for_notional, stock_position_qty
from RenTech.live.protocols import PortfolioSnapshot
from RenTech.live.stock_book_utils import (
    CORE_BOOK_ETFS,
    DEFAULT_DIP_MAX_HOLDINGS,
    STOCK_ONLY_WEIGHTS,
    dip_open_symbols_from_ib_mv,
    dip_per_name_budget_usd,
    sleeve_budget_usd,
)
from RenTech.live.stock_only_signals import _dip_actions, _scan_cm_dip_signals
from RenTech.strategy_stack.data_loader import DataLoader

STRATEGY_ID = "stock_book_cm_dip"


def _resolve_path(repo_root: Path, p: str | Path) -> Path:
    path = Path(p)
    return path if path.is_absolute() else (repo_root / path).resolve()


def load_dip_state(path: Path) -> dict[str, Any]:
    if not path.is_file():
        return {"positions": {}}
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError):
        return {"positions": {}}
    if not isinstance(data, dict):
        return {"positions": {}}
    pos = data.get("positions")
    if not isinstance(pos, dict):
        data["positions"] = {}
    return data


def save_dip_state(path: Path, state: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(".tmp")
    tmp.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
    tmp.replace(path)


def open_dip_market_value_usd(portfolio: PortfolioSnapshot, open_symbols: set[str]) -> float:
    """Gross long MV of open dip names. Falls back to avg_cost×qty when IB MV is 0."""
    total = 0.0
    for leg in portfolio.positions:
        if leg.sec_type != "STK":
            continue
        if leg.symbol.upper() not in open_symbols:
            continue
        if float(leg.position) <= 0:
            continue
        mv = abs(float(leg.market_value))
        if mv < 1.0:
            # ib.positions() often omits marketValue; avgCost is per-share for STK
            mv = abs(float(leg.avg_cost) * float(leg.position))
        total += mv
    return total


def open_dip_symbols(
    state: dict[str, Any],
    portfolio: PortfolioSnapshot,
    *,
    sync_ib_non_etf: bool = True,
) -> set[str]:
    """Union of state-tracked dips still long at IB + optional IB non-ETF longs."""
    out: set[str] = set()
    positions = state.get("positions") or {}
    for sym, meta in positions.items():
        s = str(sym).upper()
        qty = stock_position_qty(portfolio.positions, s)
        if qty > 0.5:
            out.add(s)
        elif isinstance(meta, dict) and float(meta.get("shares") or 0) > 0:
            # pending / recommend — still count toward max if flagged open
            if meta.get("status") in ("open", "pending_entry", None):
                if qty > 0.5 or meta.get("status") == "pending_entry":
                    out.add(s)
    if sync_ib_non_etf:
        ib_mv = {
            leg.symbol.upper(): float(leg.market_value)
            for leg in portfolio.positions
            if leg.sec_type == "STK"
        }
        out |= dip_open_symbols_from_ib_mv(ib_mv)
    return out


def sleeve_nav_usd(
    portfolio: PortfolioSnapshot,
    *,
    fund_scale: float = 1.5,
    equity_dip_weight: float | None = None,
) -> float:
    w = float(equity_dip_weight if equity_dip_weight is not None else STOCK_ONLY_WEIGHTS.get("equity_dip", 0.17))
    return sleeve_budget_usd(float(portfolio.net_liquidation_usd), w, float(fund_scale))


def in_hhmm_window(now_et: datetime, after: str, before: str) -> bool:
    def _hm(s: str) -> tuple[int, int]:
        parts = str(s).strip().split(":")
        return int(parts[0]), int(parts[1]) if len(parts) > 1 else 0

    ah, am = _hm(after)
    bh, bm = _hm(before)
    mins = now_et.hour * 60 + now_et.minute
    return (ah * 60 + am) <= mins < (bh * 60 + bm)


def _prior_session_high(symbol: str, asof: date) -> float | None:
    loader = DataLoader()
    raw = loader.fetch_daily(symbol, period="3mo")
    if raw is None or raw.empty:
        return None
    df = raw.copy()
    df.columns = [str(c).lower() for c in df.columns]
    df.index = pd.to_datetime(df.index).tz_localize(None)
    df = df.sort_index()
    if "high" not in df.columns:
        return None
    cutoff = pd.Timestamp(asof) - pd.Timedelta(days=1)
    hist = df.loc[:cutoff]
    if hist.empty:
        hist = df.iloc[:-1] if len(df) > 1 else df
    if hist.empty:
        return None
    return float(hist["high"].iloc[-1])


def _trading_days_held(entry_date: str | date, today: date) -> int:
    ed = pd.Timestamp(entry_date).date() if not isinstance(entry_date, date) else entry_date
    if today < ed:
        return 0
    return int(len(pd.bdate_range(ed, today)))


def evaluate_exits(
    state: dict[str, Any],
    portfolio: PortfolioSnapshot,
    *,
    today: date,
    profit_atr_mult: float = 0.5,
    hold_trading_days: int = 10,
    last_prices: dict[str, float] | None = None,
) -> list[EquityOrderIntent]:
    """Close-based CM exits → MOC sell intents for names still held at IB."""
    last_prices = last_prices or {}
    intents: list[EquityOrderIntent] = []
    positions = dict(state.get("positions") or {})

    for sym, meta in list(positions.items()):
        s = str(sym).upper()
        if not isinstance(meta, dict):
            continue
        qty = stock_position_qty(portfolio.positions, s)
        if qty <= 0.5:
            continue
        shares = max(1, int(round(abs(qty))))
        px = float(last_prices.get(s) or 0.0)
        if px <= 0:
            # fall back to avg cost mark — skip profit check if unknown
            px = float("nan")

        fill = float(meta.get("fill_price") or meta.get("limit_price") or 0.0)
        atr = float(meta.get("atr_at_signal") or meta.get("atr_usd") or 0.0)
        entry = meta.get("entry_date") or meta.get("signal_date")
        reasons: list[str] = []

        if math.isfinite(px) and fill > 0 and atr > 0 and px >= fill + profit_atr_mult * atr:
            reasons.append("profit_atr")
        prior_hi = _prior_session_high(s, today)
        if prior_hi is not None and math.isfinite(px) and px > prior_hi:
            reasons.append("prior_high")
        if entry and _trading_days_held(str(entry), today) > int(hold_trading_days):
            reasons.append("time_stop")

        if not reasons:
            continue
        intents.append(
            EquityOrderIntent(
                symbol=s,
                action="SELL",
                shares=shares,
                notional_usd=float(shares * (px if math.isfinite(px) else fill)),
                order_type="MOC",
                reason=f"cm_dip_exit:{'+'.join(reasons)}",
            )
        )
    return intents


def build_entry_actions(
    *,
    fund_nav: float,
    fund_scale: float,
    equity_dip_weight: float,
    max_holdings: int,
    open_symbols: set[str],
    limit_atr_mult: float = 0.9,
    profit_atr_mult: float = 0.5,
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    signals, meta = _scan_cm_dip_signals(
        limit_atr_mult=limit_atr_mult,
        profit_atr_mult=profit_atr_mult,
    )
    actions = _dip_actions(
        signals,
        fund_nav=fund_nav,
        fund_weight=equity_dip_weight,
        fund_scale=fund_scale,
        max_holdings=max_holdings,
        open_symbols=open_symbols,
    )
    return actions, {**meta, "open_symbols": sorted(open_symbols)}


def actions_to_entry_intents(
    actions: list[dict[str, Any]],
    *,
    prices: dict[str, float] | None = None,
) -> list[EquityOrderIntent]:
    """Convert ADD dip actions into DAY LMT intents (shares from notional / limit)."""
    prices = prices or {}
    intents: list[EquityOrderIntent] = []
    for a in actions:
        if a.get("action") != "ADD" or a.get("book") != "equity_dip":
            continue
        det = a.get("detail") or {}
        sym = str(a.get("symbol") or det.get("ticker") or "").upper()
        lim = float(det.get("limit_price") or 0.0)
        notional = float(det.get("target_notional_usd") or 0.0)
        if not sym or lim <= 0 or notional <= 0:
            continue
        # Prefer limit for share count; fall back to last price.
        ref = lim if lim > 0 else float(prices.get(sym) or 0.0)
        shares = shares_for_notional(notional, ref)
        if shares <= 0:
            continue
        intents.append(
            EquityOrderIntent(
                symbol=sym,
                action="BUY",
                shares=shares,
                notional_usd=notional,
                order_type="LMT",
                reason="cm_dip_entry",
                limit_price=lim,
                tif="DAY",
            )
        )
    return intents


def record_entries_in_state(
    state: dict[str, Any],
    intents: list[EquityOrderIntent],
    actions: list[dict[str, Any]],
    *,
    status: str = "pending_entry",
) -> dict[str, Any]:
    pos = dict(state.get("positions") or {})
    by_sym = {
        str(a.get("symbol") or "").upper(): (a.get("detail") or {})
        for a in actions
        if a.get("action") == "ADD"
    }
    for intent in intents:
        det = by_sym.get(intent.symbol, {})
        pos[intent.symbol] = {
            "status": status,
            "shares": int(intent.shares),
            "limit_price": intent.limit_price,
            "fill_price": float(det.get("limit_price") or intent.limit_price or 0.0),
            "atr_at_signal": float(det.get("atr_usd") or 0.0),
            "atr_usd": float(det.get("atr_usd") or 0.0),
            "signal_date": det.get("signal_date"),
            "entry_date": det.get("entry_date"),
            "profit_target": det.get("profit_target"),
            "target_notional_usd": intent.notional_usd,
        }
    out = dict(state)
    out["positions"] = pos
    out["updated_at"] = datetime.now().isoformat(timespec="seconds")
    return out


def mark_exits_in_state(state: dict[str, Any], symbols: list[str]) -> dict[str, Any]:
    pos = dict(state.get("positions") or {})
    for s in symbols:
        pos.pop(str(s).upper(), None)
    out = dict(state)
    out["positions"] = pos
    out["updated_at"] = datetime.now().isoformat(timespec="seconds")
    return out


def recommendation_payload(
    *,
    phase: str,
    fund_nav: float,
    sleeve_budget: float,
    per_name: float,
    open_symbols: list[str],
    intents: list[EquityOrderIntent],
    actions: list[dict[str, Any]] | None = None,
    meta: dict[str, Any] | None = None,
    recommend_only: bool = True,
) -> dict[str, Any]:
    return {
        "strategy_id": STRATEGY_ID,
        "phase": phase,
        "recommend_only": recommend_only,
        "fund_nav_usd": round(fund_nav, 2),
        "sleeve_budget_usd": round(sleeve_budget, 2),
        "per_name_budget_usd": round(per_name, 2),
        "max_holdings": DEFAULT_DIP_MAX_HOLDINGS,
        "open_symbols": open_symbols,
        "open_count": len(open_symbols),
        "intents": [
            {
                "symbol": i.symbol,
                "action": i.action,
                "shares": i.shares,
                "notional_usd": i.notional_usd,
                "order_type": i.order_type,
                "limit_price": i.limit_price,
                "tif": i.tif,
                "reason": i.reason,
            }
            for i in intents
        ],
        "actions": actions or [],
        "meta": meta or {},
    }


def bootstrap_state_from_ib(
    portfolio: PortfolioSnapshot,
    existing: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Seed state for IB non-ETF longs missing from the ledger (manual fills)."""
    state = dict(existing or {"positions": {}})
    pos = dict(state.get("positions") or {})
    for leg in portfolio.positions:
        if leg.sec_type != "STK" or float(leg.position) <= 0.5:
            continue
        s = leg.symbol.upper()
        if s in CORE_BOOK_ETFS:
            continue
        if s in pos:
            continue
        shares = max(1, int(round(abs(float(leg.position)))))
        fill = abs(float(leg.market_value) / float(leg.position)) if leg.position else 0.0
        pos[s] = {
            "status": "open",
            "shares": shares,
            "fill_price": round(fill, 4),
            "atr_at_signal": 0.0,
            "entry_date": None,
            "signal_date": None,
            "source": "ib_bootstrap",
        }
    state["positions"] = pos
    return state
