"""
Market Microstructure Models for Cryptocurrency Order Books.

Implements price impact and spread models:
- Kyle's Lambda: Permanent price impact coefficient
- Amihud Illiquidity: Daily illiquidity measure
- Bid-Ask Spread Models: Adverse selection decomposition

Mathematical Foundation (Kyle 1985):
- Informed trader: x = β(v - p₀) where β = σᵤ/σᵥ
- Market maker: p = p₀ + λy where y = x + u (aggregate flow)
- Equilibrium: λ* = σᵥ/(2σᵤ) = √(Σ₀)/(2σᵤ)
- Market depth: 1/λ = 2σᵤ/σᵥ

Kyle's lambda measures permanent price impact per unit order flow.
Higher λ = less liquid market.

Glosten-Milgrom (1985):
Bid-ask spread from adverse selection:
Spread = A - B = μ(V_H - V_L)
where μ is probability of informed trader.

Crypto MEV considerations:
- >$7.2B extracted (2020-2024)
- 30% sandwich attacks, 35% arbitrage
- Front-running cost: ~0.1-0.5% per trade
"""
import math
from collections import deque
from dataclasses import dataclass

from models import Tick
from ..base import BaseModel, SignalCallback


@dataclass(slots=True)
class LiquidityMetrics:
    """Container for liquidity measurements."""
    kyle_lambda: float        # Price impact coefficient
    market_depth: float       # 1/λ
    amihud_illiq: float      # Amihud illiquidity ratio
    effective_spread: float   # Realized spread
    bid_ask_spread_bps: float # Current spread in bps


class KyleLambda(BaseModel):
    """
    Kyle's Lambda price impact model.

    Estimates permanent price impact from order flow:
    Δp = λ × Δq

    where:
    - Δp = price change
    - Δq = net order flow (positive = buy pressure)
    - λ = Kyle's lambda (price impact per unit flow)

    Uses regression of price changes on signed volume:
    λ = Cov(Δp, sign(Δp)) / Var(sign(Δp))

    For tick data without volume, approximates with return sign.
    """

    __slots__ = (
        'symbol', '_window_size',
        '_returns', '_signs', '_prices',
        '_prev_price', '_sum_ret_sign', '_sum_sign_sq',
        '_bid_ask_spread', '_tick_count_window',
    )

    def __init__(
        self,
        symbol: str,
        window_size: int = 200,
        warmup_ticks: int = 200,
        on_signal: SignalCallback | None = None,
    ):
        """
        Initialize Kyle's lambda estimator.

        Args:
            symbol: Asset symbol to track
            window_size: Rolling window for estimation
            warmup_ticks: Ticks before stable estimate
        """
        super().__init__(f"kyle_{symbol}", warmup_ticks, on_signal)
        self.symbol = symbol
        self._window_size = window_size

        self._returns: deque = deque(maxlen=window_size)
        self._signs: deque = deque(maxlen=window_size)
        self._prices: deque = deque(maxlen=window_size)

        self._prev_price = 0.0
        self._sum_ret_sign = 0.0  # Σ(rᵢ × sign(rᵢ))
        self._sum_sign_sq = 0.0   # Σ(sign²) = count of non-zero

        self._bid_ask_spread = 0.0
        self._tick_count_window = 0

    async def on_tick(self, tick: Tick) -> None:
        """Update Kyle's lambda with new tick."""
        if tick.symbol != self.symbol:
            return

        price = float(tick.mid_price())
        self._prices.append(price)

        # Track bid-ask spread
        spread = float(tick.ask - tick.bid) / price if price > 0 else 0
        self._bid_ask_spread = 0.95 * self._bid_ask_spread + 0.05 * spread

        if self._prev_price <= 0:
            self._prev_price = price
            return

        # Calculate return and sign (proxy for order flow direction)
        ret = math.log(price / self._prev_price)
        sign = 1.0 if ret > 0 else (-1.0 if ret < 0 else 0.0)

        self._prev_price = price

        # Update running sums (remove old if window full)
        if len(self._returns) == self._window_size:
            old_ret = self._returns[0]
            old_sign = self._signs[0]
            self._sum_ret_sign -= old_ret * old_sign
            self._sum_sign_sq -= old_sign * old_sign

        self._returns.append(ret)
        self._signs.append(sign)
        self._sum_ret_sign += ret * sign
        self._sum_sign_sq += sign * sign
        self._tick_count_window += 1

        self._check_warmup()

    def get_kyle_lambda(self) -> float:
        """
        Estimate Kyle's lambda (price impact coefficient).

        λ = Cov(Δp, sign) / Var(sign)
          = E[Δp × sign] / E[sign²]
          ≈ Σ(rᵢ × signᵢ) / Σ(signᵢ²)

        Note: This is a simplified estimate using return signs
        as proxy for order flow direction.
        """
        n = len(self._returns)
        if n < 30 or self._sum_sign_sq < 10:
            return 0.0

        # λ ≈ E[|return|] (since sign² = 1 when non-zero)
        return self._sum_ret_sign / self._sum_sign_sq

    def get_market_depth(self) -> float:
        """
        Get market depth (inverse of Kyle's lambda).

        Depth = 1/λ

        Higher depth = more liquid, can trade larger sizes
        without moving price.
        """
        lam = self.get_kyle_lambda()
        if lam < 1e-10:
            return float('inf')
        return 1.0 / lam

    def get_amihud_illiquidity(self) -> float:
        """
        Calculate Amihud illiquidity ratio.

        ILLIQ = |return| / volume

        Without volume data, use |return| as proxy.
        Higher = less liquid.
        """
        if len(self._returns) < 10:
            return 0.0

        # Sum of absolute returns (proxy for illiquidity)
        return sum(abs(r) for r in self._returns) / len(self._returns)

    def get_effective_spread(self) -> float:
        """
        Estimate effective spread from price reversals.

        Effective spread captures actual trading costs including
        market impact. Estimated from autocorrelation of returns.

        Roll's model: Spread = 2√(-Cov(rₜ, rₜ₋₁))
        """
        if len(self._returns) < 20:
            return self._bid_ask_spread

        returns = list(self._returns)

        # Autocovariance at lag 1
        mean_r = sum(returns) / len(returns)
        cov = sum(
            (returns[i] - mean_r) * (returns[i-1] - mean_r)
            for i in range(1, len(returns))
        ) / (len(returns) - 1)

        # Roll's spread estimate
        if cov < 0:
            return 2 * math.sqrt(-cov)
        else:
            # Positive autocorrelation - use bid-ask spread
            return self._bid_ask_spread

    def get_metrics(self) -> LiquidityMetrics:
        """Get all liquidity metrics."""
        lam = self.get_kyle_lambda()
        return LiquidityMetrics(
            kyle_lambda=lam,
            market_depth=1/lam if lam > 1e-10 else float('inf'),
            amihud_illiq=self.get_amihud_illiquidity(),
            effective_spread=self.get_effective_spread(),
            bid_ask_spread_bps=self._bid_ask_spread * 10000,
        )

    def get_stats(self) -> dict:
        """Return model statistics for dashboard."""
        metrics = self.get_metrics()
        stats = self._base_stats()
        stats.update({
            "symbol": self.symbol,
            "window_size": self._window_size,
            "samples": len(self._returns),
            "kyle_lambda": round(metrics.kyle_lambda * 10000, 4),  # in bps
            "kyle_lambda_interpretation": "price impact per unit flow (bps)",
            "market_depth": round(metrics.market_depth, 2),
            "amihud_illiq": round(metrics.amihud_illiq * 10000, 4),  # in bps
            "effective_spread_bps": round(metrics.effective_spread * 10000, 2),
            "bid_ask_spread_bps": round(metrics.bid_ask_spread_bps, 2),
        })
        return stats


class AMMImpermanentLoss:
    """
    Automated Market Maker impermanent loss calculator.

    For Uniswap-style constant product AMM: x × y = k

    Impermanent Loss:
    IL = V_LP / V_HODL - 1 = 2√P_ratio / (1 + P_ratio) - 1

    where P_ratio = P_current / P_deposit

    At P_ratio = 2 (price doubles): IL ≈ -5.7%
    At P_ratio = 4 (price 4x): IL ≈ -20%
    """

    @staticmethod
    def impermanent_loss(price_ratio: float) -> float:
        """
        Calculate impermanent loss for given price ratio.

        Args:
            price_ratio: P_current / P_deposit

        Returns:
            Impermanent loss (negative value = loss)
        """
        if price_ratio <= 0:
            return -1.0  # Total loss

        return 2 * math.sqrt(price_ratio) / (1 + price_ratio) - 1

    @staticmethod
    def breakeven_fees(price_ratio: float, days: float) -> float:
        """
        Calculate required daily fee APY to offset IL.

        Args:
            price_ratio: P_current / P_deposit
            days: Number of days LP position held

        Returns:
            Required daily APY to break even
        """
        il = AMMImpermanentLoss.impermanent_loss(price_ratio)
        if il >= 0:
            return 0.0

        # Required return to offset IL
        required = -il

        # Daily rate needed
        return required / days * 365  # Annualized


class SandwichAttackEstimator:
    """
    MEV Sandwich Attack cost estimator.

    Sandwich attack profit formula:
    Profit ≈ δ × Δx × (x + δ)⁻¹ × (x + δ + Δx)⁻¹ × y

    where:
    - δ = front-runner's trade size
    - Δx = victim's trade size
    - x, y = pool reserves
    """

    @staticmethod
    def victim_loss(
        trade_size: float,
        reserve_x: float,
        reserve_y: float,
        front_run_size: float,
    ) -> float:
        """
        Estimate victim's loss from sandwich attack.

        Args:
            trade_size: Victim's trade size (Δx)
            reserve_x: Pool reserve of token X
            reserve_y: Pool reserve of token Y
            front_run_size: Attacker's front-run size (δ)

        Returns:
            Victim's additional cost (in token Y terms)
        """
        # Original execution price
        original_price = reserve_y / reserve_x

        # Price after front-run
        x_after_front = reserve_x + front_run_size
        y_after_front = reserve_x * reserve_y / x_after_front

        # Victim's execution price
        victim_price = y_after_front / x_after_front

        # Price slippage as loss
        slippage = (victim_price - original_price) / original_price

        return slippage * trade_size

    @staticmethod
    def optimal_front_run(
        trade_size: float,
        reserve_x: float,
    ) -> float:
        """
        Estimate optimal front-run size for attacker.

        Simplified: δ* ≈ √(x × Δx) - x

        This maximizes attacker profit.
        """
        return math.sqrt(reserve_x * (reserve_x + trade_size)) - reserve_x


class GlostenMilgromSpread:
    """
    Glosten-Milgrom adverse selection spread model.

    Spread = A - B = μ × (V_H - V_L)

    where μ is probability of informed trader.
    """

    @staticmethod
    def theoretical_spread(
        prob_informed: float,
        value_high: float,
        value_low: float,
    ) -> float:
        """
        Calculate theoretical spread from adverse selection.

        Args:
            prob_informed: Probability trader is informed (μ)
            value_high: High value state
            value_low: Low value state

        Returns:
            Bid-ask spread
        """
        return prob_informed * (value_high - value_low)

    @staticmethod
    def implied_informed_prob(
        spread: float,
        price: float,
        daily_vol: float,
    ) -> float:
        """
        Infer probability of informed trading from spread.

        Assumes V_H - V_L ≈ 2 × daily_vol × price
        """
        value_range = 2 * daily_vol * price
        if value_range < 1e-10:
            return 0.0
        return min(spread / value_range, 1.0)
