"""
Trading Domain Models

Core trading structures for strategy authoring: Signals, Orders, Positions,
Trades, AccountBalance, TradingContext.
"""
from dataclasses import dataclass
from typing import TypedDict, Optional, Dict, Any
from enum import Enum


class SignalType(str, Enum):
    """Trading signal types."""
    BUY = "buy"
    SELL = "sell"
    HOLD = "hold"


class OrderSide(str, Enum):
    """Order side."""
    BUY = "buy"
    SELL = "sell"


class OrderType(str, Enum):
    """Order types."""
    MARKET = "market"
    LIMIT = "limit"
    STOP = "stop"
    STOP_LIMIT = "stop_limit"


class OrderStatus(str, Enum):
    """Order status."""
    PENDING = "pending"
    OPEN = "open"
    FILLED = "filled"
    PARTIAL = "partial"
    CANCELLED = "cancelled"
    REJECTED = "rejected"


class PositionSide(str, Enum):
    """Position direction."""
    LONG = "long"
    SHORT = "short"
    FLAT = "flat"


# ============================================================================
# Signal
# ============================================================================

class SignalDict(TypedDict, total=False):
    """Signal for serialization."""
    timestamp: int
    signal_type: str
    price: float
    reason: str
    extra: Dict[str, Any]


@dataclass(frozen=True, slots=True)
class Signal:
    """
    Trading signal from a strategy.

    Supports flexible position sizing:
    - quantity: Absolute number of units
    - percent_equity: Percentage of equity (0.1 = 10%)
    - percent_position: Percentage of current position (for partial closes)

    If no sizing is specified, the backtest config defaults are used.
    """
    timestamp: int
    signal_type: SignalType
    price: float
    reason: str = ""
    strength: float = 1.0  # 0.0 to 1.0

    # Position sizing (optional - strategy can override config)
    quantity: Optional[float] = None        # Absolute quantity
    percent_equity: Optional[float] = None  # % of equity (0.1 = 10%)
    percent_position: Optional[float] = None  # % of position (for partial closes)

    # Risk management (optional)
    stop_loss: Optional[float] = None
    take_profit: Optional[float] = None

    # Feature tracking (optional) - capture any values at signal time
    extra: Optional[Dict[str, Any]] = None

    @property
    def is_buy(self) -> bool:
        return self.signal_type == SignalType.BUY

    @property
    def is_sell(self) -> bool:
        return self.signal_type == SignalType.SELL

    @property
    def is_close(self) -> bool:
        """Check if this is a close signal (sell for long positions)."""
        return self.signal_type == SignalType.SELL

    @property
    def has_sizing(self) -> bool:
        """Check if explicit sizing is provided."""
        return (
            self.quantity is not None or
            self.percent_equity is not None or
            self.percent_position is not None
        )

    def to_dict(self) -> SignalDict:
        d = SignalDict(
            timestamp=self.timestamp,
            signal_type=self.signal_type.value,
            price=self.price,
            reason=self.reason,
        )
        if self.extra:
            d['extra'] = self.extra
        return d

    @classmethod
    def from_dict(cls, data: SignalDict) -> 'Signal':
        return cls(
            timestamp=data['timestamp'],
            signal_type=SignalType(data['signal_type']),
            price=data['price'],
            reason=data.get('reason', ''),
            extra=data.get('extra'),
        )


# ============================================================================
# Order
# ============================================================================

class OrderDict(TypedDict):
    """Order for serialization."""
    id: str
    symbol: str
    side: str
    type: str
    status: str
    quantity: float
    price: float
    filled_quantity: float
    filled_price: float
    created_at: int


@dataclass
class Order:
    """Trading order."""
    id: str
    symbol: str
    side: OrderSide
    type: OrderType
    quantity: float
    price: float = 0.0  # For limit orders
    status: OrderStatus = OrderStatus.PENDING
    filled_quantity: float = 0.0
    filled_price: float = 0.0
    created_at: int = 0
    updated_at: int = 0

    @property
    def is_filled(self) -> bool:
        return self.status == OrderStatus.FILLED

    @property
    def is_pending(self) -> bool:
        return self.status in (OrderStatus.PENDING, OrderStatus.OPEN)

    @property
    def remaining_quantity(self) -> float:
        return self.quantity - self.filled_quantity

    def to_dict(self) -> OrderDict:
        return OrderDict(
            id=self.id,
            symbol=self.symbol,
            side=self.side.value,
            type=self.type.value,
            status=self.status.value,
            quantity=self.quantity,
            price=self.price,
            filled_quantity=self.filled_quantity,
            filled_price=self.filled_price,
            created_at=self.created_at,
        )


# ============================================================================
# Position
# ============================================================================

class PositionDict(TypedDict):
    """Position for serialization."""
    symbol: str
    side: str
    quantity: float
    entry_price: float
    current_price: float
    unrealized_pnl: float
    unrealized_pnl_pct: float


@dataclass
class Position:
    """Open trading position."""
    symbol: str
    side: PositionSide
    quantity: float
    entry_price: float
    entry_timestamp: int
    stop_loss: Optional[float] = None
    take_profit: Optional[float] = None
    entry_reason: str = ""

    def unrealized_pnl(self, current_price: float) -> float:
        """Calculate unrealized P&L."""
        if self.side == PositionSide.LONG:
            return (current_price - self.entry_price) * self.quantity
        elif self.side == PositionSide.SHORT:
            return (self.entry_price - current_price) * self.quantity
        return 0.0

    def unrealized_pnl_pct(self, current_price: float) -> float:
        """Calculate unrealized P&L percentage."""
        if self.entry_price == 0:
            return 0.0
        if self.side == PositionSide.LONG:
            return ((current_price - self.entry_price) / self.entry_price) * 100
        elif self.side == PositionSide.SHORT:
            return ((self.entry_price - current_price) / self.entry_price) * 100
        return 0.0

    def to_dict(self, current_price: float) -> PositionDict:
        return PositionDict(
            symbol=self.symbol,
            side=self.side.value,
            quantity=self.quantity,
            entry_price=self.entry_price,
            current_price=current_price,
            unrealized_pnl=self.unrealized_pnl(current_price),
            unrealized_pnl_pct=self.unrealized_pnl_pct(current_price),
        )


# ============================================================================
# Open Trade (Unrealized Position)
# ============================================================================

class OpenTradeDict(TypedDict, total=False):
    """Open trade for serialization."""
    entry_timestamp: int
    entry_price: float
    current_price: float
    signal_type: str
    quantity: float
    unrealized_pnl: float
    unrealized_pnl_pct: float
    entry_reason: str
    entry_count: int
    entry_extra: Dict[str, Any]


@dataclass(frozen=True, slots=True)
class OpenTrade:
    """Open trade (position not yet closed)."""
    entry_timestamp: int
    entry_price: float
    current_price: float
    side: PositionSide
    quantity: float
    unrealized_pnl: float
    unrealized_pnl_pct: float
    entry_reason: str = ""
    entry_count: int = 1  # Number of pyramid entries
    entry_extra: Optional[Dict[str, Any]] = None

    @property
    def is_profitable(self) -> bool:
        return self.unrealized_pnl > 0

    @property
    def market_value(self) -> float:
        return self.quantity * self.current_price

    def to_dict(self) -> OpenTradeDict:
        d = OpenTradeDict(
            entry_timestamp=self.entry_timestamp,
            entry_price=self.entry_price,
            current_price=self.current_price,
            signal_type=self.side.value,
            quantity=self.quantity,
            unrealized_pnl=self.unrealized_pnl,
            unrealized_pnl_pct=self.unrealized_pnl_pct,
            entry_reason=self.entry_reason,
            entry_count=self.entry_count,
        )
        if self.entry_extra:
            d['entry_extra'] = self.entry_extra
        return d


# ============================================================================
# Trade (Closed Position)
# ============================================================================

class TradeDict(TypedDict, total=False):
    """Completed trade for serialization."""
    entry_timestamp: int
    exit_timestamp: int
    entry_price: float
    exit_price: float
    signal_type: str
    quantity: float
    pnl: float
    pnl_percent: float
    entry_reason: str
    exit_reason: str
    commission: float
    entry_extra: Dict[str, Any]
    exit_extra: Dict[str, Any]


@dataclass(frozen=True, slots=True)
class Trade:
    """Completed trade (closed position)."""
    entry_timestamp: int
    exit_timestamp: int
    entry_price: float
    exit_price: float
    side: PositionSide
    quantity: float
    pnl: float
    pnl_pct: float
    entry_reason: str = ""
    exit_reason: str = ""
    commission: float = 0.0
    entry_extra: Optional[Dict[str, Any]] = None
    exit_extra: Optional[Dict[str, Any]] = None

    @property
    def is_winner(self) -> bool:
        return self.pnl > 0

    @property
    def is_loser(self) -> bool:
        return self.pnl < 0

    @property
    def duration_ms(self) -> int:
        return self.exit_timestamp - self.entry_timestamp

    @property
    def net_pnl(self) -> float:
        return self.pnl - self.commission

    def to_dict(self) -> TradeDict:
        d = TradeDict(
            entry_timestamp=self.entry_timestamp,
            exit_timestamp=self.exit_timestamp,
            entry_price=self.entry_price,
            exit_price=self.exit_price,
            signal_type=self.side.value,
            quantity=self.quantity,
            pnl=self.pnl,
            pnl_percent=self.pnl_pct,
            entry_reason=self.entry_reason,
            exit_reason=self.exit_reason,
            commission=self.commission,
        )
        if self.entry_extra:
            d['entry_extra'] = self.entry_extra
        if self.exit_extra:
            d['exit_extra'] = self.exit_extra
        return d

    @classmethod
    def from_dict(cls, data: TradeDict) -> 'Trade':
        return cls(
            entry_timestamp=data['entry_timestamp'],
            exit_timestamp=data['exit_timestamp'],
            entry_price=data['entry_price'],
            exit_price=data['exit_price'],
            side=PositionSide(data['side']),
            quantity=data.get('quantity', 1.0),
            pnl=data['pnl'],
            pnl_pct=data['pnl_pct'],
            entry_reason=data.get('entry_reason', ''),
            exit_reason=data.get('exit_reason', ''),
            commission=data.get('commission', 0.0),
            entry_extra=data.get('entry_extra'),
            exit_extra=data.get('exit_extra'),
        )


# ============================================================================
# Account Balance
# ============================================================================

class AccountBalanceDict(TypedDict):
    """Account balance for serialization."""
    currency: str
    cash: float
    equity: float
    buying_power: float
    market_value: float
    unrealized_pnl: float
    realized_pnl: float
    total_btc_value: float
    total_usd_value: float


@dataclass
class AccountBalance:
    """Account balance information."""
    currency: str = "USD"
    cash: float = 0.0
    equity: float = 0.0
    buying_power: float = 0.0
    market_value: float = 0.0
    unrealized_pnl: float = 0.0
    realized_pnl: float = 0.0
    total_btc_value: float = 0.0
    total_usd_value: float = 0.0

    @property
    def total_pnl(self) -> float:
        """Total P&L (realized + unrealized)."""
        return self.realized_pnl + self.unrealized_pnl

    def to_dict(self) -> AccountBalanceDict:
        return AccountBalanceDict(
            currency=self.currency,
            cash=self.cash,
            equity=self.equity,
            buying_power=self.buying_power,
            market_value=self.market_value,
            unrealized_pnl=self.unrealized_pnl,
            realized_pnl=self.realized_pnl,
            total_btc_value=self.total_btc_value,
            total_usd_value=self.total_usd_value,
        )


# ============================================================================
# Trading Context (from domain/context.py)
# ============================================================================

class SizingMode(str, Enum):
    """Position sizing modes."""
    PERCENT_EQUITY = "percent_equity"
    FIXED_QUANTITY = "fixed_quantity"
    FIXED_VALUE = "fixed_value"


@dataclass(frozen=True, slots=True)
class PositionInfo:
    """Immutable snapshot of current position state."""
    side: PositionSide
    quantity: float
    avg_entry_price: float
    unrealized_pnl: float
    unrealized_pnl_pct: float
    entry_count: int  # Number of pyramid entries

    @property
    def is_flat(self) -> bool:
        """No position."""
        return self.side == PositionSide.FLAT or self.quantity == 0

    @property
    def is_long(self) -> bool:
        """Long position."""
        return self.side == PositionSide.LONG and self.quantity > 0

    @property
    def is_short(self) -> bool:
        """Short position."""
        return self.side == PositionSide.SHORT and self.quantity > 0

    @classmethod
    def flat(cls) -> 'PositionInfo':
        """Create a flat (no position) state."""
        return cls(
            side=PositionSide.FLAT,
            quantity=0.0,
            avg_entry_price=0.0,
            unrealized_pnl=0.0,
            unrealized_pnl_pct=0.0,
            entry_count=0,
        )


@dataclass(frozen=True, slots=True)
class TradingContext:
    """
    Immutable context passed to strategies for decision-making.

    Provides all the information a strategy needs:
    - Portfolio state (capital, equity, position)
    - Configuration (sizing mode, pyramiding limits)
    - Current market state (bar index, price)
    """
    # Portfolio State
    initial_capital: float
    equity: float
    cash: float

    # Current Position
    position: PositionInfo

    # Market State
    bar_index: int
    current_price: float
    timestamp: int

    # Configuration
    default_size: float = 1.0
    sizing_mode: SizingMode = SizingMode.PERCENT_EQUITY
    max_entries: int = 1  # Max pyramid entries

    @property
    def can_add_entry(self) -> bool:
        """Can we add another pyramid entry?"""
        return self.position.entry_count < self.max_entries

    @property
    def available_capital(self) -> float:
        """Capital available for new positions."""
        return self.cash

    @property
    def position_value(self) -> float:
        """Current position market value."""
        return self.position.quantity * self.current_price

    def size_for_percent(self, percent: float) -> float:
        """Calculate quantity for a percentage of equity."""
        if self.current_price <= 0:
            return 0.0
        value = self.equity * percent
        return value / self.current_price

    def size_for_value(self, value: float) -> float:
        """Calculate quantity for a fixed dollar value."""
        if self.current_price <= 0:
            return 0.0
        return value / self.current_price

    def size_for_risk(self, risk_percent: float, stop_distance: float) -> float:
        """Calculate quantity based on risk per trade."""
        if stop_distance <= 0:
            return 0.0
        risk_amount = self.equity * risk_percent
        return risk_amount / stop_distance
