"""
Limit Order Book (LOB) 再構築モジュール

MBOイベントから個別注文レベルのLimit Order Bookを再構築する。
add/modify/cancelイベントを処理し、
リアルタイムの板状態を管理する。
"""

import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple

from ..models import MBOEvent, MBOAction, Side

logger = logging.getLogger(__name__)

# numpyはオプション依存（クライアント側でのみ必要）
try:
    import numpy as np
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False


@dataclass
class Order:
    """個別注文の情報"""
    order_id: str     # 注文のユニークID
    side: str         # 方向 (bid/ask)
    price: float      # 価格
    size: int          # 数量
    timestamp: float   # タイムスタンプ


class OrderBook:
    """
    Limit Order Book (LOB) 再構築クラス。

    MBOイベントストリームから板情報をリアルタイムに再構築する。
    個別注文レベルの粒度を保持し、
    価格レベルごとの集約ビューも提供する。
    """

    def __init__(self, symbol: str = ""):
        """
        Args:
            symbol: シンボル名
        """
        self.symbol = symbol

        # 個別注文の管理 (order_id -> Order)
        self._orders: Dict[str, Order] = {}

        # 価格レベルごとの注文数量集約
        # bid側: price -> total_size
        self._bid_levels: Dict[float, int] = defaultdict(int)
        # ask側: price -> total_size
        self._ask_levels: Dict[float, int] = defaultdict(int)

        # 価格レベルごとの注文数
        self._bid_order_counts: Dict[float, int] = defaultdict(int)
        self._ask_order_counts: Dict[float, int] = defaultdict(int)

        # 更新カウンター
        self._update_count = 0

    def process_event(self, event: MBOEvent) -> None:
        """
        MBOイベントを処理して板を更新する。

        Args:
            event: MBOイベント
        """
        action = event.action

        if action in (MBOAction.ADD, MBOAction.IMAGE):
            self._add_order(event)
        elif action == MBOAction.MODIFY:
            self._modify_order(event)
        elif action == MBOAction.CANCEL:
            self._cancel_order(event)
        elif action in (MBOAction.CLEAR, MBOAction.RESET):
            self.clear()
        else:
            logger.warning(f"不明なMBOアクション: {action}")

        self._update_count += 1

    def _add_order(self, event: MBOEvent) -> None:
        """新規注文を追加する"""
        order = Order(
            order_id=event.order_id,
            side=event.side,
            price=event.price,
            size=event.size,
            timestamp=event.timestamp,
        )

        # 既存注文のチェック（上書きの場合は先にキャンセル処理）
        if event.order_id in self._orders:
            self._remove_from_levels(self._orders[event.order_id])

        self._orders[event.order_id] = order

        # 価格レベルに追加
        if event.side == Side.BID:
            self._bid_levels[event.price] += event.size
            self._bid_order_counts[event.price] += 1
        else:
            self._ask_levels[event.price] += event.size
            self._ask_order_counts[event.price] += 1

    def _modify_order(self, event: MBOEvent) -> None:
        """既存注文を変更する"""
        if event.order_id not in self._orders:
            # 不明な注文IDの場合は新規追加として処理
            logger.debug(f"不明な注文IDのmodify: {event.order_id}")
            self._add_order(event)
            return

        old_order = self._orders[event.order_id]

        # 古い値を価格レベルから除去
        self._remove_from_levels(old_order)

        # 新しい値で更新
        old_order.price = event.price
        old_order.size = event.size
        old_order.timestamp = event.timestamp

        # 新しい値を価格レベルに追加
        if old_order.side == Side.BID:
            self._bid_levels[event.price] += event.size
            self._bid_order_counts[event.price] += 1
        else:
            self._ask_levels[event.price] += event.size
            self._ask_order_counts[event.price] += 1

    def _cancel_order(self, event: MBOEvent) -> None:
        """注文をキャンセルする"""
        if event.order_id not in self._orders:
            logger.debug(f"不明な注文IDのcancel: {event.order_id}")
            return

        order = self._orders.pop(event.order_id)
        self._remove_from_levels(order)

    def _remove_from_levels(self, order: Order) -> None:
        """注文を価格レベルから除去する"""
        if order.side == Side.BID:
            self._bid_levels[order.price] -= order.size
            self._bid_order_counts[order.price] -= 1
            # ゼロ以下になった価格レベルをクリーンアップ
            if self._bid_levels[order.price] <= 0:
                del self._bid_levels[order.price]
            if self._bid_order_counts[order.price] <= 0:
                del self._bid_order_counts[order.price]
        else:
            self._ask_levels[order.price] -= order.size
            self._ask_order_counts[order.price] -= 1
            if self._ask_levels[order.price] <= 0:
                del self._ask_levels[order.price]
            if self._ask_order_counts[order.price] <= 0:
                del self._ask_order_counts[order.price]

    def get_bids(self, n: int = 10) -> List[Tuple[float, int, int]]:
        """
        上位N段のBid板情報を取得する（価格降順）。

        Returns:
            List[(price, total_size, order_count)]
        """
        sorted_prices = sorted(self._bid_levels.keys(), reverse=True)[:n]
        return [
            (price, self._bid_levels[price], self._bid_order_counts.get(price, 0))
            for price in sorted_prices
        ]

    def get_asks(self, n: int = 10) -> List[Tuple[float, int, int]]:
        """
        上位N段のAsk板情報を取得する（価格昇順）。

        Returns:
            List[(price, total_size, order_count)]
        """
        sorted_prices = sorted(self._ask_levels.keys())[:n]
        return [
            (price, self._ask_levels[price], self._ask_order_counts.get(price, 0))
            for price in sorted_prices
        ]

    def get_best_bid(self) -> Optional[Tuple[float, int]]:
        """最良Bid（最高価格）を取得する"""
        if not self._bid_levels:
            return None
        price = max(self._bid_levels.keys())
        return (price, self._bid_levels[price])

    def get_best_ask(self) -> Optional[Tuple[float, int]]:
        """最良Ask（最低価格）を取得する"""
        if not self._ask_levels:
            return None
        price = min(self._ask_levels.keys())
        return (price, self._ask_levels[price])

    def get_mid_price(self) -> Optional[float]:
        """仲値（Mid Price）を取得する"""
        best_bid = self.get_best_bid()
        best_ask = self.get_best_ask()
        if best_bid is None or best_ask is None:
            return None
        return (best_bid[0] + best_ask[0]) / 2.0

    def get_spread(self) -> Optional[float]:
        """スプレッドを取得する"""
        best_bid = self.get_best_bid()
        best_ask = self.get_best_ask()
        if best_bid is None or best_ask is None:
            return None
        return best_ask[0] - best_bid[0]

    def get_total_bid_volume(self, n: int = 10) -> int:
        """上位N段のBid合計数量を取得する"""
        return sum(size for _, size, _ in self.get_bids(n))

    def get_total_ask_volume(self, n: int = 10) -> int:
        """上位N段のAsk合計数量を取得する"""
        return sum(size for _, size, _ in self.get_asks(n))

    def get_book_imbalance(self, n: int = 10) -> float:
        """
        板のインバランス（買い圧力と売り圧力の比率）を計算する。

        Returns:
            float: -1（完全売り優勢）から+1（完全買い優勢）の範囲
        """
        bid_vol = self.get_total_bid_volume(n)
        ask_vol = self.get_total_ask_volume(n)
        total = bid_vol + ask_vol
        if total == 0:
            return 0.0
        return (bid_vol - ask_vol) / total

    def to_numpy(self, n_levels: int = 10) -> Optional["np.ndarray"]:
        """
        板情報をNumpy配列に変換する。

        配列形式: [bid_price_1, bid_size_1, bid_count_1,
                   ask_price_1, ask_size_1, ask_count_1,
                   bid_price_2, bid_size_2, bid_count_2,
                   ..., mid_price, spread, imbalance]

        Args:
            n_levels: 含める価格レベル数

        Returns:
            np.ndarray: (n_levels * 6 + 3,) 形状の配列、
                        numpyが未インストールの場合はNone
        """
        if not HAS_NUMPY:
            logger.warning("numpyが利用できません。pip install numpy を実行してください。")
            return None

        bids = self.get_bids(n_levels)
        asks = self.get_asks(n_levels)

        # 配列サイズ: n_levels * 6 (bid3 + ask3 per level) + 3 (mid, spread, imb)
        arr = np.zeros(n_levels * 6 + 3, dtype=np.float32)

        # 板データの充填
        for i, (price, size, count) in enumerate(bids):
            offset = i * 6
            arr[offset] = price
            arr[offset + 1] = float(size)
            arr[offset + 2] = float(count)

        for i, (price, size, count) in enumerate(asks):
            offset = i * 6 + 3
            arr[offset] = price
            arr[offset + 1] = float(size)
            arr[offset + 2] = float(count)

        # メタ指標
        mid = self.get_mid_price()
        spread = self.get_spread()
        imbalance = self.get_book_imbalance(n_levels)

        arr[-3] = mid if mid is not None else 0.0
        arr[-2] = spread if spread is not None else 0.0
        arr[-1] = imbalance

        return arr

    def clear(self) -> None:
        """板情報をすべてクリアする"""
        self._orders.clear()
        self._bid_levels.clear()
        self._ask_levels.clear()
        self._bid_order_counts.clear()
        self._ask_order_counts.clear()
        self._update_count = 0

    @property
    def order_count(self) -> int:
        """現在の注文数"""
        return len(self._orders)

    @property
    def bid_level_count(self) -> int:
        """Bidの価格レベル数"""
        return len(self._bid_levels)

    @property
    def ask_level_count(self) -> int:
        """Askの価格レベル数"""
        return len(self._ask_levels)

    @property
    def update_count(self) -> int:
        """累計更新回数"""
        return self._update_count

    def __repr__(self) -> str:
        best_bid = self.get_best_bid()
        best_ask = self.get_best_ask()
        spread = self.get_spread()
        return (
            f"OrderBook(symbol={self.symbol!r}, "
            f"orders={self.order_count}, "
            f"bid_levels={self.bid_level_count}, "
            f"ask_levels={self.ask_level_count}, "
            f"best_bid={best_bid}, "
            f"best_ask={best_ask}, "
            f"spread={spread})"
        )
