"""Crypto execution orchestrator. Wires: target weights -> capacity caps -> edge-decay×Kelly gross scaler ->
target $ -> deltas vs live positions -> hard gates -> smart router -> maker->IOC place loop (rate-limited via
the adapter) -> TCA + reconciliation -> persist. Dry-run plans without placing. Halts on any gate or drift."""
from __future__ import annotations

import datetime as dt
import time
from dataclasses import dataclass

from fxhnt.adapters.exchange.binance_ccxt import client_id
from fxhnt.adapters.persistence.exec_store import ExecStore
from fxhnt.domain.execution.capacity import cap_weights
from fxhnt.domain.execution.reconciliation import diff_positions
from fxhnt.domain.execution.risk_gates import GateConfig, gross_scaler, pretrade_block
from fxhnt.domain.execution.smart_router import RouteInput, plan_orders
from fxhnt.domain.execution.tca import shortfall_bps
from fxhnt.ports.crypto_exchange import ChildOrder, CryptoExchange


@dataclass(frozen=True, slots=True)
class ExecConfig:
    testnet: bool
    allow_live: bool
    kill_switch: bool
    leverage: int
    gross_cap: float
    per_symbol_cap: float
    participation_rate: float
    depth_cap: float
    max_slippage_bps: float
    hysteresis: float
    kelly_cap: float
    maker_wait_s: float
    session_dd: float = -0.10
    recon_tol_frac: float = 0.02


@dataclass
class RebalancePlan:
    rebalance_id: str
    n_orders: int
    halted: bool
    block_reason: str | None
    drift: dict[str, float]
    rate_limited: bool = False


class CryptoExecutionService:
    def __init__(self, exchange: CryptoExchange, store: ExecStore, cfg: ExecConfig) -> None:
        self._x = exchange
        self._store = store
        self._cfg = cfg

    def rebalance(self, weights: dict[str, float], *, adv_usd: dict[str, float], edge_theta: float,
                  kelly_fraction: float, rebalance_id: str, execute: bool,
                  session_pnl_pct: float = 0.0) -> RebalancePlan:
        cfg = self._cfg
        block = pretrade_block(
            GateConfig(allow_live=cfg.allow_live, kill_switch=cfg.kill_switch, testnet=cfg.testnet,
                       gross_cap=cfg.gross_cap, session_dd=cfg.session_dd, kelly_cap=cfg.kelly_cap),
            session_pnl_pct=session_pnl_pct)
        if block and execute:
            return RebalancePlan(rebalance_id, 0, halted=True, block_reason=block, drift={})

        acct = self._x.account()
        filters = self._x.exchange_info()
        capped = cap_weights(weights, nlv=acct.nlv, adv_usd=adv_usd,
                             per_symbol_cap=cfg.per_symbol_cap, participation_rate=cfg.participation_rate)
        scale = gross_scaler(edge_theta=edge_theta, kelly_fraction=kelly_fraction,
                             kelly_cap=cfg.kelly_cap, gross_cap=cfg.gross_cap)

        # I5: θ=0 (EMA/Kelly warm-up or NaN-recovery) HOLDS, never flattens.
        # Deliberate exits use the kill-switch or crypto-flatten CLI — not a transient zero scaler.
        if scale <= 0.0:
            return RebalancePlan(rebalance_id, 0, halted=False,
                                 block_reason="zero_gross_scale_hold", drift={})

        # I4: capture the true decision-time mid per symbol for TCA (arrival_mid).
        # The order-book fetch already happens here for routing; store it now so the place loop
        # uses the ACTUAL bid/ask mid at decision time, not a blend of maker/abort prices.
        mids: dict[str, float] = {}
        inputs: list[RouteInput] = []
        for sym, w in capped.items():
            if sym not in filters:
                continue
            target_usd = w * scale * acct.nlv
            cur_qty = acct.positions.get(sym, 0.0)
            book = self._x.order_book(sym)
            mids[sym] = book.mid           # I4: authoritative arrival mid for TCA
            cur_usd = cur_qty * book.mid
            delta_usd = target_usd - cur_usd
            if abs(delta_usd) < cfg.hysteresis * acct.nlv:
                continue
            inputs.append(RouteInput(symbol=sym, delta_usd=delta_usd, current_qty=cur_qty, filt=filters[sym],
                                     book=book, adv_usd=adv_usd.get(sym, 0.0),
                                     participation_rate=cfg.participation_rate, depth_cap=cfg.depth_cap,
                                     max_slippage_bps=cfg.max_slippage_bps))

        plans = [p for p in plan_orders(inputs) if p.skip_reason is None]

        # I3: dry-run surfaces block_reason for monitoring even when not placing.
        if not execute:
            return RebalancePlan(rebalance_id, len(plans), halted=False, block_reason=block, drift={})

        self._x.ensure_one_way_mode()
        intended = dict(acct.positions)
        rate_limited = False
        for p in plans:
            self._x.set_leverage(p.symbol, cfg.leverage)
            # I4: use the true decision-time mid, NOT the maker/abort blend.
            arrival_mid = mids[p.symbol]
            # maker leg (post-only @ touch)
            mk = ChildOrder(p.symbol, p.side, p.qty, "LIMIT", p.price, p.reduce_only, True,
                            client_id(rebalance_id, p.symbol, "maker"), "GTX")
            f = self._x.place(mk)
            if f.status == "REJECTED":
                rate_limited = True
            if f.status != "FILLED":
                if cfg.maker_wait_s > 0:
                    time.sleep(cfg.maker_wait_s)
                self._x.cancel(p.symbol, mk.client_id)
                # C2: fetch AUTHORITATIVE post-cancel fill — the maker may have continued filling
                # during the sleep; using the stale place-response would size the IOC too large.
                f = self._x.order_status(p.symbol, mk.client_id)
            self._persist(rebalance_id, mk, f, arrival_mid)
            # C1: accumulate BOTH maker and IOC fills into `intended` so reconciliation sees the
            # full executed quantity and does not false-halt on the IOC leg.
            intended[p.symbol] = intended.get(p.symbol, 0.0) + (f.qty if p.side == "BUY" else -f.qty)
            remaining = round(p.qty - f.qty, 12)
            if remaining > 0:
                ioc = ChildOrder(p.symbol, p.side, remaining, "LIMIT", p.abort_price, p.reduce_only, False,
                                 client_id(rebalance_id, p.symbol, "ioc"), "IOC")
                f2 = self._x.place(ioc)
                if f2.status == "REJECTED":
                    rate_limited = True
                if f2.qty > 0:
                    self._persist(rebalance_id, ioc, f2, arrival_mid)
                    intended[p.symbol] = intended.get(p.symbol, 0.0) + (f2.qty if p.side == "BUY" else -f2.qty)

        actual = {pp.symbol: pp.qty for pp in self._x.positions()}
        drift = diff_positions(intended, actual, tol_frac=cfg.recon_tol_frac)
        return RebalancePlan(rebalance_id, len(plans), halted=bool(drift), block_reason=None, drift=drift,
                             rate_limited=rate_limited)

    def _persist(self, rebalance_id, order, fill, arrival_mid) -> None:
        self._store.record(client_id=fill.client_id, rebalance_id=rebalance_id, symbol=fill.symbol,
                           side=fill.side, qty=fill.qty, price=fill.price, fee=fill.fee, status=fill.status,
                           shortfall_bps=shortfall_bps(fill.side, arrival_mid=arrival_mid, fill_price=fill.price),
                           at=dt.datetime.now(dt.UTC).replace(tzinfo=None))
