import signal
import asyncio
import uuid
import warnings
import nexuslog as logging

from typing import Callable, Coroutine, Any, TypeVar, Union, cast
from typing import Dict, List
from dataclasses import dataclass
from nexustrader.core.nautilius_core import LiveClock
from nexustrader.schema import (
    Kline,
    BookL1,
    Trade,
    FundingRate,
    BookL2,
    IndexPrice,
    MarkPrice,
)

T = TypeVar("T")
InputDataType = Union[
    Kline, BookL1, Trade, FundingRate, BookL2, IndexPrice, IndexPrice, MarkPrice
]


class OidGen:
    __slots__ = ("_shard", "_last_ms", "_seq", "_clock")

    def __init__(self, clock: LiveClock):
        self._shard = self._generate_shard()
        self._last_ms = 0
        self._seq = 0
        self._clock = clock

    def _generate_shard(self) -> int:
        uuid_int = cast(int, uuid.uuid4().int)
        shard = (uuid_int % 9999) + 1
        return shard

    @property
    def oid(self) -> str:
        ms = self._clock.timestamp_ms()
        if ms == self._last_ms:
            self._seq = (self._seq + 1) % 1000
            if self._seq == 0:
                while True:
                    ms2 = self._clock.timestamp_ms()
                    if ms2 > ms:
                        ms = ms2
                        break
        else:
            self._seq = 0
        self._last_ms = ms
        return f"{ms:013d}{self._seq:03d}{self._shard:04d}"

    def get_shard(self) -> int:
        return self._shard


def is_redis_available() -> bool:
    """Check if Redis dependencies and server are available"""
    try:
        # Check if Redis Python packages are installed
        import redis
        import socket
        from nexustrader.constants import get_redis_config

        # Check if Redis server is accessible
        try:
            # Get Redis config and test connection
            redis_config = get_redis_config()
            client = redis.Redis(**redis_config)
            client.ping()  # This will raise an exception if Redis is not accessible
            client.close()
            return True
        except (redis.ConnectionError, redis.TimeoutError, socket.error, Exception):
            return False

    except ImportError:
        return False


def get_redis_client_if_available():
    import redis

    """Get Redis client if available, otherwise return None"""
    if not is_redis_available():
        return None

    try:
        from nexustrader.constants import get_redis_config

        redis_config = get_redis_config()
        return redis.Redis(**redis_config)
    except Exception:
        return None


@dataclass
class RateLimit:
    """
    max_rate: Allow up to max_rate / time_period acquisitions before blocking.
    time_period: Time period in seconds.
    """

    max_rate: float
    time_period: float = 60


class TaskManager:
    def __init__(
        self,
        loop: asyncio.AbstractEventLoop,
        enable_signal_handlers: bool = True,
        cancel_timeout: float | None = 5.0,
    ):
        self._log = logging.getLogger(name=type(self).__name__)
        self._tasks: Dict[str, asyncio.Task] = {}
        self._shutdown_event = asyncio.Event()
        self._loop = loop
        self._cancel_timeout = cancel_timeout
        if enable_signal_handlers:
            self._setup_signal_handlers()

    def _setup_signal_handlers(self):
        try:
            for sig in (signal.SIGINT, signal.SIGTERM):
                self._loop.add_signal_handler(
                    sig, lambda: self.create_task(self._shutdown())
                )
        except NotImplementedError:
            warnings.warn("Signal handlers not supported on this platform")

    async def _shutdown(self):
        self._shutdown_event.set()
        self._log.debug("Shutdown signal received, cleaning up...")

    def create_task(
        self, coro: Coroutine[Any, Any, Any], name: str | None = None
    ) -> asyncio.Task:
        task = asyncio.create_task(coro, name=name)
        self._tasks[task.get_name()] = task
        task.add_done_callback(self._handle_task_done)
        return task

    def run_sync(self, coro: Coroutine[Any, Any, T]) -> T:
        """
        Run an async coroutine in a synchronous context.

        Args:
            coro: The coroutine to run

        Returns:
            The result of the coroutine

        Raises:
            RuntimeError: If the event loop is not running and cannot be started
            Exception: Any exception raised by the coroutine
        """
        try:
            if self._loop.is_running():
                future = asyncio.run_coroutine_threadsafe(coro, self._loop)
                return future.result()
            else:
                if self._loop.is_closed():
                    raise RuntimeError("Event loop is closed")
                return self._loop.run_until_complete(coro)
        except asyncio.CancelledError:
            raise RuntimeError("Coroutine was cancelled")
        except Exception as e:
            self._log.error(f"Error running coroutine: {e}")
            raise

    def cancel_task(self, name: str) -> bool:
        if name in self._tasks:
            self._tasks[name].cancel()
            return True
        return False

    def _handle_task_done(self, task: asyncio.Task):
        try:
            name = task.get_name()
            self._tasks.pop(name, None)
            task.result()
        except asyncio.CancelledError:
            pass
        except Exception as e:
            self._log.error(f"Error during task done: {e}")
            raise

    async def wait(self):
        try:
            if self._tasks:
                await self._shutdown_event.wait()
        except Exception as e:
            self._log.error(f"Error during wait: {e}")
            raise

    async def cancel(self, timeout: float | None = None):
        try:
            # Snapshot tasks to avoid dict mutation during iteration
            tasks = [t for t in self._tasks.values() if not t.done()]

            for task in tasks:
                task.cancel()

            if tasks:
                gather_coro = asyncio.gather(*tasks, return_exceptions=True)
                try:
                    effective_timeout = (
                        self._cancel_timeout if timeout is None else timeout
                    )
                    if effective_timeout is not None:
                        results = await asyncio.wait_for(
                            gather_coro, timeout=effective_timeout
                        )
                    else:
                        results = await gather_coro
                except asyncio.TimeoutError:
                    self._log.warning(
                        "Cancellation timed out; some tasks may still be running"
                    )
                else:
                    for result in results:
                        if isinstance(result, Exception) and not isinstance(
                            result, asyncio.CancelledError
                        ):
                            self._log.error(
                                f"Task failed during cancellation: {result}"
                            )

        except Exception as e:
            self._log.error(f"Error during cancellation: {e}")
            raise
        finally:
            self._tasks.clear()


# class Clock:
#     def __init__(self, tick_size: float = 1.0):
#         """
#         :param tick_size_s: Time interval of each tick in seconds (supports sub-second precision).
#         """
#         self._tick_size = tick_size  # Tick size in seconds
#         self._current_tick = (time.time() // self._tick_size) * self._tick_size
#         self._clock = LiveClock()
#         self._tick_callbacks: List[Callable[[float], None]] = []
#         self._started = False

#     @property
#     def tick_size(self) -> float:
#         return self._tick_size

#     @property
#     def current_timestamp(self) -> float:
#         return self._clock.timestamp()

#     def add_tick_callback(self, callback: Callable[[float], None]):
#         """
#         Register a callback to be called on each tick.
#         :param callback: Function to be called with current_tick as argument.
#         """
#         self._tick_callbacks.append(callback)

#     async def run(self):
#         if self._started:
#             raise RuntimeError("Clock is already running.")
#         self._started = True
#         while True:
#             now = time.time()
#             next_tick_time = self._current_tick + self._tick_size
#             sleep_duration = next_tick_time - now
#             if sleep_duration > 0:
#                 await asyncio.sleep(sleep_duration)
#             else:
#                 # If we're behind schedule, skip to the next tick to prevent drift
#                 next_tick_time = now
#             self._current_tick = next_tick_time
#             for callback in self._tick_callbacks:
#                 if asyncio.iscoroutinefunction(callback):
#                     await callback(self.current_timestamp)
#                 else:
#                     callback(self.current_timestamp)


class ZeroMQSignalRecv:
    def __init__(self, config, callback: Callable, task_manager: TaskManager):
        self._socket = config.socket
        self._callback = callback
        self._task_manager = task_manager

    async def _recv(self):
        while True:
            date = await self._socket.recv()
            if asyncio.iscoroutinefunction(self._callback):
                await self._callback(date)
            else:
                self._callback(date)

    async def start(self):
        self._task_manager.create_task(self._recv())


# class MovingAverage:
#     """
#     Calculate moving median or mean using a sliding window.

#     Args:
#         length: Length of the sliding window
#         method: 'median' or 'mean' calculation method
#     """

#     def __init__(self, length: int, method: Literal["median", "mean"] = "mean"):
#         if method not in ["median", "mean"]:
#             raise ValueError("method must be either 'median' or 'mean'")

#         self._length = length
#         self._method = method
#         self._window = deque(maxlen=length)
#         self._calc_func = median if method == "median" else mean

#     def input(self, value: float) -> float | None:
#         """
#         Input a new value and return the current median/mean.

#         Args:
#             value: New value to add to sliding window

#         Returns:
#             Current median/mean value, or None if window not filled
#         """
#         self._window.append(value)

#         if len(self._window) < self._length:
#             return None

#         return self._calc_func(self._window)


class DataReady:
    def __init__(
        self,
        symbols: List[str],
        name: str,
        clock: LiveClock,
        timeout: int = 60,
        permanently_ready: bool = False,
    ):
        """
        Initialize DataReady class

        Args:
            symbols: symbols list need to monitor
            timeout: timeout in seconds
        """
        self._log = logging.getLogger(name=type(self).__name__)

        # Optimization: Store symbols as a set for faster lookups if needed,
        # but dict is fine for tracking status.
        self._symbols_status = {symbol: False for symbol in symbols}
        self._total_symbols = len(symbols)
        self._ready_symbols_count = 0

        self._timeout_ms = timeout * 1000  # Store timeout in ms
        self._clock = clock
        self._first_data_time: int | None = None
        self._name = name
        # Optimization: A flag to indicate that the "ready" state is final
        # (either all symbols received or timed out).
        self._is_permanently_ready: bool = permanently_ready

        if not symbols:  # If no symbols to monitor, it's ready immediately (or upon first data for timestamp)
            # We'll consider it ready once _first_data_time is set, which requires at least one input call
            # Or, we can decide it's ready right away if that's the desired behavior.
            # For now, let's assume if symbols is empty, it becomes ready when `ready` is first checked
            # after _first_data_time is set, or on timeout.
            # Alternatively, one could set self._is_permanently_ready = True here.
            # The current logic for _ready_symbols_count == _total_symbols (0 == 0) will handle this.
            self._is_permanently_ready = True

    def input(self, data: InputDataType) -> None:
        """
        Input data, update the status of the corresponding symbol

        Args:
            data: data object with symbol attribute
        """
        # Optimization: If already permanently ready, do nothing.
        if self._is_permanently_ready:
            return

        symbol = data.symbol

        if self._first_data_time is None:
            self._first_data_time = self._clock.timestamp_ms()

        # Process only if the symbol is one we are monitoring and it's not yet marked ready.
        if symbol in self._symbols_status and not self._symbols_status[symbol]:
            self._symbols_status[symbol] = True
            self._ready_symbols_count += 1

            # Optimization: Check if all symbols are now ready.
            if self._ready_symbols_count == self._total_symbols:
                self._log.debug(
                    f"All {self._total_symbols} symbols received. {self._name} is ready."
                )
                self._is_permanently_ready = True
                # No need to call self.ready here, the flag is enough.

    def add_symbols(self, symbols: str | list[str]) -> None:
        """
        Add a new symbol to monitor

        Args:
            symbol: symbol to add
        """
        if isinstance(symbols, str):
            symbols = [symbols]

        for symbol in symbols:
            if symbol not in self._symbols_status:
                self._symbols_status[symbol] = False
                self._total_symbols += 1
                # If we were already permanently ready, adding a new symbol means we are no longer ready.
                if self._is_permanently_ready:
                    self._is_permanently_ready = False

    @property
    def ready(self) -> bool:
        """
        Check if all data is ready or if it has timed out

        Returns:
            bool: if all data is ready or timed out, return True
        """
        # Optimization: If already determined to be permanently ready, return True.
        if self._is_permanently_ready:
            return True

        if self._first_data_time is None:
            # No data received yet, so not ready (unless symbols list was empty and we decided that's ready from init)
            return False  # Or handle empty symbols list case from __init__

        # Check if all symbols have been received (could have been set by input)
        if self._ready_symbols_count == self._total_symbols:
            # This ensures that even if input didn't set _is_permanently_ready
            # (e.g. if ready is called before next input after last symbol arrived),
            # it's correctly identified.
            if (
                not self._is_permanently_ready
            ):  # To avoid repeated logging if already logged by input
                self._log.debug(
                    f"All {self._total_symbols} symbols confirmed ready by property access."
                )
            self._is_permanently_ready = True
            return True

        # Check for timeout
        if self._clock.timestamp_ms() - self._first_data_time > self._timeout_ms:
            # Only issue warning if not all symbols were ready before timeout
            if self._ready_symbols_count < self._total_symbols:
                not_ready_symbols = [
                    symbol
                    for symbol, status in self._symbols_status.items()
                    if not status
                ]
                if not_ready_symbols:  # Should always be true if count < total
                    warnings.warn(
                        f"Data receiving timed out. Timeout: {self._timeout_ms / 1000}s. "
                        f"Received {self._ready_symbols_count}/{self._total_symbols} symbols. "
                        f"The following symbols are not ready: {', '.join(not_ready_symbols)}"
                    )
            else:  # All symbols were received, but timeout check ran.
                self._log.debug(
                    f"Timeout checked, but all symbols were already received. {self._name} is ready."
                )

            self._is_permanently_ready = True  # Timed out, so state is now final.
            return True

        return False  # Not all symbols ready and not timed out yet.
