from dataclasses import dataclass, field
from typing import Dict, List, Literal
from nexustrader.constants import AccountType, ExchangeType, StorageType
from nexustrader.strategy import Strategy
from zmq.asyncio import Socket

LOG_LEVELS = Literal["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"]


@dataclass
class LogConfig:
    """LogConfig for NexusTrader logging system using nexuslog.

    Maps directly to nexuslog.basicConfig() parameters.

    Attributes:
        filename: Optional file path for log output. If None, logs to stdout.
        level: Minimum log level to record. Options: TRACE, DEBUG, INFO, WARNING, ERROR
        unix_ts: If True, emit unix timestamps instead of formatted local time.
    """

    filename: str | None = None
    level: LOG_LEVELS = "INFO"
    name_levels: dict[str | None, LOG_LEVELS] | None = None
    unix_ts: bool = False
    batch_size: int = 1

    def __post_init__(self):
        if self.level not in ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"]:
            raise ValueError(
                f"Invalid level: {self.level}. Must be one of TRACE, DEBUG, INFO, WARNING, ERROR."
            )

        if self.name_levels:
            for name, level in self.name_levels.items():
                if level not in ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR"]:
                    raise ValueError(
                        f"Invalid level: {level} for logger name: {name}. Must be one of TRACE, DEBUG, INFO, WARNING, ERROR."
                    )


@dataclass
class BasicConfig:
    """
    Basic configuration for the trading system.

    Attributes:
        api_key (str): API key for the exchange. For HyperLiquid, this is the real wallet address, not the address generated by the Agent.
        secret (str): Secret key for the exchange. For HyperLiquid, this is the Agent private key, you can get it at https://app.hyperliquid-testnet.xyz/API
        testnet (bool): Whether to use the testnet environment. Defaults to False.
        passphrase (str | None): Passphrase for the exchange, if required. Defaults to None.
    """

    api_key: str | None = None
    secret: str | None = None
    testnet: bool = False
    passphrase: str = None


@dataclass
class PublicConnectorConfig:
    account_type: AccountType
    enable_rate_limit: bool = True
    custom_url: str | None = None
    max_subscriptions_per_client: int | None = None
    max_clients: int | None = None


@dataclass
class PrivateConnectorConfig:
    """
    Configuration for private connector settings.

    Attributes
    ----------
    account_type : AccountType
        The type of account for the private connector.
    enable_rate_limit : bool, default=True
        Whether to enable rate limiting for API requests.
    max_retries for failed order request : int, default=0
    delay_initial_ms : int, default=100
    delay_max_ms : int, default=800
    backoff_factor : int, default=2
    max_slippage : float, default=0.02
        Maximum slippage allowed for market orders, expressed as a percentage (e.g., 0.02 for 2%). Only applicable for Bitget and HyperLiquid exchanges.
    """

    account_type: AccountType
    enable_rate_limit: bool = True
    max_retries: int = 0
    delay_initial_ms: int = 100
    delay_max_ms: int = 800
    backoff_factor: int = 2
    max_slippage: float = 0.02  # 2% slippage
    max_subscriptions_per_client: int | None = None
    max_clients: int | None = None


@dataclass
class ZeroMQSignalConfig:
    """ZeroMQ Signal Configuration Class.

    Used to configure the ZeroMQ subscriber socket to receive custom trade signals.

    Attributes:
        socket (`zmq.asyncio.Socket`): ZeroMQ asynchronous socket object

    Example:
        >>> from zmq.asyncio import Context
        >>> context = Context()
        >>> socket = context.socket(zmq.SUB)
        >>> socket.connect("ipc:///tmp/zmq_custom_signal")
        >>> socket.setsockopt(zmq.SUBSCRIBE, b"")
        >>> config = ZeroMQSignalConfig(socket=socket)
    """

    socket: Socket


@dataclass
class WebConfig:
    """Configuration for the optional FastAPI web server."""

    enabled: bool = False
    host: str = "0.0.0.0"
    port: int = 8000
    log_level: Literal["critical", "error", "warning", "info", "debug"] = "info"

    def __post_init__(self):
        if not (0 < self.port <= 65535):
            raise ValueError("port must be between 1 and 65535")


@dataclass
class MockConnectorConfig:
    initial_balance: Dict[str, float | int]
    account_type: AccountType
    fee_rate: float = 0.0005
    quote_currency: str = "USDT"
    overwrite_balance: bool = False
    overwrite_position: bool = False
    update_interval: int = 60
    leverage: float = 1.0

    def __post_init__(self):
        if not self.account_type.is_mock:
            raise ValueError(
                f"Invalid account type: {self.account_type} for mock connector. Must be `LINEAR_MOCK`, `INVERSE_MOCK`, or `SPOT_MOCK`."
            )


@dataclass
class Config:
    """
    Represents the configuration for a trading system.

    This class holds all necessary configuration information including strategy details,
    exchange configurations, connector setups, database settings, and logging preferences.

    Attributes:
        strategy_id (str): Unique identifier for the strategy.
        user_id (str): Identifier for the user running the strategy.
        strategy (Strategy): The trading strategy to be executed.
        basic_config (Dict[ExchangeType, BasicConfig]): Basic configuration for each exchange.
        public_conn_config (Dict[ExchangeType, List[PublicConnectorConfig]]): Public connector configurations by exchange.
        private_conn_config (Dict[ExchangeType, List[PrivateConnectorConfig | MockConnectorConfig]]):
            Private connector configurations by exchange, can include mock connectors.
        zero_mq_signal_config (ZeroMQSignalConfig | None): Configuration for ZeroMQ signal, if used.
        db_path (str): Path to the database file. Defaults to ".keys/cache.db".
        storage_backend (StorageType): Type of storage backend to use. Defaults to SQLITE.
        cache_sync_interval (int): Interval in seconds for cache synchronization. Defaults to 60.
        cache_expired_time (int): Time in seconds after which cache entries expire. Defaults to 3600.
        is_mock (bool): Flag indicating if the system is running in mock mode. Defaults to False.
        log_config (LogConfig): Configuration for logging. Defaults to a new LogConfig instance.
        enable_cli (bool): Flag to enable command-line interface. Defaults to False.
        web_config (WebConfig): Settings for the optional FastAPI web interface.
        flashduty_integration_key (str | None): FlashDuty integration key for push alerts. Defaults to None.
        exit_after_cancel (bool): Whether to cancel all open orders when the engine is disposed (e.g., on Ctrl+C). Defaults to True.

    Notes:
        The __post_init__ method enforces that you cannot mix mock and real private connectors.
        Either all private connectors must be mock or all must be real.
    """

    strategy_id: str
    user_id: str
    strategy: Strategy
    basic_config: Dict[ExchangeType, BasicConfig]
    public_conn_config: Dict[ExchangeType, List[PublicConnectorConfig]]
    private_conn_config: Dict[
        ExchangeType, List[PrivateConnectorConfig | MockConnectorConfig]
    ] = field(default_factory=dict)
    zero_mq_signal_config: ZeroMQSignalConfig | None = None
    db_path: str = ".keys/cache.db"
    storage_backend: StorageType = StorageType.SQLITE
    cache_sync_interval: int = 60
    cache_expired_time: int = 3600
    is_mock: bool = False
    log_config: LogConfig = field(default_factory=LogConfig)
    enable_cli: bool = False
    web_config: WebConfig = field(default_factory=WebConfig)
    exit_after_cancel: bool = True
    flashduty_integration_key: str | None = None

    def __post_init__(self):
        # Check if any connector is mock, then all must be mock
        has_mock = False
        has_private = False

        for connectors in self.private_conn_config.values():
            for connector in connectors:
                if isinstance(connector, MockConnectorConfig):
                    has_mock = True
                elif isinstance(connector, PrivateConnectorConfig):
                    has_private = True

                if has_mock and has_private:
                    raise ValueError(
                        "Cannot mix mock and real private connectors. Use either all mock or all private connectors."
                    )

        self.is_mock = has_mock
