"""
Turbo CLOB — Central Limit Order Book engine.

Price-time priority matching. Stores resting limit orders,
matches incoming orders against them, returns fills.

All prices are in 6 decimals (500000 = 50c = 50%).
All sizes are in 6 decimals (1000000 = 1 share = 1 USDC if wins).
"""

import time
import uuid
from dataclasses import dataclass, field
from enum import IntEnum
from typing import Optional


class Side(IntEnum):
    BUY = 0
    SELL = 1


class Outcome(IntEnum):
    YES = 0
    NO = 1


@dataclass
class Order:
    id: str
    market_id: str
    maker: str  # wallet address
    side: Side
    outcome: Outcome
    price: int  # 6 decimals
    size: int  # 6 decimals, original size
    remaining: int  # unfilled portion
    timestamp: float
    signature: str = ""
    nonce: int = 0
    expiration: int = 0

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "marketId": self.market_id,
            "maker": self.maker,
            "side": int(self.side),
            "outcome": int(self.outcome),
            "price": self.price,
            "size": self.size,
            "remaining": self.remaining,
            "timestamp": self.timestamp,
            "signature": self.signature,
            "nonce": self.nonce,
            "expiration": self.expiration,
        }


@dataclass
class Fill:
    """A matched fill between two orders."""
    buy_order: Order
    sell_order: Order
    price: int  # execution price (midpoint)
    size: int  # fill size
    timestamp: float


class OrderBook:
    """
    Single-outcome orderbook (YES or NO).
    Bids sorted highest-first (best bid at index 0).
    Asks sorted lowest-first (best ask at index 0).
    Price-time priority within each level.
    """

    def __init__(self):
        self.bids: list[Order] = []  # BUY orders, highest price first
        self.asks: list[Order] = []  # SELL orders, lowest price first

    def add_order(self, order: Order) -> list[Fill]:
        """Add an order. Returns list of fills if it crosses the spread."""
        fills = []

        if order.side == Side.BUY:
            # Match against best ask only (one fill per taker to avoid nonce reuse)
            if order.remaining > 0 and self.asks:
                best_ask = self.asks[0]
                if order.price >= best_ask.price:
                    fill_size = min(order.remaining, best_ask.remaining)
                    fill_price = best_ask.price  # fill at resting order's price
                    order.remaining -= fill_size
                    best_ask.remaining -= fill_size
                    fills.append(Fill(
                        buy_order=order,
                        sell_order=best_ask,
                        price=fill_price,
                        size=fill_size,
                        timestamp=time.time(),
                    ))
                    if best_ask.remaining == 0:
                        self.asks.pop(0)

            # Rest on book if unfilled
            if order.remaining > 0:
                self._insert_bid(order)

        else:  # SELL
            # Match against best bid only (one fill per taker to avoid nonce reuse)
            if order.remaining > 0 and self.bids:
                best_bid = self.bids[0]
                if order.price <= best_bid.price:
                    fill_size = min(order.remaining, best_bid.remaining)
                    fill_price = best_bid.price  # fill at resting order's price
                    order.remaining -= fill_size
                    best_bid.remaining -= fill_size
                    fills.append(Fill(
                        buy_order=best_bid,
                        sell_order=order,
                        price=fill_price,
                        size=fill_size,
                        timestamp=time.time(),
                    ))
                    if best_bid.remaining == 0:
                        self.bids.pop(0)

            if order.remaining > 0:
                self._insert_ask(order)

        return fills

    def cancel_order(self, order_id: str) -> Optional[Order]:
        """Cancel and remove an order by ID. Returns the order if found."""
        for lst in (self.bids, self.asks):
            for i, o in enumerate(lst):
                if o.id == order_id:
                    return lst.pop(i)
        return None

    def amend_order(self, order_id: str, new_price: Optional[int] = None,
                    new_size: Optional[int] = None) -> tuple[Optional[Order], list[Fill]]:
        """
        Amend an order's price or size.
        Price change = cancel + re-insert (loses priority).
        Size decrease = in-place (keeps priority).
        Returns (amended_order, fills).
        """
        old = self.cancel_order(order_id)
        if old is None:
            return None, []

        if new_price is not None:
            old.price = new_price
        if new_size is not None:
            filled_so_far = old.size - old.remaining
            old.size = new_size
            old.remaining = max(0, new_size - filled_so_far)

        if old.remaining <= 0:
            return old, []

        fills = self.add_order(old)
        return old, fills

    def get_levels(self, side: str, depth: int = 10) -> list[dict]:
        """Get aggregated price levels for display."""
        orders = self.bids if side == "bids" else self.asks
        levels: dict[int, float] = {}
        for o in orders:
            if o.remaining > 0:
                levels[o.price] = levels.get(o.price, 0) + o.remaining
        result = [{"price": p, "size": s} for p, s in levels.items()]
        return result[:depth]

    def _insert_bid(self, order: Order):
        """Insert bid in sorted position (highest price first, then oldest first)."""
        for i, existing in enumerate(self.bids):
            if order.price > existing.price:
                self.bids.insert(i, order)
                return
            if order.price == existing.price and order.timestamp < existing.timestamp:
                self.bids.insert(i, order)
                return
        self.bids.append(order)

    def _insert_ask(self, order: Order):
        """Insert ask in sorted position (lowest price first, then oldest first)."""
        for i, existing in enumerate(self.asks):
            if order.price < existing.price:
                self.asks.insert(i, order)
                return
            if order.price == existing.price and order.timestamp < existing.timestamp:
                self.asks.insert(i, order)
                return
        self.asks.append(order)


class CLOB:
    """
    Full CLOB for one market. Contains a YES book and a NO book.
    For Kalshi-style display, the frontend shows the YES book
    (UP = YES bids/asks).
    """

    def __init__(self, market_id: str):
        self.market_id = market_id
        self.yes_book = OrderBook()
        self.no_book = OrderBook()
        self.trades: list[dict] = []  # recent trade history
        self.all_orders: dict[str, Order] = {}  # id -> order

    def place_order(
        self,
        maker: str,
        side: Side,
        outcome: Outcome,
        price: int,
        size: int,
        signature: str = "",
        nonce: int = 0,
        expiration: int = 0,
    ) -> tuple[Order, list[Fill]]:
        """Place a new order. Returns (order, fills)."""
        order = Order(
            id=uuid.uuid4().hex[:12],
            market_id=self.market_id,
            maker=maker,
            side=side,
            outcome=outcome,
            price=price,
            size=size,
            remaining=size,
            timestamp=time.time(),
            signature=signature,
            nonce=nonce,
            expiration=expiration,
        )

        book = self.yes_book if outcome == Outcome.YES else self.no_book
        fills = book.add_order(order)
        self.all_orders[order.id] = order

        # Record trades
        for f in fills:
            self.trades.append({
                "marketId": self.market_id,
                "buyer": f.buy_order.maker,
                "seller": f.sell_order.maker,
                "outcome": int(outcome),
                "price": f.price,
                "size": f.size,
                "timestamp": f.timestamp,
            })
            if len(self.trades) > 100:
                self.trades = self.trades[-100:]

        return order, fills

    def cancel_order(self, order_id: str) -> Optional[Order]:
        """Cancel an order by ID."""
        order = self.all_orders.get(order_id)
        if not order:
            return None
        book = self.yes_book if order.outcome == Outcome.YES else self.no_book
        removed = book.cancel_order(order_id)
        if removed:
            del self.all_orders[order_id]
        return removed

    def amend_order(self, order_id: str, new_price: Optional[int] = None,
                    new_size: Optional[int] = None) -> tuple[Optional[Order], list[Fill]]:
        """Amend an order's price or size."""
        order = self.all_orders.get(order_id)
        if not order:
            return None, []
        book = self.yes_book if order.outcome == Outcome.YES else self.no_book
        amended, fills = book.amend_order(order_id, new_price, new_size)

        for f in fills:
            self.trades.append({
                "marketId": self.market_id,
                "buyer": f.buy_order.maker,
                "seller": f.sell_order.maker,
                "outcome": int(order.outcome),
                "price": f.price,
                "size": f.size,
                "timestamp": f.timestamp,
            })

        return amended, fills

    def get_orders_by_maker(self, maker: str) -> list[Order]:
        """Get all resting orders for a maker."""
        addr = maker.lower()
        return [o for o in self.all_orders.values()
                if o.maker.lower() == addr and o.remaining > 0]

    def get_book_snapshot(self, outcome: Outcome) -> dict:
        """Get orderbook snapshot for display."""
        book = self.yes_book if outcome == Outcome.YES else self.no_book
        return {
            "bids": book.get_levels("bids"),
            "asks": book.get_levels("asks"),
        }
