"""
APEX V16 — Broker abstract interface.

Defines the contract that any broker adapter must implement.
TopstepX adapter (existing, reused from V15) will be wrapped/extended
to implement this interface in a follow-up step.

Why an interface here even though we only have one broker today:
  1. Enables MockBroker for unit/integration tests without API keys
  2. Future-proofs if we add a second broker
  3. Forces explicit thinking about what the orchestrator actually
     needs from "the broker" (read-only ops vs order ops)
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from typing import Optional


# ============================================================
# RESULT TYPES — broker calls return structured results
# ============================================================

@dataclass
class OrderResult:
    """Result of placing a market order with bracket (SL+TP)."""
    success: bool
    entry_price: Optional[float] = None
    sl_price: Optional[float] = None
    tp_price: Optional[float] = None
    entry_id: Optional[str] = None
    stop_id: Optional[str] = None
    target_id: Optional[str] = None
    error: Optional[str] = None     # set on failure
    raw: Optional[dict] = None      # raw broker response for debugging


@dataclass
class Position:
    """Broker-side position (live snapshot)."""
    symbol: str
    direction: str          # "BUY" or "SELL"
    contracts: int
    avg_price: float
    raw: Optional[dict] = None


@dataclass
class CancelResult:
    """Result of canceling an order."""
    success: bool
    order_id: Optional[str] = None
    error: Optional[str] = None


@dataclass(frozen=True)
class Order:
    """
    Pending order on the broker side (read-only snapshot).

    Used by Reconciler to detect naked SL/TP orphans (V15-BUG-9).
    `kind` is normalized across brokers: TopstepX type=1 -> "LIMIT",
    type=4 -> "STOP", type=2 -> "MARKET", anything else -> "OTHER".
    """
    order_id: str
    symbol: str
    kind: str           # "STOP" | "LIMIT" | "MARKET" | "OTHER"
    price: float
    contracts: int


@dataclass(frozen=True)
class ClosedTrade:
    """
    Trade that closed broker-side (read-only snapshot).

    Used by Reconciler case (ii) — when state thinks a trade is open
    but broker has no position, fetch recent_trades and recover P&L
    so RiskManager counters (daily_pnl, consecutive_sl_count) remain
    consistent across crashes.

    `closed_at` is ISO-8601 string from the broker (no parsing in V16).
    """
    trade_id: str
    symbol: str
    contracts: int
    side: str           # "BUY" | "SELL"
    exit_price: float
    pnl_usd: float
    closed_at: str


# ============================================================
# READ-ERROR EXCEPTION
# ============================================================

class BrokerReadError(Exception):
    """
    Raised when a broker READ operation cannot determine the true state
    of the broker side (positions, orders, balance) due to transport,
    auth, or account-resolution failure.

    Critical distinction from "empty result": a [] return means the broker
    confirmed there are no positions; raising BrokerReadError means we
    DON'T KNOW. Callers that gate destructive actions (e.g.,
    trade_opener._open_live's pre-check before place_market_bracket)
    MUST treat read failure as 'unknown', NOT as 'flat' — silent []
    on transport failure caused the 28 apr 6J duplicate-bracket incident.
    """
    pass


# ============================================================
# BROKER INTERFACE
# ============================================================

class BrokerBase(ABC):
    """
    Abstract broker. All methods are async (orchestrator runs async loop).

    Implementations should:
      - Wrap any SDK exceptions into structured results (no raw exceptions
        leaking to orchestrator)
      - Use rate limiting / retries internally where appropriate
      - Be idempotent where possible (e.g., position queries)
    """

    name: str = "BASE"   # subclasses override: "TopstepX", "Mock", etc.

    # ============================================================
    # CONNECTION
    # ============================================================

    @abstractmethod
    async def connect(self) -> bool:
        """Establish session/auth with broker. Returns True on success."""
        raise NotImplementedError

    @abstractmethod
    async def disconnect(self) -> None:
        """Clean shutdown of broker session."""
        raise NotImplementedError

    @abstractmethod
    async def is_connected(self) -> bool:
        """Health check."""
        raise NotImplementedError

    # ============================================================
    # MARKET DATA
    # ============================================================

    @abstractmethod
    async def get_last_price(self, symbol: str) -> Optional[float]:
        """Last traded price for symbol. None if unavailable."""
        raise NotImplementedError

    # ============================================================
    # POSITIONS (read-only)
    # ============================================================

    @abstractmethod
    async def positions_get(self, symbol: Optional[str] = None) -> list[Position]:
        """
        Get current positions. If symbol is None, return all positions.
        Returns empty list if no positions.
        """
        raise NotImplementedError

    @abstractmethod
    async def pending_orders(self, symbol: Optional[str] = None) -> list[Order]:
        """
        Get pending orders (open SL/TP/limit/etc.) on the broker.
        If symbol is None, return all pending orders.
        Returns empty list if no pending orders.

        Used by Reconciler to detect naked SL/TP orders left behind
        after a position closed broker-side (V15-BUG-9).
        """
        raise NotImplementedError

    @abstractmethod
    async def recent_trades(
        self,
        symbol: Optional[str] = None,
        since: Optional[datetime] = None,
        limit: int = 50,
    ) -> list[ClosedTrade]:
        """
        Get recent closed trades from broker-side history.
        If symbol is None, return trades for all symbols.
        If since is None, broker chooses a sensible default (e.g. 24h).

        Used by Reconciler case (ii) to recover P&L of a trade that
        closed broker-side while bot was down, so daily counters stay
        consistent across crashes.
        """
        raise NotImplementedError

    async def get_daily_rpnl(
        self,
        *,
        since: Optional[datetime] = None,
        limit: int = 500,
    ) -> Optional[float]:
        """
        Realized P&L since `since`, summed across every symbol — the
        broker-side aggregate that mirrors what TopstepX displays as
        "RP&L" in the platform UI.

        Default implementation reuses `recent_trades`: ClosedTrade is
        already filtered to closed legs only (open fills have pnl=0
        and are dropped by the impl), so a plain sum is correct.

        Returns None on failure (caller should fall back to its
        in-memory counter rather than report a zero). Subclasses can
        override if the broker exposes a cheaper aggregate endpoint.
        """
        try:
            trades = await self.recent_trades(symbol=None, since=since, limit=limit)
        except Exception:
            return None
        try:
            return float(sum(float(t.pnl_usd or 0.0) for t in trades))
        except (TypeError, ValueError):
            return None

    # ============================================================
    # ORDERS (write)
    # ============================================================

    @abstractmethod
    async def place_market_bracket(
        self,
        symbol: str,
        direction: str,
        contracts: int,
        sl_price: float,
        tp_price: float,
        *,
        sl_absolute_price: Optional[float] = None,
        sl_ticks: Optional[int] = None,
        tp_ticks: Optional[int] = None,
        sl_source: str = "ATR",
    ) -> OrderResult:
        """
        Place a market order with attached SL and TP (bracket order).

        Implementations should ensure SL/TP are attached to the broker
        after the entry fills (V15 used a 4-step REST flow for this:
        market order -> poll fill -> attach SL -> attach TP).

        Returns:
            OrderResult.success=True with all IDs populated on success,
            OrderResult.success=False with error message on failure.
        """
        raise NotImplementedError

    @abstractmethod
    async def place_stop_order(
        self,
        symbol: str,
        side: str,
        contracts: int,
        stop_price: float,
    ) -> OrderResult:
        """
        Place a standalone STOP order (used to re-attach SL after a
        bracket-place failure recovers an orphan position).

        `side` is the protective side (opposite of the entry: BUY entry -> SELL stop).
        OrderResult.entry_id holds the new stop order id on success.
        """
        raise NotImplementedError

    @abstractmethod
    async def place_limit_order(
        self,
        symbol: str,
        side: str,
        contracts: int,
        limit_price: float,
    ) -> OrderResult:
        """
        Place a standalone LIMIT order (used to re-attach TP after a
        bracket-place failure recovers an orphan position).

        `side` is the protective side. OrderResult.entry_id holds the
        new target order id on success.
        """
        raise NotImplementedError

    @abstractmethod
    async def cancel_order(self, symbol: str, order_id: str) -> CancelResult:
        """Cancel a pending order. `symbol` is required for broker routing."""
        raise NotImplementedError

    @abstractmethod
    async def cancel_all_for_symbol(self, symbol: str) -> int:
        """
        Cancel all pending orders for symbol (used in cleanup of
        orphan SL/TP after position closes). Returns count canceled.
        """
        raise NotImplementedError

    @abstractmethod
    async def close_position(
        self,
        symbol: str,
        contracts: Optional[int] = None,
    ) -> OrderResult:
        """
        Market-close a position (full or partial).
        contracts=None means close everything.
        """
        raise NotImplementedError

    @abstractmethod
    async def partial_close_via_opposite_order(
        self,
        symbol: str,
        direction: str,
        contracts_to_close: int,
        residual_contracts: int,
        new_sl_price: float,
        new_tp_price: float,
        old_stop_order_id: Optional[str],
        old_target_order_id: Optional[str],
    ) -> OrderResult:
        """V18 12-mag — chiusura parziale via ordine contrario + ricostruzione bracket.

        Sostituisce `close_position(size=N)` per gli asset (es. 6A) su cui
        ProjectX `/Position/partialCloseContract` ritorna 400. Flow:

          1. Market order direzione opposta per `contracts_to_close` (chiude metà)
          2. Cancella `old_stop_order_id` e `old_target_order_id` (bracket originale)
          3. Piazza nuovo STOP @ `new_sl_price` su `residual_contracts`
          4. Piazza nuovo LIMIT (TP) @ `new_tp_price` su `residual_contracts`

        `direction` = direzione del trade originale (BUY/SELL); l'ordine
        di chiusura usa il side opposto, e gli stop/limit del nuovo
        bracket riusano lo stesso side opposto (com'è già la convenzione
        in place_market_bracket).

        OrderResult.success indica il successo dello STEP 1 (closing market).
        Se step 1 ok ma 3/4 falliscono, il broker emergency-closes per
        evitare posizione senza SL. stop_id / target_id valorizzati con i
        nuovi ID (caller aggiorna `entry.stop_order_id` / `target_order_id`).
        """
        raise NotImplementedError

    @abstractmethod
    async def modify_stop(
        self,
        symbol: str,
        order_id: str,
        new_sl_price: float,
    ) -> OrderResult:
        """
        Modify the stop price of an existing SL order (used for breakeven
        and trailing stops).
        """
        raise NotImplementedError

    # ============================================================
    # ACCOUNT
    # ============================================================

    @abstractmethod
    async def get_account_balance(self) -> float:
        """
        Current account balance/equity in USD. Used by sizing for
        position calculation. Implementations should rely on broker-side
        caching (SDK socket-pushed account_info); V16 adds no own cache.
        Raises on transport/auth failure — caller decides fallback.
        """
        raise NotImplementedError

    # ============================================================
    # MARKET DATA (bars)
    # ============================================================

    @abstractmethod
    async def fetch_bars(self, symbol: str, timeframe: str, n: int):
        """
        Fetch the most recent N bars for `symbol` at `timeframe`.

        timeframe values: "5min" / "1hour" / "4hour" (V16 standard).
        Returns a pandas DataFrame with columns
        [open, high, low, close, volume]; empty DataFrame if no data.

        Used by analysis.market_data.BrokerMarketDataProvider to
        feed build_tech_snapshot. Promoted to abstract in Phase C1
        (was duck-typed before).
        """
        raise NotImplementedError
