"""Mock exchange implementation for testing and simulation.

This module provides a MockExchange class that simulates exchange behavior
without making real API calls, useful for testing and paper trading.
"""

import asyncio
import random
import uuid
from datetime import datetime
from typing import Dict, List, Optional

from openclaw.exchange.base import Exchange, ExchangeError, InsufficientFundsError
from openclaw.exchange.models import (
    Balance,
    Order,
    OrderSide,
    OrderStatus,
    OrderType,
    Position,
    Ticker,
)
from openclaw.utils.logging import get_logger


class MockExchange(Exchange):
    """Mock exchange for testing and simulation.

    Simulates exchange behavior with configurable latency, slippage,
    and market data. Useful for testing strategies without real money.

    Args:
        name: Exchange name
        initial_balances: Initial balance configuration {asset: amount}
        latency_ms: Simulated API latency in milliseconds
        slippage_pct: Simulated price slippage percentage
    """

    def __init__(
        self,
        name: str = "mock",
        initial_balances: Optional[Dict[str, float]] = None,
        latency_ms: float = 10.0,
        slippage_pct: float = 0.1,
    ):
        super().__init__(name=name, is_simulated=True)
        self.logger = get_logger(f"exchange.{name}")

        # Configuration
        self.latency_ms = latency_ms
        self.slippage_pct = slippage_pct

        # Initialize balances
        self._balances: Dict[str, Balance] = {}
        initial_balances = initial_balances or {"USDT": 10000.0}
        for asset, amount in initial_balances.items():
            self._balances[asset.upper()] = Balance(asset=asset.upper(), free=amount, locked=0.0)

        # Orders and positions
        self._orders: Dict[str, Order] = {}
        self._positions: Dict[str, Position] = {}

        # Simulated market data
        self._tickers: Dict[str, Ticker] = {}
        self._setup_default_tickers()

        self.logger.info(f"MockExchange initialized with {len(self._balances)} assets")

    def _setup_default_tickers(self) -> None:
        """Set up default simulated tickers."""
        default_prices = {
            "BTC/USDT": 65000.0,
            "ETH/USDT": 3500.0,
            "AAPL": 175.0,
            "GOOGL": 140.0,
            "MSFT": 420.0,
            "TSLA": 250.0,
        }

        for symbol, price in default_prices.items():
            self._tickers[symbol] = Ticker(
                symbol=symbol,
                bid=price * 0.9995,
                ask=price * 1.0005,
                last=price,
                high=price * 1.05,
                low=price * 0.95,
                volume=random.uniform(1000000, 10000000),
            )

    async def _simulate_latency(self) -> None:
        """Simulate network latency."""
        if self.latency_ms > 0:
            await asyncio.sleep(self.latency_ms / 1000)

    async def connect(self) -> bool:
        """Connect to mock exchange (always succeeds)."""
        await self._simulate_latency()
        self._connected = True
        self.logger.info("Connected to mock exchange")
        return True

    async def disconnect(self) -> None:
        """Disconnect from mock exchange."""
        await self._simulate_latency()
        self._connected = False
        self.logger.info("Disconnected from mock exchange")

    def _get_base_quote(self, symbol: str) -> tuple[str, str]:
        """Extract base and quote assets from symbol."""
        if "/" in symbol:
            base, quote = symbol.split("/", 1)
            return base.upper(), quote.upper()
        # For stocks, assume USD as quote
        return symbol.upper(), "USD"

    def _apply_slippage(self, price: float, side: OrderSide) -> float:
        """Apply slippage to price based on side."""
        slippage = price * (self.slippage_pct / 100)
        if side == OrderSide.BUY:
            return price + slippage
        return price - slippage

    async def place_order(
        self,
        symbol: str,
        side: OrderSide,
        amount: float,
        price: Optional[float] = None,
        order_type: str = "market",
    ) -> Order:
        """Place a simulated order."""
        await self._simulate_latency()

        base_asset, quote_asset = self._get_base_quote(symbol)

        # Get current price
        ticker = await self.get_ticker(symbol)
        if price is None:
            price = ticker.ask if side == OrderSide.BUY else ticker.bid

        # Apply slippage
        executed_price = self._apply_slippage(price, side)

        # Calculate required quote amount
        quote_amount = executed_price * amount

        # Check balance
        quote_balance = self._balances.get(quote_asset, Balance(asset=quote_asset, free=0.0))
        if side == OrderSide.BUY and quote_balance.free < quote_amount:
            raise InsufficientFundsError(
                f"Insufficient {quote_asset} balance: {quote_balance.free:.4f} < {quote_amount:.4f}"
            )

        base_balance = self._balances.get(base_asset, Balance(asset=base_asset, free=0.0))
        if side == OrderSide.SELL and base_balance.free < amount:
            raise InsufficientFundsError(
                f"Insufficient {base_asset} balance: {base_balance.free:.4f} < {amount:.4f}"
            )

        # Create order
        order_id = str(uuid.uuid4())[:8]
        order = Order(
            order_id=order_id,
            symbol=symbol.upper(),
            side=side,
            order_type=OrderType(order_type),
            amount=amount,
            price=executed_price,
            status=OrderStatus.FILLED,
            filled_amount=amount,
            exchange_id=f"mock_{order_id}",
        )

        # Update balances
        if side == OrderSide.BUY:
            # Deduct quote, add base
            quote_balance.free -= quote_amount
            self._balances[quote_asset] = quote_balance

            if base_asset not in self._balances:
                self._balances[base_asset] = Balance(asset=base_asset, free=0.0)
            self._balances[base_asset].free += amount
        else:
            # Deduct base, add quote
            base_balance.free -= amount
            self._balances[base_asset] = base_balance

            if quote_asset not in self._balances:
                self._balances[quote_asset] = Balance(asset=quote_asset, free=0.0)
            self._balances[quote_asset].free += quote_amount

        # Update position
        await self._update_position(symbol, side, amount, executed_price)

        self._orders[order_id] = order
        side_str = side.value if hasattr(side, 'value') else side
        self.logger.info(f"Order placed: {order_id} {side_str} {amount} {symbol} @ {executed_price:.4f}")

        return order

    async def _update_position(
        self, symbol: str, side: OrderSide, amount: float, price: float
    ) -> None:
        """Update position after a trade."""
        symbol_upper = symbol.upper()

        if symbol_upper not in self._positions:
            # Create new position
            self._positions[symbol_upper] = Position(
                symbol=symbol_upper,
                side=side,
                amount=amount,
                entry_price=price,
                current_price=price,
            )
        else:
            pos = self._positions[symbol_upper]
            if pos.side == side:
                # Adding to existing position
                total_value = (pos.amount * pos.entry_price) + (amount * price)
                total_amount = pos.amount + amount
                pos.entry_price = total_value / total_amount
                pos.amount = total_amount
            else:
                # Reducing or reversing position
                if amount >= pos.amount:
                    # Position closed or reversed
                    remaining = amount - pos.amount
                    if remaining > 0:
                        # Reversed
                        pos.side = side
                        pos.amount = remaining
                        pos.entry_price = price
                    else:
                        # Closed
                        del self._positions[symbol_upper]
                else:
                    # Partial close
                    pos.amount -= amount

            pos.current_price = price

    async def cancel_order(self, order_id: str) -> bool:
        """Cancel a simulated order."""
        await self._simulate_latency()

        if order_id not in self._orders:
            return False

        order = self._orders[order_id]
        if order.status in (OrderStatus.FILLED, OrderStatus.CANCELLED):
            return False

        order.status = OrderStatus.CANCELLED
        order.updated_at = datetime.now()
        self.logger.info(f"Order cancelled: {order_id}")

        return True

    async def get_order(self, order_id: str) -> Optional[Order]:
        """Get order details."""
        await self._simulate_latency()
        return self._orders.get(order_id)

    async def get_open_orders(self, symbol: Optional[str] = None) -> List[Order]:
        """Get open orders."""
        await self._simulate_latency()

        orders = [
            order for order in self._orders.values()
            if order.status in (OrderStatus.PENDING, OrderStatus.OPEN, OrderStatus.PARTIALLY_FILLED)
        ]

        if symbol:
            symbol_upper = symbol.upper()
            orders = [o for o in orders if o.symbol == symbol_upper]

        return orders

    async def get_balance(self, asset: Optional[str] = None) -> List[Balance]:
        """Get account balance."""
        await self._simulate_latency()

        if asset:
            asset_upper = asset.upper()
            if asset_upper in self._balances:
                return [self._balances[asset_upper]]
            return []

        return list(self._balances.values())

    async def get_positions(self, symbol: Optional[str] = None) -> List[Position]:
        """Get current positions."""
        await self._simulate_latency()

        positions = list(self._positions.values())

        if symbol:
            symbol_upper = symbol.upper()
            positions = [p for p in positions if p.symbol == symbol_upper]

        return positions

    async def get_ticker(self, symbol: str) -> Ticker:
        """Get market ticker."""
        await self._simulate_latency()

        symbol_upper = symbol.upper()

        if symbol_upper in self._tickers:
            ticker = self._tickers[symbol_upper]
            # Simulate small price movements
            price_change = random.uniform(-0.001, 0.001)
            new_last = ticker.last * (1 + price_change)
            ticker.last = new_last
            ticker.bid = new_last * 0.9995
            ticker.ask = new_last * 1.0005
            ticker.timestamp = datetime.now()
            return ticker

        # Generate a default ticker
        default_price = random.uniform(10.0, 1000.0)
        ticker = Ticker(
            symbol=symbol_upper,
            bid=default_price * 0.9995,
            ask=default_price * 1.0005,
            last=default_price,
            high=default_price * 1.05,
            low=default_price * 0.95,
            volume=random.uniform(100000, 1000000),
        )
        self._tickers[symbol_upper] = ticker
        return ticker

    def update_ticker(self, symbol: str, price: float) -> None:
        """Manually update ticker price (for testing)."""
        symbol_upper = symbol.upper()
        self._tickers[symbol_upper] = Ticker(
            symbol=symbol_upper,
            bid=price * 0.9995,
            ask=price * 1.0005,
            last=price,
            high=price * 1.05,
            low=price * 0.95,
            volume=random.uniform(100000, 1000000),
        )

    def set_balance(self, asset: str, amount: float) -> None:
        """Manually set balance (for testing)."""
        asset_upper = asset.upper()
        self._balances[asset_upper] = Balance(asset=asset_upper, free=amount, locked=0.0)
