"""
ZeroMQ SUBレシーバーモジュール

ZeroMQ SUBソケットでWindows側パブリッシャーからの
MBO/Tradeイベントを非同期で高速受信する。
コールバックベースのイベントディスパッチ機能を提供。
"""

import json
import logging
import threading
import time
from collections import deque
from typing import Callable, Optional, List

import zmq
from rich.console import Console

from ..config import BridgeConfig
from ..models import (
    MBOEvent, TradeEvent, HeartbeatEvent, parse_event,
    TOPIC_MBO, TOPIC_TRADE, TOPIC_HEARTBEAT, TOPIC_ALL,
    DataType,
)

logger = logging.getLogger(__name__)
console = Console()

# コールバック型定義
MBOCallback = Callable[[MBOEvent], None]
TradeCallback = Callable[[TradeEvent], None]
HeartbeatCallback = Callable[[HeartbeatEvent, float], None]  # (event, latency_ms)


class ZmqReceiver:
    """
    ZeroMQ SUBソケットを管理するレシーバー。

    パブリッシャーからのメッセージを受信し、
    登録されたコールバックにディスパッチする。
    """

    def __init__(self, config: BridgeConfig):
        """
        Args:
            config: ブリッジ設定オブジェクト
        """
        self.config = config
        # ZeroMQコンテキストとソケット
        self._context: Optional[zmq.Context] = None
        self._socket: Optional[zmq.Socket] = None
        # レシーバースレッド
        self._thread: Optional[threading.Thread] = None
        # 停止フラグ
        self._running = threading.Event()
        # コールバック登録リスト
        self._mbo_callbacks: List[MBOCallback] = []
        self._trade_callbacks: List[TradeCallback] = []
        self._heartbeat_callbacks: List[HeartbeatCallback] = []
        # 統計情報
        self._stats = {
            "mbo_count": 0,
            "trade_count": 0,
            "heartbeat_count": 0,
            "errors": 0,
            "reconnects": 0,
            "start_time": 0.0,
        }
        # レイテンシ計測（直近20件の移動平均）
        self._latency_window: deque = deque(maxlen=20)
        # パブリッシャー接続状態
        self._last_heartbeat_time: float = 0.0
        self._heartbeat_timeout = 5.0  # 5秒以上で切断判定
        self._last_heartbeat_seq: int = 0
        self._missed_heartbeats: int = 0
        # 自動再接続設定
        self._auto_reconnect = True
        self._reconnect_interval = 5.0  # 再接続試行間隔（秒）
        self._reconnect_max_interval = 60.0  # 最大再接続間隔（指数バックオフ）
        self._reconnect_backoff = 1.5  # バックオフ係数
        # start時のトピック保持（再接続時に再購読するため）
        self._subscribed_topics: Optional[List[bytes]] = None
        # 再接続時コールバック（OrderBookリセット等）
        self._on_reconnect_callbacks: List[Callable[[], None]] = []

    def on_mbo(self, callback: MBOCallback) -> None:
        """
        MBOイベントのコールバックを登録する。

        Args:
            callback: MBOEventを受け取るコールバック関数
        """
        self._mbo_callbacks.append(callback)

    def on_trade(self, callback: TradeCallback) -> None:
        """
        Tradeイベントのコールバックを登録する。

        Args:
            callback: TradeEventを受け取るコールバック関数
        """
        self._trade_callbacks.append(callback)

    def on_heartbeat(self, callback: HeartbeatCallback) -> None:
        """
        Heartbeatイベントのコールバックを登録する。

        コールバックには HeartbeatEvent とレイテンシ（ミリ秒）が渡される。

        Args:
            callback: (HeartbeatEvent, latency_ms) を受け取るコールバック関数
        """
        self._heartbeat_callbacks.append(callback)

    def on_reconnect(self, callback: Callable[[], None]) -> None:
        """
        再接続時のコールバックを登録する。

        OrderBookのclear()やTradeAnalyzerのclear()など、
        再接続時にリセットが必要な処理を登録する。

        Args:
            callback: 再接続時に実行するコールバック関数
        """
        self._on_reconnect_callbacks.append(callback)

    def start(
        self,
        topics: Optional[List[bytes]] = None,
    ) -> None:
        """
        レシーバーを開始する。

        Args:
            topics: 購読するトピックリスト (Noneの場合は全トピック)
        """
        if self._running.is_set():
            logger.warning("レシーバーは既に実行中です")
            return

        connect_addr = self.config.get_sub_connect_addr()
        logger.info(f"ZeroMQ SUBソケットを {connect_addr} に接続中...")

        # ZeroMQコンテキストとソケットの初期化
        self._context = zmq.Context()
        self._socket = self._context.socket(zmq.SUB)
        # 高水位マーク（HWM）の設定
        self._socket.setsockopt(zmq.RCVHWM, self.config.zmq.hwm)
        # 受信タイムアウト設定
        self._socket.setsockopt(zmq.RCVTIMEO, self.config.zmq.recv_timeout)

        # トピックフィルタの設定
        if topics is None:
            # 全トピックを購読
            self._socket.setsockopt(zmq.SUBSCRIBE, TOPIC_ALL)
        else:
            for topic in topics:
                self._socket.setsockopt(zmq.SUBSCRIBE, topic)

        # 接続
        self._socket.connect(connect_addr)

        # トピック保持（再接続時に再購読するため）
        self._subscribed_topics = topics

        # レシーバースレッド開始
        self._running.set()
        self._stats["start_time"] = time.time()
        self._thread = threading.Thread(
            target=self._receive_loop,
            name="zmq-receiver",
            daemon=True,
        )
        self._thread.start()

        console.print(
            f"[bold green]✓ ZeroMQ SUBレシーバー開始[/] - {connect_addr}",
        )

    def stop(self) -> None:
        """レシーバーを停止する"""
        if not self._running.is_set():
            return

        logger.info("レシーバーを停止中...")
        self._running.clear()

        # スレッド終了を待機
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=5.0)

        # ソケットとコンテキストのクリーンアップ
        if self._socket:
            self._socket.close()
            self._socket = None
        if self._context:
            self._context.term()
            self._context = None

        # 統計情報表示
        self._print_stats()
        console.print("[bold yellow]■ ZeroMQ SUBレシーバー停止[/]")

    def _receive_loop(self) -> None:
        """
        受信ループ（別スレッドで実行）。

        ZeroMQ SUBソケットからマルチパートメッセージを受信し、
        登録されたコールバックにディスパッチする。
        """
        logger.info("受信ループ開始")

        while self._running.is_set():
            try:
                # マルチパートメッセージの受信
                # [トピック, JSONデータ]
                parts = self._socket.recv_multipart()

                if len(parts) != 2:
                    logger.warning(f"不正なメッセージ形式: {len(parts)} parts")
                    self._stats["errors"] += 1
                    continue

                topic = parts[0]
                data = parts[1].decode("utf-8")

                # トピックに基づいてイベントをパースしディスパッチ
                if topic == TOPIC_MBO:
                    event = MBOEvent.from_json(data)
                    self._stats["mbo_count"] += 1
                    for cb in self._mbo_callbacks:
                        try:
                            cb(event)
                        except Exception as e:
                            logger.error(f"MBOコールバックエラー: {e}")
                            self._stats["errors"] += 1

                elif topic == TOPIC_TRADE:
                    event = TradeEvent.from_json(data)
                    self._stats["trade_count"] += 1
                    for cb in self._trade_callbacks:
                        try:
                            cb(event)
                        except Exception as e:
                            logger.error(f"Tradeコールバックエラー: {e}")
                            self._stats["errors"] += 1

                else:
                    # Heartbeatトピックの処理
                    if topic == TOPIC_HEARTBEAT:
                        self._process_heartbeat(data)
                    else:
                        # 不明なトピック - data_typeで判別を試みる
                        try:
                            event = parse_event(data)
                            if isinstance(event, MBOEvent):
                                self._stats["mbo_count"] += 1
                                for cb in self._mbo_callbacks:
                                    cb(event)
                            elif isinstance(event, TradeEvent):
                                self._stats["trade_count"] += 1
                                for cb in self._trade_callbacks:
                                    cb(event)
                            elif isinstance(event, HeartbeatEvent):
                                self._process_heartbeat(data)
                        except Exception as e:
                            logger.warning(f"不明なトピック: {topic}, エラー: {e}")
                            self._stats["errors"] += 1

            except zmq.Again:
                # タイムアウト - Heartbeat途絶チェック
                if self._auto_reconnect and self._should_reconnect():
                    self._do_reconnect()
                continue
            except zmq.ZMQError as e:
                if self._running.is_set():
                    logger.error(f"ZeroMQ受信エラー: {e}")
                    self._stats["errors"] += 1
                    # 接続エラー時に再接続試行
                    if self._auto_reconnect:
                        self._do_reconnect()
            except Exception as e:
                logger.error(f"受信エラー: {e}")
                self._stats["errors"] += 1

        logger.info("受信ループ終了")

    def _should_reconnect(self) -> bool:
        """
        再接続が必要かどうか判定する。

        Heartbeatを過去に受信したことがあり、
        かつ現在タイムアウトしている場合に再接続が必要。
        """
        # まだHeartbeatを受信したことがない場合は、最初の接続待ちなので再接続不要
        if self._last_heartbeat_time == 0.0:
            return False
        # Heartbeatタイムアウトが発生している場合に再接続
        return not self.is_publisher_connected

    def _do_reconnect(self) -> None:
        """
        ソケットを再接続する。

        指数バックオフ付きのリトライで安定的に再接続を試みる。
        再接続成功時には購読トピックを復元し、
        登録されたリセットコールバックを実行する。
        """
        if not self._running.is_set():
            return

        self._stats["reconnects"] += 1
        connect_addr = self.config.get_sub_connect_addr()
        console.print(
            f"[bold yellow]⚠ パブリッシャー接続断 - 再接続試行 #{self._stats['reconnects']}[/]"
        )
        logger.warning(
            f"再接続試行 #{self._stats['reconnects']}: {connect_addr}"
        )

        try:
            # 既存ソケットをクローズ
            if self._socket:
                self._socket.close(linger=0)
                self._socket = None

            # バックオフ待機
            backoff = min(
                self._reconnect_interval
                * (self._reconnect_backoff ** (self._stats["reconnects"] - 1)),
                self._reconnect_max_interval,
            )
            logger.info(f"再接続まで {backoff:.1f}秒待機...")
            # 待機中も停止フラグを確認
            for _ in range(int(backoff * 10)):
                if not self._running.is_set():
                    return
                time.sleep(0.1)

            # 新しいソケットを作成して接続
            self._socket = self._context.socket(zmq.SUB)
            self._socket.setsockopt(zmq.RCVHWM, self.config.zmq.hwm)
            self._socket.setsockopt(zmq.RCVTIMEO, self.config.zmq.recv_timeout)

            # トピック再購読
            if self._subscribed_topics is None:
                self._socket.setsockopt(zmq.SUBSCRIBE, TOPIC_ALL)
            else:
                for topic in self._subscribed_topics:
                    self._socket.setsockopt(zmq.SUBSCRIBE, topic)

            self._socket.connect(connect_addr)

            # 状態リセット
            self._last_heartbeat_time = 0.0
            self._last_heartbeat_seq = 0
            self._latency_window.clear()

            # 再接続コールバック実行（OrderBookリセット等）
            for cb in self._on_reconnect_callbacks:
                try:
                    cb()
                except Exception as e:
                    logger.error(f"再接続コールバックエラー: {e}")

            console.print(
                f"[bold green]✓ 再接続完了[/] - {connect_addr}"
            )
            logger.info(f"再接続完了: {connect_addr}")

        except Exception as e:
            logger.error(f"再接続失敗: {e}")
            self._stats["errors"] += 1

    def _print_stats(self) -> None:
        """統計情報をリッチコンソールで表示"""
        elapsed = time.time() - self._stats["start_time"]
        total = self._stats["mbo_count"] + self._stats["trade_count"]
        rate = total / elapsed if elapsed > 0 else 0

        from rich.table import Table

        table = Table(title="レシーバー統計")
        table.add_column("項目", style="cyan")
        table.add_column("値", style="green")
        table.add_row("MBOイベント数", str(self._stats["mbo_count"]))
        table.add_row("Tradeイベント数", str(self._stats["trade_count"]))
        table.add_row("合計", str(total))
        table.add_row("エラー数", str(self._stats["errors"]))
        table.add_row("稼働時間", f"{elapsed:.1f}秒")
        table.add_row("レート", f"{rate:.1f} msg/s")
        console.print(table)

    @property
    def is_running(self) -> bool:
        """レシーバーが実行中かどうか"""
        return self._running.is_set()

    @property
    def stats(self) -> dict:
        """現在の統計情報を返す（レイテンシ含む）"""
        result = dict(self._stats)
        result["latency_ms"] = self.latency_ms
        result["publisher_connected"] = self.is_publisher_connected
        result["missed_heartbeats"] = self._missed_heartbeats
        return result

    @property
    def latency_ms(self) -> float:
        """直近のレイテンシ（ミリ秒、移動平均）"""
        if not self._latency_window:
            return 0.0
        return sum(self._latency_window) / len(self._latency_window)

    @property
    def is_publisher_connected(self) -> bool:
        """パブリッシャーが接続中かどうか（Heartbeat基準）"""
        if self._last_heartbeat_time == 0.0:
            return False
        return (time.time() - self._last_heartbeat_time) < self._heartbeat_timeout

    def _process_heartbeat(self, data: str) -> None:
        """
        Heartbeatイベントを処理する。

        レイテンシの計測、シーケンス番号のギャップ検出、
        コールバックの呼び出しを行う。
        """
        try:
            event = HeartbeatEvent.from_json(data)
            recv_time = time.time()

            # レイテンシ計測 (ms)
            latency_ms = (recv_time - event.publisher_ts) * 1000.0
            self._latency_window.append(latency_ms)

            # Heartbeat受信時刻の更新
            self._last_heartbeat_time = recv_time
            self._stats["heartbeat_count"] += 1

            # シーケンス番号ギャップ検出（パケットロス）
            if self._last_heartbeat_seq > 0:
                gap = event.seq - self._last_heartbeat_seq - 1
                if gap > 0:
                    self._missed_heartbeats += gap
                    logger.warning(
                        f"Heartbeatギャップ検出: {gap}件のHeartbeatが欠落 "
                        f"(seq {self._last_heartbeat_seq} -> {event.seq})"
                    )
            self._last_heartbeat_seq = event.seq

            # コールバック呼び出し
            for cb in self._heartbeat_callbacks:
                try:
                    cb(event, latency_ms)
                except Exception as e:
                    logger.error(f"Heartbeatコールバックエラー: {e}")

        except Exception as e:
            logger.error(f"Heartbeat処理エラー: {e}")
            self._stats["errors"] += 1
