"""Esecuzione REALE su Deribit (testnet) con verifica post-ordine e fee reali.

Flusso per ogni ordine:
  1. converte il notional (USD) in `amount` Deribit, arrotondato allo step del
     contratto (BTC-PERPETUAL step $10, ETH-PERPETUAL step $1) e clampato al minimo;
  2. piazza un market order REALE via Cerbero → Deribit private/buy|sell;
  3. RIVERIFICA su Deribit: rilegge get_positions (la posizione esiste con la size
     giusta?) e get_trade_history (ritrova il fill per order_id) — non si fida della
     sola risposta dell'ordine;
  4. estrae la FEE REALE dai trades[] del fill (per i perp inverse la fee e' nel
     coin di settlement: BTC/ETH → la convertiamo anche in USD col prezzo di fill).

NB perp inverse Deribit: `amount` e la dimensione posizione sono in USD notional;
la fee dei trade e' denominata nel base-coin (BTC/ETH).
"""
from __future__ import annotations

import time
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any

from src.live.cerbero_client import CerberoClient

# Specifiche contratto (verificate da test.deribit.com/public/get_instrument).
# INVERSE (reversed): amount in USD, step in USD (es. BTC $10, ETH $1).
# LINEAR (USDC): amount nel base-coin, step nel base-coin (BTC 0.0001, ETH 0.001);
#   il notional USD si converte col prezzo. Fee/settle in USDC.
_CONTRACT: dict[str, dict[str, Any]] = {
    "BTC-PERPETUAL":      {"linear": False, "min": 10.0,   "step": 10.0,   "tick": 0.5},
    "ETH-PERPETUAL":      {"linear": False, "min": 1.0,    "step": 1.0,    "tick": 0.05},
    "BTC_USDC-PERPETUAL": {"linear": True,  "min": 0.0001, "step": 0.0001, "tick": 0.5,  "settle": "USDC"},
    "ETH_USDC-PERPETUAL": {"linear": True,  "min": 0.001,  "step": 0.001,  "tick": 0.05, "settle": "USDC"},
}


def contract_spec(instrument: str) -> dict[str, Any]:
    return _CONTRACT.get(instrument, {"linear": False, "min": 1.0, "step": 1.0})


def settlement_currency(instrument: str) -> str:
    """Inverse → base-coin (BTC/ETH); lineari USDC → USDC. Usato per get_positions
    e per denominare la fee."""
    spec = contract_spec(instrument)
    if spec.get("settle"):
        return spec["settle"]
    return instrument.split("-")[0].split("_")[0]


def _quantize_step(value: float, step: float, mn: float) -> float:
    """Arrotonda `value` al multiplo di `step` SENZA artefatti float
    (es. 72*0.001 = 0.07200000000000001 → Deribit 'Invalid params')."""
    n_steps = round(value / step)
    return float(max(n_steps * Decimal(str(step)), Decimal(str(mn))))


def quantize_price(instrument: str, price: float) -> float:
    """Arrotonda il prezzo al tick dello strumento (Decimal, niente artefatti
    float) — richiesto per gli ordini limit (Deribit rifiuta prezzi off-tick)."""
    tick = contract_spec(instrument).get("tick")
    if not tick or price <= 0:
        return price
    return float(round(price / tick) * Decimal(str(tick)))


def notional_to_amount(instrument: str, notional_usd: float,
                       price: float | None = None) -> float:
    """Notional USD → `amount` Deribit, arrotondato allo step e clampato al minimo.
    Inverse: amount in USD (step USD). Lineari USDC: amount in base-coin (serve il
    `price` per convertire). Ritorna 0.0 se sotto mezzo step (niente ordine)."""
    spec = contract_spec(instrument)
    step, mn = spec["step"], spec["min"]
    if spec.get("linear"):
        if not price:
            return 0.0
        units = notional_usd / price          # base-coin richiesti
        if units < step / 2:
            return 0.0
        return _quantize_step(units, step, mn)
    if notional_usd < step / 2:
        return 0.0
    return _quantize_step(notional_usd, step, mn)


@dataclass
class Fill:
    """Esito verificato di un ordine reale."""
    instrument: str
    side: str                    # "buy" | "sell"
    requested_notional: float    # USD chiesti dalla strategia
    amount: float                # USD effettivi (arrotondati allo step)
    fill_price: float | None     # prezzo medio di esecuzione (da Deribit)
    fee_coin: float              # fee reale nel coin di settlement (BTC/ETH)
    fee_usd: float               # fee reale convertita in USD (fee_coin * fill_price)
    order_id: str | None
    order_state: str | None      # "filled" atteso per market
    verified: bool               # posizione/trade riscontrati su Deribit
    raw: dict[str, Any] = field(default_factory=dict)
    notes: str = ""


def _avg_fill_price(order: dict, trades: list[dict]) -> float | None:
    p = order.get("average_price")
    if p:
        return float(p)
    # fallback: media pesata per amount dai trade
    tot_amt = sum(float(t.get("amount", 0) or 0) for t in trades)
    if tot_amt > 0:
        return sum(float(t.get("price", 0) or 0) * float(t.get("amount", 0) or 0)
                   for t in trades) / tot_amt
    return None


@dataclass
class ExecutionClient:
    """Wrapper d'esecuzione reale sopra CerberoClient. ogni open/close ritorna un
    Fill VERIFICATO (o verified=False con la ragione in notes)."""
    client: CerberoClient = field(default_factory=CerberoClient)
    verify_polls: int = 4        # tentativi di riverifica
    verify_sleep: float = 0.6    # attesa fra i poll (s)
    # Disaster-bracket on-book (2026-06-07): distanza % dello STOP_MARKET reduce-only
    # piazzato a ogni REAL_OPEN come assicurazione per gli outage del feed/runner
    # (poll-loop fermo = posizione reale senza valutazione exit). None = disattivo.
    # Configurato dal runner da overrides.execution.disaster_sl_pct.
    disaster_sl_pct: float | None = None
    # NB leva: su Deribit la leva per-strumento NON e' impostabile (private/set_leverage
    # risponde 400 Bad Request — verificato 2026-06-03 nei log Cerbero; il set_leverage
    # di Cerbero fallisce sempre, soppresso). Il campo "leverage: 50" in get_positions
    # e' il MASSIMO dello strumento (informativo): l'esposizione reale la decide la SIZE
    # dell'ordine, non una leva account. Percio' qui NON si passa leverage per-ordine.

    # --- helper di verifica ---

    def _position_size(self, instrument: str) -> float:
        """Size assoluta (USD) della posizione aperta sull'instrument, 0 se flat."""
        cur = settlement_currency(instrument)
        try:
            for p in self.client.get_positions(currency=cur):
                if p.get("instrument") == instrument:
                    return abs(float(p.get("size", 0) or 0))
        except Exception:
            pass
        return 0.0

    def _trade_by_order(self, instrument: str, order_id: str | None) -> dict | None:
        """Ritrova il fill nel trade history per order_id (fonte autorevole fee)."""
        if not order_id:
            return None
        try:
            for t in self.client.get_trade_history(limit=50, instrument_name=instrument):
                if str(t.get("order_id")) == str(order_id):
                    return t
        except Exception:
            pass
        return None

    # --- API ---

    def _mark_price(self, instrument: str) -> float | None:
        try:
            t = self.client.get_ticker(instrument)
            return float(t.get("mark_price") or t.get("last_price") or 0) or None
        except Exception:
            return None

    def amount_for(self, instrument: str, notional_usd: float) -> float:
        """Notional USD → amount Deribit (gestisce inverse/lineare, prezzo per i lineari)."""
        spec = contract_spec(instrument)
        price = self._mark_price(instrument) if spec.get("linear") else None
        return notional_to_amount(instrument, notional_usd, price=price)

    def _submit(self, instrument: str, side: str, amount: float,
                requested_notional: float, reduce_only: bool,
                label: str | None, order_type: str = "market",
                price: float | None = None) -> Fill:
        """Ordine REALE (market o limit resting) + parsing del fill. Verifica
        per-worker basata sul TRADE (order_id/trades), non sulla size netta — lo
        strumento e' condiviso fra piu' worker e la posizione su Deribit e'
        aggregata per conto. Per i limit la verifica e' l'ACCETTAZIONE in book
        (state 'open', o 'filled' se crossa subito)."""
        spec = contract_spec(instrument)
        if amount <= 0:
            return Fill(instrument, side, requested_notional, 0.0, None, 0.0, 0.0,
                        None, None, False, notes="notional sotto il minimo contratto")

        resp = self.client.place_order(instrument, side, amount, order_type=order_type,
                                       price=price, label=label, reduce_only=reduce_only)
        if not isinstance(resp, dict) or resp.get("state") == "error" or "error" in resp:
            return Fill(instrument, side, requested_notional, amount, None, 0.0, 0.0,
                        None, "error", False, raw=resp if isinstance(resp, dict) else {},
                        notes=f"place_order error: {resp}")

        order = resp.get("order", resp) or {}
        trades = resp.get("trades", []) or []
        order_id = order.get("order_id")
        state = order.get("order_state")
        fill_price = _avg_fill_price(order, trades)

        # fee reale dai trade del fill (coin di settlement)
        fee_coin = sum(float(t.get("fee", 0) or 0) for t in trades)
        # riconciliazione su trade history per order_id (fonte autorevole) —
        # inutile per un limit ancora in book senza fill
        th = None
        if order_type == "market" or trades:
            th = self._trade_by_order(instrument, order_id)
        if fee_coin == 0 and th and th.get("fee") is not None:
            fee_coin = float(th["fee"])
        if fill_price is None and th:
            fill_price = float(th.get("price") or 0) or None
        # lineari USDC: fee gia' in USDC; inverse: nel base-coin → * prezzo
        fee_usd = fee_coin if spec.get("linear") else (
            fee_coin * fill_price if (fee_coin and fill_price) else 0.0)

        # VERIFICA: market = ordine filled E fill riscontrato (trades o history);
        # limit = accettato in book ('open') o gia' eseguito ('filled');
        # stop_market = trigger accettato ('untriggered' finche' il mark non tocca)
        if order_type == "market":
            verified = (state == "filled") and (bool(trades) or th is not None)
        elif order_type == "stop_market":
            verified = state in ("untriggered", "open", "filled")
        else:
            verified = state in ("open", "filled")
        return Fill(instrument, side, requested_notional, amount, fill_price,
                    fee_coin, fee_usd, order_id, state, verified, raw=resp,
                    notes="" if verified else f"fill non verificato (state={state}, trades={len(trades)})")

    def open(self, instrument: str, side: str, notional_usd: float,
             label: str | None = None) -> Fill:
        """Apre la quota del worker (market, NON reduce_only)."""
        amount = self.amount_for(instrument, notional_usd)
        return self._submit(instrument, side, amount, notional_usd,
                            reduce_only=False, label=label)

    def close_amount(self, instrument: str, entry_side: str, amount: float,
                     label: str | None = None) -> Fill:
        """Chiude SOLO la quota del worker: market reduce_only di lato opposto,
        stesso `amount` dell'apertura. Non usa close_position (flatterebbe anche
        le quote degli altri worker sullo stesso strumento)."""
        spec = contract_spec(instrument)
        # quantizza difensivamente: il chiamante puo' passare un residuo
        # (amount aperto − fill parziale del TP) con artefatti float
        amount = _quantize_step(amount, spec["step"], spec["min"]) if amount > 0 else 0.0
        opp = "sell" if entry_side == "buy" else "buy"
        return self._submit(instrument, opp, amount, 0.0,
                            reduce_only=True, label=label)

    def place_tp_limit(self, instrument: str, entry_side: str, amount: float,
                       tp_price: float, label: str | None = None) -> Fill:
        """LIMIT reduce-only appoggiato al livello TP (lato opposto all'entrata):
        il fill reale replica l'assunzione intrabar del backtest (exit AL livello)
        invece del market-on-poll post-rimbalzo (+235 bps misurati il 2026-06-04).
        Copre la SOLA quota del worker (stesso `amount` dell'apertura) — lo
        strumento e' condiviso fra piu' worker. NB: se il prezzo e' gia' oltre il
        TP il limit crossa e filla subito (state 'filled'); la riconciliazione a
        valle (resting_fills) lo gestisce come un fill normale."""
        opp = "sell" if entry_side == "buy" else "buy"
        px = quantize_price(instrument, tp_price)
        if amount <= 0 or px <= 0:
            return Fill(instrument, opp, 0.0, amount, None, 0.0, 0.0,
                        None, None, False, notes="amount/tp non validi")
        return self._submit(instrument, opp, amount, 0.0, reduce_only=True,
                            label=label, order_type="limit", price=px)

    def place_disaster_sl(self, instrument: str, entry_side: str, amount: float,
                          stop_price: float, label: str | None = None) -> Fill:
        """STOP_MARKET reduce-only LONTANO (disaster bracket ~-30%): assicurazione
        on-book per gli outage — in operativita' normale non scatta mai (lo SL
        della strategia esce molto prima, market-on-poll) -> 0 costo Sharpe.
        Trigger sul MARK price; copre la SOLA quota del worker. Nei crash il fill
        e' al gap, non al livello: cappa la coda, non la elimina (exit-lab).
        NB: il set_stop_loss di cerbero_client e' un private/edit Deribit (solo
        ordini APERTI) -> inutilizzabile su una posizione gia' fillata; il
        bracket si piazza come ordine trigger autonomo."""
        opp = "sell" if entry_side == "buy" else "buy"
        px = quantize_price(instrument, stop_price)
        if amount <= 0 or px <= 0:
            return Fill(instrument, opp, 0.0, amount, None, 0.0, 0.0,
                        None, None, False, notes="amount/stop non validi")
        return self._submit(instrument, opp, amount, 0.0, reduce_only=True,
                            label=label, order_type="stop_market", price=px)

    def cancel_order(self, order_id: str) -> dict:
        """Cancella un ordine resting. {'state': 'cancelled'} su successo;
        'error' se l'ordine non e' piu' open (es. gia' fillato) — NON fatale:
        il chiamante riconcilia i fill via resting_fills (trade history)."""
        if not order_id:
            return {"state": "error", "error": "no order_id"}
        try:
            return self.client.cancel_order(order_id)
        except Exception as exc:
            return {"state": "error", "error": str(exc)}

    def resting_fills(self, instrument: str,
                      order_id: str) -> tuple[float, float | None, float]:
        """Fill (anche parziali) di un ordine resting, riconciliati dal trade
        history per order_id (fonte autorevole, come per i market). Ritorna
        (amount_fillato, prezzo_medio, fee_usd)."""
        if not order_id:
            return 0.0, None, 0.0
        spec = contract_spec(instrument)
        try:
            trades = [t for t in self.client.get_trade_history(
                          limit=100, instrument_name=instrument)
                      if str(t.get("order_id")) == str(order_id)]
        except Exception:
            return 0.0, None, 0.0
        amt = sum(float(t.get("amount", 0) or 0) for t in trades)
        if amt <= 0:
            return 0.0, None, 0.0
        avg = sum(float(t.get("price", 0) or 0) * float(t.get("amount", 0) or 0)
                  for t in trades) / amt
        fee_coin = sum(float(t.get("fee", 0) or 0) for t in trades)
        fee_usd = fee_coin if spec.get("linear") else (fee_coin * avg if avg else 0.0)
        return amt, avg or None, fee_usd

    def close(self, instrument: str, label: str | None = None) -> Fill:
        """Chiude a mercato la posizione e riverifica che il conto sia flat,
        leggendo la fee di chiusura dal trade history."""
        side = "close"
        resp = self.client.close_position(instrument)
        if not isinstance(resp, dict) or resp.get("state") == "error" or "error" in resp:
            return Fill(instrument, side, 0.0, 0.0, None, 0.0, 0.0, None, "error",
                        False, raw=resp if isinstance(resp, dict) else {},
                        notes=f"close error: {resp}")
        order_id = resp.get("order_id")

        # fee/prezzo di chiusura dal trade history (close_position non li ritorna)
        th = self._trade_by_order(instrument, order_id)
        fee_coin = float(th["fee"]) if th and th.get("fee") is not None else 0.0
        fill_price = float(th.get("price")) if th and th.get("price") else None
        if contract_spec(instrument).get("linear"):
            fee_usd = fee_coin
        else:
            fee_usd = fee_coin * fill_price if (fee_coin and fill_price) else 0.0

        # verifica: la posizione deve essere tornata flat
        pos = 1.0
        for _ in range(self.verify_polls):
            pos = self._position_size(instrument)
            if pos == 0:
                break
            time.sleep(self.verify_sleep)
        verified = pos == 0

        return Fill(instrument, side, 0.0, 0.0, fill_price, fee_coin, fee_usd,
                    order_id, resp.get("state"), verified, raw=resp,
                    notes="" if verified else f"posizione non flat dopo close (pos={pos})")
