"""Execution use-case — runs a multi-strategy Book live: net all sleeves into per-market targets,
reconcile against the broker, and place whole-share orders through the gates. Depends only on ports
(DataProvider, Broker), injected by the caller. The safety gates are hard-won from live paper trading.
"""
from __future__ import annotations

from pydantic import BaseModel, Field

from fxhnt.config import Settings
from fxhnt.domain.portfolio import Book, compute_target_weights
from fxhnt.ports.broker import Broker, Order
from fxhnt.ports.data import DataProvider


class PlannedOrder(BaseModel):
    symbol: str
    side: str
    quantity: float
    est_value: float


class RebalancePlan(BaseModel):
    book: str
    nlv: float
    target_weights: dict[str, float] = Field(default_factory=dict)
    orders: list[PlannedOrder] = Field(default_factory=list)
    executed: bool = False
    blocked: bool = False
    notes: list[str] = Field(default_factory=list)


class ExecutionService:
    def __init__(self, data: DataProvider, broker: Broker, settings: Settings) -> None:
        self._data = data
        self._broker = broker
        self._s = settings

    def rebalance(self, book: Book, *, execute: bool = False) -> RebalancePlan:
        ex = self._s.execution
        notes: list[str] = []

        prices = {m: self._data.fetch(m) for m in book.markets()}
        targets = compute_target_weights(book, prices)  # {Market: signed weight}
        acct = self._broker.account_state()

        # data-consistency sanity (the IBKR-paper glitch lesson): cross-check NLV vs cash+positions,
        # size on the CONSERVATIVE value, hard-block if they disagree badly.
        nlv = acct.nlv
        data_gap = 0.0
        if acct.gross_position_value is not None:
            est = acct.cash + acct.gross_position_value
            data_gap = abs(acct.nlv - est) / max(acct.nlv, est, 1.0)
            if acct.nlv > 0 and est > 0:
                nlv = min(acct.nlv, est)
            if data_gap > 0.02:
                notes.append(f"account data inconsistent (gap {100*data_gap:.1f}%) — using conservative NLV")

        plan = RebalancePlan(book=book.name, nlv=nlv,
                             target_weights={m.symbol: round(w, 4) for m, w in targets.items()})

        # --- gates (refuse to act on unsafe state) ---
        if not acct.is_paper and not ex.allow_live:
            plan.blocked = True
            plan.notes = [*notes, "LIVE account but allow_live not set — refusing real money"]
            return plan
        if data_gap > ex.data_tol:
            plan.blocked = True
            plan.notes = [*notes, f"account data gap {100*data_gap:.0f}% > {100*ex.data_tol:.0f}% — broker data unreliable, refusing"]
            return plan
        gross = sum(abs(w) for w in targets.values())
        if gross > book.max_gross_leverage + 1e-9:
            plan.blocked = True
            plan.notes = [*notes, f"gross exposure {gross:.2f} > book max {book.max_gross_leverage}"]
            return plan
        if nlv <= 0:
            plan.blocked = True
            plan.notes = [*notes, "NLV <= 0"]
            return plan

        # --- orders: whole shares, entry-floor on first entry, hysteresis on rebalances ---
        orders: list[PlannedOrder] = []
        for market, weight in targets.items():
            px = float(prices[market].close[-1])
            target_shares = nlv * weight / px
            current = acct.positions.get(market.symbol, 0.0)
            delta = target_shares - current
            threshold = (ex.entry_floor if current == 0 else ex.hysteresis) * nlv
            if abs(delta * px) <= threshold:
                continue
            qty = int(round(abs(delta)))
            if qty == 0:
                continue
            orders.append(PlannedOrder(symbol=market.symbol, side="BUY" if delta > 0 else "SELL",
                                       quantity=qty, est_value=round(qty * px)))
        plan.orders = orders

        if not orders:
            notes.append("in band — nothing to do")
        elif execute:
            placed = 0
            for o in orders:
                try:
                    status = self._broker.place_order(Order(symbol=o.symbol, side=o.side, quantity=o.quantity))
                    notes.append(f"{o.side} {o.quantity:g} {o.symbol}: {status}")
                    placed += 1
                except Exception as e:  # one bad order must not abort the rest
                    notes.append(f"order failed {o.symbol}: {str(e)[:80]}")
            plan.executed = placed > 0
        plan.notes = notes
        return plan
