"""IBKR live adapter for CrackingMarkets equity-dip sleeve (stock-only book).

Recommend-only by default. Sizes each name at ``sleeve / max_holdings`` and
skips new DAY limits when ≥ max open dips.
"""

from __future__ import annotations

import json
from pathlib import Path

from RenTech.live.ibkr_equity_orders import last_price, place_stock_order
from RenTech.live.protocols import RiskAction, StrategyContext, StrategyCycleReport
from RenTech.live.stock_book_cm_dip_live import (
    STRATEGY_ID,
    actions_to_entry_intents,
    bootstrap_state_from_ib,
    build_entry_actions,
    evaluate_exits,
    in_hhmm_window,
    load_dip_state,
    mark_exits_in_state,
    open_dip_market_value_usd,
    open_dip_symbols,
    recommendation_payload,
    record_entries_in_state,
    save_dip_state,
    sleeve_nav_usd,
)
from RenTech.live.stock_book_utils import (
    DEFAULT_DIP_MAX_HOLDINGS,
    STOCK_ONLY_WEIGHTS,
    dip_per_name_budget_usd,
)


class StockBookIbkrStrategy:
    strategy_id = STRATEGY_ID

    async def audit(self, ctx: StrategyContext) -> StrategyCycleReport:
        scfg = ctx.strategy_config
        state_path = Path(str(scfg.get("state_path", "RenTech/data/live_state/cm_dip_positions.json")))
        if not state_path.is_absolute():
            state_path = (ctx.repo_root / state_path).resolve()
        state = load_dip_state(state_path)
        open_syms = sorted(open_dip_symbols(state, ctx.portfolio, sync_ib_non_etf=True))
        return StrategyCycleReport(
            strategy_id=self.strategy_id,
            ok=True,
            messages=[f"open_dips={len(open_syms)}: {', '.join(open_syms) or 'none'}"],
            metadata={"open_symbols": open_syms, "state_path": str(state_path)},
        )

    async def run_cycle(self, ctx: StrategyContext) -> StrategyCycleReport:
        report = StrategyCycleReport(strategy_id=self.strategy_id, ok=True)
        scfg = ctx.strategy_config

        fund_scale = float(scfg.get("fund_scale", 1.5))
        dip_w = float(scfg.get("equity_dip_weight", STOCK_ONLY_WEIGHTS.get("equity_dip", 0.17)))
        max_holdings = int(scfg.get("max_holdings", ctx.max_open_positions or DEFAULT_DIP_MAX_HOLDINGS))
        profit_atr_mult = float(scfg.get("profit_atr_mult", 0.5))
        limit_atr_mult = float(scfg.get("limit_atr_mult", 0.9))
        hold_days = int(scfg.get("hold_trading_days", 10))
        entry_after = str(scfg.get("entry_after_et", "09:35"))
        entry_before = str(scfg.get("entry_before_et", "15:30"))
        exit_after = str(scfg.get("exit_after_et", "15:50"))
        exit_before = str(scfg.get("exit_before_et", "16:00"))
        bootstrap = bool(scfg.get("bootstrap_from_ib", True))

        state_path = Path(str(scfg.get("state_path", "RenTech/data/live_state/cm_dip_positions.json")))
        if not state_path.is_absolute():
            state_path = (ctx.repo_root / state_path).resolve()
        rec_out = Path(
            str(scfg.get("recommend_out", "RenTech/data/logs/stock_book_cm_dip_recommendation.json"))
        )
        if not rec_out.is_absolute():
            rec_out = (ctx.repo_root / rec_out).resolve()

        if ctx.platform_risk.action == RiskAction.HALT_ALL:
            report.messages.append("platform halt")
            return report

        state = load_dip_state(state_path)
        if bootstrap:
            state = bootstrap_state_from_ib(ctx.portfolio, state)

        open_syms = open_dip_symbols(state, ctx.portfolio, sync_ib_non_etf=True)
        open_mv = open_dip_market_value_usd(ctx.portfolio, open_syms)
        sleeve = sleeve_nav_usd(ctx.portfolio, fund_scale=fund_scale, equity_dip_weight=dip_w)
        per_name = dip_per_name_budget_usd(
            float(ctx.portfolio.net_liquidation_usd),
            dip_w,
            fund_scale,
            max_holdings=max_holdings,
        )
        rec_only = ctx.recommend_only

        # --- Exits first (near close) ---
        if ctx.force_entry_now or in_hhmm_window(ctx.now_et, exit_after, exit_before):
            prices: dict[str, float] = {}
            for sym in open_syms:
                try:
                    prices[sym] = await last_price(ctx.ib, sym)
                except Exception as exc:
                    report.messages.append(f"price {sym}: {exc}")
            exit_intents = evaluate_exits(
                state,
                ctx.portfolio,
                today=ctx.today,
                profit_atr_mult=profit_atr_mult,
                hold_trading_days=hold_days,
                last_prices=prices,
            )
            payload = recommendation_payload(
                phase="exit_moc",
                fund_nav=float(ctx.portfolio.net_liquidation_usd),
                sleeve_budget=sleeve,
                per_name=per_name,
                open_symbols=sorted(open_syms),
                intents=exit_intents,
                meta={"prices": prices, "open_mv_usd": round(open_mv, 2)},
                recommend_only=rec_only,
            )
            rec_out.parent.mkdir(parents=True, exist_ok=True)
            rec_out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
            if exit_intents:
                if not rec_only:
                    for intent in exit_intents:
                        await place_stock_order(ctx.ib, intent, recommend_only=False)
                        report.exits_attempted += 1
                    state = mark_exits_in_state(state, [i.symbol for i in exit_intents])
                    save_dip_state(state_path, state)
                report.messages.append(f"exit_moc n={len(exit_intents)}")
            else:
                report.messages.append("exit_window: no CM exits")
            if not ctx.force_entry_now and in_hhmm_window(ctx.now_et, exit_after, exit_before):
                report.metadata = payload
                return report

        # --- Entries (DAY limits) ---
        # recommend_only sets allow_new_entries=False at the platform gate; still emit tickets.
        if not ctx.recommend_only and not ctx.strategy_risk.allow_new_entries:
            report.messages.append("strategy risk blocks entries")
            report.metadata = {"open_symbols": sorted(open_syms), "open_mv_usd": open_mv}
            return report
        if not (
            ctx.force_entry_now
            or in_hhmm_window(ctx.now_et, entry_after, entry_before)
        ):
            report.messages.append("outside entry window")
            report.metadata = {"open_symbols": sorted(open_syms), "open_mv_usd": open_mv}
            return report

        over_count = len(open_syms) >= max_holdings
        over_budget = open_mv >= sleeve * 0.95  # leave a little headroom
        if over_count or over_budget:
            reason = (
                f"full {len(open_syms)}/{max_holdings}"
                if over_count
                else f"over budget MV ${open_mv:,.0f} >= sleeve ${sleeve:,.0f}"
            )
            payload = recommendation_payload(
                phase="entry_skip_full",
                fund_nav=float(ctx.portfolio.net_liquidation_usd),
                sleeve_budget=sleeve,
                per_name=per_name,
                open_symbols=sorted(open_syms),
                intents=[],
                meta={"reason": reason, "open_mv_usd": round(open_mv, 2)},
                recommend_only=rec_only,
            )
            rec_out.parent.mkdir(parents=True, exist_ok=True)
            rec_out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
            report.messages.append(f"skip entries — {reason}")
            report.metadata = payload
            return report

        actions, meta = build_entry_actions(
            fund_nav=float(ctx.portfolio.net_liquidation_usd),
            fund_scale=fund_scale,
            equity_dip_weight=dip_w,
            max_holdings=max_holdings,
            open_symbols=open_syms,
            limit_atr_mult=limit_atr_mult,
            profit_atr_mult=profit_atr_mult,
        )
        intents = actions_to_entry_intents(actions)
        payload = recommendation_payload(
            phase="entry_lmt",
            fund_nav=float(ctx.portfolio.net_liquidation_usd),
            sleeve_budget=sleeve,
            per_name=per_name,
            open_symbols=sorted(open_syms),
            intents=intents,
            actions=actions,
            meta={**meta, "open_mv_usd": round(open_mv, 2)},
            recommend_only=rec_only,
        )
        rec_out.parent.mkdir(parents=True, exist_ok=True)
        rec_out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")

        if intents and not rec_only:
            for intent in intents:
                ticket = await place_stock_order(ctx.ib, intent, recommend_only=False)
                report.entries_attempted += 1
                report.messages.append(
                    f"LMT {intent.symbol} x{intent.shares} @ {intent.limit_price}: {ticket.get('status')}"
                )
            state = record_entries_in_state(state, intents, actions, status="pending_entry")
            save_dip_state(state_path, state)
        elif intents:
            state = record_entries_in_state(state, intents, actions, status="recommended")
            save_dip_state(state_path, state)
            report.messages.append(f"recommend entry_lmt n={len(intents)} @ ~${per_name:,.0f}/name")
        else:
            summaries = [a.get("summary", "") for a in actions[:3]]
            report.messages.append("no new dip entries: " + ("; ".join(summaries) or "none"))

        if bootstrap:
            save_dip_state(state_path, state)

        report.metadata = payload
        return report
