"""
Strategy Configuration Model

Provides the StrategyConfig class for complete strategy configuration.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Union, overload
from uuid import UUID

import polars as pl
from pydantic import BaseModel, ConfigDict, Field, field_serializer, field_validator

from ..enums import Freq
from ..indicators import PolarsExprField, PolarsExprType
from .blueprint import Blueprint
from .data_source import DataSource
from .indicator_spec import IndicatorSpec
from .params import StrategyParams


class StrategyConfig(BaseModel):
    """完整策略配置"""

    id: Optional[UUID] = Field(None, description="策略 ID（由資料庫自動填充）")
    name: str = Field(..., description="策略名稱")
    base_instrument: str = Field(..., description="基礎交易標的")
    base_instrument_id: Optional[int] = Field(
        None, description="基礎交易標的 ID（由 Gateway 自動填充）"
    )
    base_freq: Freq = Field(..., description="基礎頻率")
    note: str = Field(default="", description="備註")
    tz: str = Field(
        default="UTC",
        description="策略時區（IANA 時區字串），用於將 OHLCV ts 從 UTC 轉為指定時區。預設 UTC。",
    )

    params: Optional[StrategyParams] = Field(
        None,
        description="策略參數（用於追溯）",
        exclude=True,
    )

    volatility_indicator: Optional[PolarsExprType] = Field(
        None,
        description="波動率欄位表達式，如 pl.col('1D_ATR|14') 或 pl.col('profile').struct.field('shape')",
    )
    indicators: List[IndicatorSpec] = Field(default_factory=list, description="指標列表")
    data_sources: List[DataSource] = Field(
        default_factory=list,
        description="Extra (instrument, freq) pairs to load from DB for cross-instrument indicators",
    )

    base_blueprint: Blueprint = Field(..., description="基礎藍圖")
    advanced_blueprints: List[Blueprint] = Field(default_factory=list, description="進階藍圖列表")

    @field_validator("volatility_indicator", mode="before")
    @classmethod
    def validate_volatility_indicator(cls, v: Any) -> Optional[pl.Expr]:
        """自動轉換 volatility_indicator 為 pl.Expr

        支援：
        - None / null
        - 字串（如 "1D_ATR|14"）→ 自動轉為 pl.col("1D_ATR|14")
        - 完整 Expr JSON（如 {"Column": "..."}）
        """
        if v is None:
            return None
        return PolarsExprField.deserialize(v)

    @field_serializer("volatility_indicator")
    def serialize_volatility_indicator(self, expr: Optional[pl.Expr]) -> Optional[dict]:
        """序列化 volatility_indicator 為 dict（與服務器格式一致）"""
        if expr is None:
            return None
        return PolarsExprField.serialize(expr)

    model_config = ConfigDict(
        arbitrary_types_allowed=True  # 允許 pl.Expr 這種自定義類型
    )

    @classmethod
    def from_api(cls, api_response: Union[Dict[str, Any], str]) -> "StrategyConfig":
        """從 API 響應創建策略配置

        Args:
            api_response: API 返回的 JSON 數據（dict 或 JSON 字符串）

        Returns:
            StrategyConfig 實例，conditions 和 price_expr 自動轉為 pl.Expr

        Example:
            >>> # 從 JSON 字符串
            >>> strategy = StrategyConfig.from_api(json_str)
            >>> # 從 dict
            >>> strategy = StrategyConfig.from_api(response.json())
            >>> # 直接使用
            >>> expr = strategy.base_blueprint.entry_triggers[0].conditions[0]
            >>> print(type(expr))  # <class 'polars.expr.expr.Expr'>
        """
        if isinstance(api_response, str):
            return cls.model_validate_json(api_response)
        else:
            return cls.model_validate(api_response)

    def to_json(self, indent: int = 2) -> str:
        """序列化為 JSON 字符串

        自動將 pl.Expr 轉為 JSON 字符串格式

        Returns:
            JSON 字符串，conditions 和 price_expr 已序列化
        """
        return self.model_dump_json(indent=indent, exclude_none=True).replace("\n", "")

    def to_dict(self) -> Dict[str, Any]:
        """轉換為字典（用於發送到 API）

        Returns:
            字典，pl.Expr 已序列化為 JSON 字符串
        """
        return self.model_dump(exclude_none=True)

    def save(self, filepath: str) -> None:
        """保存到 JSON 文件"""
        with open(filepath, "w", encoding="utf-8") as f:
            f.write(self.to_json())
        print(f"✅ 策略已保存到: {filepath}")

    @classmethod
    def load(cls, filepath: str) -> "StrategyConfig":
        """從 JSON 文件加載"""
        with open(filepath, "r", encoding="utf-8") as f:
            return cls.from_api(f.read())

    # ========== Advanced Blueprint Index Properties ==========

    @property
    def adv_bp_map(self) -> Dict[str, Blueprint]:
        """Blueprint name -> Blueprint mapping for O(1) lookup.

        Returns:
            Dict mapping blueprint names to Blueprint instances

        Example:
            >>> config.adv_bp_map["L_fav_0.3"]  # Get blueprint by name
        """
        return {bp.name: bp for bp in self.advanced_blueprints}

    @property
    def adv_bp_summary(self) -> List[Dict[str, Any]]:
        """Quick overview of advanced blueprints.

        Returns:
            List of dicts with idx, name, and direction for each blueprint

        Example:
            >>> for bp in config.adv_bp_summary:
            ...     print(f"{bp['idx']}: {bp['name']} ({bp['direction']})")
        """
        return [
            {"idx": i, "name": bp.name, "direction": bp.direction.value}
            for i, bp in enumerate(self.advanced_blueprints)
        ]

    def get_adv_bp(self, selector: Union[int, str]) -> Blueprint:
        """Get advanced blueprint by index or name.

        Args:
            selector: Index (int) or name (str)

        Returns:
            Blueprint instance

        Raises:
            KeyError: If name not found
            IndexError: If index out of range

        Example:
            >>> bp = config.get_adv_bp(0)       # By index
            >>> bp = config.get_adv_bp("L_fav_0.3")  # By name
        """
        if isinstance(selector, int):
            return self.advanced_blueprints[selector]
        return self.adv_bp_map[selector]

    def with_adv_blueprints(self, blueprints: List[Blueprint]) -> "StrategyConfig":
        """Replace advanced_blueprints and return a new StrategyConfig.

        Args:
            blueprints: List of Blueprint instances to set

        Returns:
            New StrategyConfig with advanced_blueprints replaced

        Example:
            >>> from tradepose_client.builder import BlueprintBuilder, frange
            >>>
            >>> # Generate blueprints using sweep
            >>> bps = BlueprintBuilder.sweep_delay_entry(
            ...     direction="Long",
            ...     entry_type="favorable",
            ...     distance_expr=atr.col(),
            ...     multipliers=frange(0.1, 0.5, 0.1),
            ... )
            >>>
            >>> # Replace and get new config
            >>> new_config = base_config.with_adv_blueprints(bps)
        """
        return self.model_copy(update={"advanced_blueprints": blueprints})

    # ========== Indicator Access ==========

    @property
    def indicator_map(self) -> Dict[str, IndicatorSpec]:
        """display_name -> IndicatorSpec mapping.

        Returns:
            Dict mapping display_name to IndicatorSpec

        Example:
            >>> config.indicator_map["1D_ATR|14"]
        """
        return {spec.display_name(): spec for spec in self.indicators}

    @overload
    def get_indicator(self) -> Dict[str, IndicatorSpec]: ...

    @overload
    def get_indicator(self, name: str) -> IndicatorSpec: ...

    def get_indicator(
        self, name: Optional[str] = None
    ) -> Union[Dict[str, IndicatorSpec], IndicatorSpec]:
        """Get indicator(s) by display_name.

        Args:
            name: display_name string (optional)

        Returns:
            If name is None: Dict[str, IndicatorSpec] mapping display_name -> IndicatorSpec
            If name is provided: The specific IndicatorSpec

        Raises:
            KeyError: If name is provided but not found

        Example:
            >>> # Get all indicators as dict
            >>> indicators = config.get_indicator()
            >>> # {'1D_ATR|14': IndicatorSpec(...), '4h_ST|2.0x_...': IndicatorSpec(...)}
            >>>
            >>> # Get specific indicator
            >>> atr = config.get_indicator("1D_ATR|14")
            >>> atr.col()  # pl.col("1D_ATR|14")
        """
        indicator_map = self.indicator_map
        if name is None:
            return indicator_map
        if name not in indicator_map:
            available = ", ".join(sorted(indicator_map.keys()))
            raise KeyError(f"No indicator with display_name '{name}'. Available: {available}")
        return indicator_map[name]
