"""
Blueprint Model

Provides the Blueprint class for strategy blueprints with entry/exit triggers.
"""

from typing import Any, List, Optional
from uuid import UUID

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

from ..enums import OrderStrategy, TradeDirection, TrendType
from ..indicators import PolarsExprField, PolarsExprType
from .trigger import Trigger

# Default sample size for historical trades
# IMPORTANT: sample_size 必須大於 expression 中的 window_size
#
# 原理說明：
# 1. ExpectedLossCalculator 會從資料庫 fetch sample_size 筆歷史交易
# 2. 套用 expected_loss_point_expr 計算（包含 rolling window 和 shift 操作）
# 3. 取最後一筆計算結果作為 expected_loss_point
#
# 如果 sample_size == window_size：
# - rolling_quantile(window_size=W) 只在最後一筆（index W-1）產生值
# - .shift(1) 將值向後移動，最後一筆變成 null
# - .last() 取得 null → 拋錯
#
# 正確設定範例：
# - window_size=10, sample_size=20 ✅ (fetch 20 筆，window 10，shift 後還有 10 個有效值)
# - window_size=10, sample_size=10 ❌ (shift 後最後一筆是 null)
#
# 建議：sample_size >= window_size * 2 以確保有足夠的資料緩衝
DEFAULT_EXPECTED_LOSS_SAMPLE_SIZE = 20

# Default expected loss point expression
# Formula: rolling quantile of mae normalized by volatility, scaled back by entry volatility
# Result: Expected loss in points (e.g., 30 pips, 100 points)
#
# 注意：此 expression 使用 window_size=10，因此 sample_size 必須 > 10
# （目前 DEFAULT_EXPECTED_LOSS_SAMPLE_SIZE=20 滿足要求）
DEFAULT_EXPECTED_LOSS_POINT_EXPR = (
    (pl.col("mae") / pl.col("mae_volatility")).rolling_quantile(
        0.9,
        window_size=10,
    )
    * pl.col("entry_volatility")
    * 4
).shift()


def _validate_expected_loss_expr(
    expr: pl.Expr,
    sample_size: int,
) -> None:
    """驗證 expected_loss_point_expr 是否會產生有效結果（非 null/NaN）

    原理：使用隨機正數序列測試 expression，模擬實際計算流程。
    如果結果為 null/NaN，表示 sample_size 不足或 expression 設定錯誤。

    測試資料範圍參考實際回測 trades 統計：
    - mae: 20-1000 點
    - mae_volatility: 150-500
    - entry_volatility: 150-500
    - mfe: 0-1500 點
    - g_mfe: 100-2000 點

    Args:
        expr: Expected loss point expression (Polars Expr)
        sample_size: 歷史交易筆數

    Raises:
        ValueError: 如果 expression 套用在 sample_size 筆資料後產生 null/NaN
    """
    import math
    import random

    # 生成隨機正數序列，模擬歷史交易資料
    # 數值範圍參考真實回測 trades（JP225, US100 等）
    random.seed(42)  # 固定 seed 確保可重現
    test_data = {
        # 核心欄位（expected_loss_point_expr 通常會使用）
        "mae": [random.uniform(20.0, 1000.0) for _ in range(sample_size)],
        "mae_volatility": [random.uniform(150.0, 500.0) for _ in range(sample_size)],
        "entry_volatility": [random.uniform(150.0, 500.0) for _ in range(sample_size)],
        # 其他可能使用的欄位
        "mfe": [random.uniform(0.0, 1500.0) for _ in range(sample_size)],
        "g_mfe": [random.uniform(100.0, 2000.0) for _ in range(sample_size)],
        "exit_volatility": [random.uniform(150.0, 500.0) for _ in range(sample_size)],
        "g_mfe_volatility": [random.uniform(150.0, 500.0) for _ in range(sample_size)],
        "mfe_volatility": [random.uniform(150.0, 500.0) for _ in range(sample_size)],
        # 價格相關（通常不用於 expected_loss 但提供完整性）
        "entry_price": [random.uniform(20000.0, 60000.0) for _ in range(sample_size)],
        "exit_price": [random.uniform(20000.0, 60000.0) for _ in range(sample_size)],
        "pnl": [random.uniform(-500.0, 1000.0) for _ in range(sample_size)],
    }

    # 建立測試 DataFrame
    df = pl.DataFrame(test_data)

    # 套用 expression
    try:
        result_series = df.select(expr.alias("result"))
        result_value = result_series.select(pl.col("result").last())["result"][0]
    except Exception as e:
        raise ValueError(
            f"expected_loss_point_expr 無法執行: {e}\n"
            f"請確認 expression 語法正確且適用於 DataFrame with columns: {list(test_data.keys())}"
        ) from e

    # 檢查結果是否為 null/NaN
    if result_value is None or (isinstance(result_value, float) and math.isnan(result_value)):
        raise ValueError(
            f"expected_loss_point_expr 在 sample_size={sample_size} 筆資料下產生 null/NaN。\n"
            f"常見原因：\n"
            f"1. sample_size 不足：如果使用 rolling window (window_size=W) + shift()，"
            f"需要 sample_size > W\n"
            f"2. min_periods 設定過大：rolling 操作的 min_periods 應 <= sample_size\n"
            f"3. shift() 偏移量過大：shift(n) 需要 sample_size > n\n"
            f"\n建議：增加 expected_loss_sample_size 或調整 expression 參數"
        )


class Blueprint(BaseModel):
    """策略藍圖

    Attributes:
        id: 藍圖 ID（由資料庫自動填充）
        name: 藍圖名稱
        direction: 方向
        trend_type: 趨勢類型
        note: 備註
        entry_triggers: 進場觸發器列表
        exit_triggers: 出場觸發器列表
        expected_loss_point_expr: 預期虧損點數表達式（Polars Expr）
            用於計算預期每筆交易會虧損幾個點
            算出來的數值必須是「點數」（例如：30 pips、100 points）
            預設: pl.col("mae").median() * 3
        expected_loss_sample_size: 計算時參考的歷史交易筆數
            從該 blueprint 的最新 N 筆已平倉交易中取樣
            預設: 100
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    id: Optional[UUID] = Field(None, description="藍圖 ID（由資料庫自動填充）")
    name: str = Field(..., description="藍圖名稱")
    direction: TradeDirection = Field(..., description="方向")
    trend_type: TrendType = Field(..., description="趨勢類型")
    note: str = Field(default="", description="備註")

    entry_triggers: List[Trigger] = Field(..., description="進場觸發器列表")
    exit_triggers: List[Trigger] = Field(..., description="出場觸發器列表")

    # Position sizing configuration
    expected_loss_point_expr: Optional[PolarsExprType] = Field(
        default=None,
        description=(
            "預期虧損點數表達式（Polars Expr）。"
            "從歷史 trades 的 mae 計算預期每筆交易會虧損幾個點。"
            "算出來的數值必須是「點數」（例如：30 pips、100 points）。"
            "預設: pl.col('mae').median() * 3"
        ),
    )
    expected_loss_sample_size: int = Field(
        default=DEFAULT_EXPECTED_LOSS_SAMPLE_SIZE,
        description="計算 expected_loss_point 時參考的歷史交易筆數（預設 100）",
        ge=1,
        le=10000,
    )

    @field_validator("direction", mode="before")
    @classmethod
    def convert_direction(cls, v: Any) -> TradeDirection:
        """自動轉換字串為 TradeDirection enum（case-insensitive）"""
        if isinstance(v, str):
            try:
                # Handle case-insensitive input (e.g., "LONG" → "Long")
                return TradeDirection(v.capitalize())
            except ValueError:
                valid_values = [e.value for e in TradeDirection]
                raise ValueError(
                    f"Invalid direction: '{v}'. Valid values: {', '.join(valid_values)}"
                )
        return v

    @field_validator("trend_type", mode="before")
    @classmethod
    def convert_trend_type(cls, v: Any) -> TrendType:
        """自動轉換字串為 TrendType enum（case-insensitive）"""
        if isinstance(v, str):
            try:
                # Handle case-insensitive input (e.g., "TREND" → "Trend")
                return TrendType(v.capitalize())
            except ValueError:
                valid_values = [e.value for e in TrendType]
                raise ValueError(
                    f"Invalid trend_type: '{v}'. Valid values: {', '.join(valid_values)}"
                )
        return v

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

        支援以下輸入格式：
        - None: 使用預設表達式
        - dict: 從 JSON dict 反序列化
        - str: 從 JSON 字串反序列化
        - pl.Expr: 直接使用
        """
        if v is None:
            return DEFAULT_EXPECTED_LOSS_POINT_EXPR
        return PolarsExprField.deserialize(v)

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

    @model_validator(mode="after")
    def validate_entry_trigger_types(self) -> "Blueprint":
        """確保 entry triggers 只有 Favorable 或 Adverse 其中一種

        Entry triggers 設計上只允許使用 FavorableDelayEntry 或 AdverseDelayEntry
        其中一種，不能同時混用。這是因為 hypothetical entry price 的計算需要
        在兩種策略中選擇一種作為 fallback。
        """
        entry_strategies = [t.order_strategy for t in self.entry_triggers]

        has_favorable = OrderStrategy.FAVORABLE_DELAY_ENTRY in entry_strategies
        has_adverse = OrderStrategy.ADVERSE_DELAY_ENTRY in entry_strategies

        if has_favorable and has_adverse:
            raise ValueError(
                "Entry triggers cannot have both FavorableDelayEntry and AdverseDelayEntry. "
                "Please use only one type of delay entry strategy."
            )
        return self

    @model_validator(mode="after")
    def validate_expected_loss_expr_produces_value(self) -> "Blueprint":
        """驗證 expected_loss_point_expr 在給定 sample_size 下會產生有效值

        使用隨機正數序列測試 expression，確保不會產生 null/NaN。
        如果 sample_size 不足（例如：sample_size == window_size 且有 shift），
        會在 Blueprint 建立時就拋錯，而不是等到實際計算時才失敗。
        """
        # 只在有設定 expected_loss_point_expr 時才驗證
        if self.expected_loss_point_expr is not None:
            _validate_expected_loss_expr(
                self.expected_loss_point_expr,
                self.expected_loss_sample_size,
            )
        return self
