"""
Blueprint 鏈式建構器

提供語意明確的 Helper API 建立 Trigger，自動處理方向邏輯和安全條件。

Usage:
    from tradepose_client.builder import BlueprintBuilder, TradingContext
    from tradepose_client import TradeDirection, TrendType
    import polars as pl

    # Base Blueprint
    base_bp = (
        BlueprintBuilder("base", TradeDirection.LONG, TrendType.REVERSAL)
        .add_immediate_entry_trigger(
            conditions=[
                st.col().struct.field("direction") == -1,
                pl.col("ts").dt.hour() == 21,
            ],
        )
        .add_immediate_exit_trigger(
            conditions=[st.col().struct.field("direction") == 1],
        )
        .build()
    )

    # Advanced Blueprint
    adv_bp = (
        BlueprintBuilder("risk", TradeDirection.LONG, TrendType.REVERSAL)
        .add_favorable_delay_entry_trigger(distance=atr.col() * 0.4)
        .add_stoploss_trigger(distance=atr.col() * 0.91)
        .add_takeprofit_trigger(distance=atr.col() * 1.5)
        .add_breakeven_trigger(threshold=atr.col() * 0.35, distance=atr.col() * 0.13)
        .add_trailingstop_trigger(distance=atr.col() * 0.8)
        .build()
    )
"""

import decimal
from typing import List, Optional, Union

import polars as pl
from tradepose_models.enums import EntryType, OrderStrategy, TradeDirection, TrendType
from tradepose_models.strategy import Blueprint, Trigger

from .helpers import create_blueprint, create_trigger
from .trading_context import TradingContext


def frange(start: float, stop: float, step: float, none: bool = True) -> list:
    """產生浮點數範圍列表。

    Parameters
    ----------
    start : float
        起始值（含）
    stop : float
        結束值（含）
    step : float
        步進值
    none : bool
        為 True 時在列表開頭加入 None（用於 sweep_exit 的「無此 trigger」組合）
        預設為 True

    Examples
    --------
    >>> frange(1.0, 2.0, 0.2)
    [None, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0]

    >>> frange(1.0, 2.0, 0.5, none=False)
    [1.0, 1.5, 2.0]
    """
    # Determine precision from step's decimal places
    d = decimal.Decimal(str(step))
    precision = max(0, -d.as_tuple().exponent)

    result: list = []
    current = start
    while current <= stop + step * 0.01:  # small epsilon to include stop
        rounded = round(current, precision) if precision > 0 else int(current)
        if isinstance(rounded, int):
            rounded = float(rounded)
        if rounded > stop:
            break
        result.append(rounded)
        current += step

    if none:
        result.insert(0, None)
    return result


def be_range(
    threshold: tuple[float, float, float],
    distance: tuple[float, float, float],
    none: bool = True,
) -> list:
    """產生 breakeven (threshold, distance) 組合列表。

    用於 sweep_exit 的 be 參數，產生所有 threshold × distance 的組合。
    自動過濾 distance >= threshold 的無效組合。

    Parameters
    ----------
    threshold : tuple[float, float, float]
        (start, stop, step) - threshold 範圍
    distance : tuple[float, float, float]
        (start, stop, step) - distance 範圍
    none : bool
        為 True 時在列表開頭加入 None（表示「無 breakeven」組合），預設 True

    Returns
    -------
    list[tuple[float, float] | None]
        所有 (threshold, distance) 組合，其中 distance < threshold

    Examples
    --------
    >>> from tradepose_client.builder import be_range
    >>>
    >>> # 預設 none=True，自動過濾 distance >= threshold
    >>> be_range((0.5, 1.0, 0.5), (0.1, 0.6, 0.5))
    [None, (0.5, 0.1), (1.0, 0.1), (1.0, 0.6)]
    >>> # (0.5, 0.6) 被過濾因為 distance >= threshold
    >>>
    >>> be_range((0.5, 1.0, 0.5), (0.1, 0.2, 0.1), none=False)
    [(0.5, 0.1), (0.5, 0.2), (1.0, 0.1), (1.0, 0.2)]
    """
    from itertools import product as itertools_product

    th_list = frange(*threshold, none=False)
    dist_list = frange(*distance, none=False)

    # Filter: distance must be less than threshold
    result: list = [(th, dist) for th, dist in itertools_product(th_list, dist_list) if dist < th]

    if none:
        result.insert(0, None)
    return result


class BlueprintBuilder:
    """
    Blueprint 鏈式建構器

    提供語意明確的 Helper API，自動處理：
    - LONG/SHORT 方向的加減邏輯
    - 安全條件（如 bars_since_entry > 1）
    - Trigger 名稱自動生成

    Attributes:
        name (str): Blueprint 名稱
        direction (TradeDirection | str): 交易方向
        trend_type (TrendType | str): 趨勢類型
        entry_first (bool): 是否必須先進場才能出場
        note (str): 備註
    """

    def __init__(
        self,
        name: str,
        direction: Union[TradeDirection, str],
        trend_type: Union[TrendType, str] = "Trend",
        entry_first: bool = True,
        note: str = "",
    ):
        """
        初始化 Blueprint 建構器

        Args:
            name: Blueprint 名稱
            direction: 交易方向 ("Long", "Short")
            trend_type: 趨勢類型 ("Trend", "Range", "Reversal")
            entry_first: 是否必須先進場才能出場
            note: 備註說明
        """
        self.name = name
        self.direction = direction
        self.trend_type = trend_type
        self.entry_first = entry_first
        self.note = note

        self._entry_triggers: List[Trigger] = []
        self._exit_triggers: List[Trigger] = []

    def _get_direction(self) -> TradeDirection:
        """取得 TradeDirection enum"""
        if isinstance(self.direction, TradeDirection):
            return self.direction
        return TradeDirection(self.direction)

    def _add_entry_trigger(
        self,
        name: str,
        conditions: List[pl.Expr],
        offset_expr: pl.Expr,
        order_strategy: Union[OrderStrategy, str],
        priority: int = 1,
        note: str = "",
    ) -> "BlueprintBuilder":
        """內部方法：添加進場觸發器"""
        trigger = create_trigger(
            name=name,
            conditions=conditions,
            offset_expr=offset_expr,
            order_strategy=order_strategy,
            priority=priority,
            note=note,
        )
        self._entry_triggers.append(trigger)
        return self

    def _add_exit_trigger(
        self,
        name: str,
        conditions: List[pl.Expr],
        offset_expr: pl.Expr,
        order_strategy: Union[OrderStrategy, str],
        priority: int = 1,
        note: str = "",
    ) -> "BlueprintBuilder":
        """內部方法：添加出場觸發器"""
        trigger = create_trigger(
            name=name,
            conditions=conditions,
            offset_expr=offset_expr,
            order_strategy=order_strategy,
            priority=priority,
            note=note,
        )
        self._exit_triggers.append(trigger)
        return self

    def _generate_name(self, prefix: str, triggers: List[Trigger]) -> str:
        """生成唯一名稱"""
        count = sum(1 for t in triggers if t.name.startswith(prefix))
        return prefix if count == 0 else f"{prefix}_{count + 1}"

    def _has_favorable_delay_entry(self) -> bool:
        """檢查是否已有 FavorableDelayEntry"""
        return any(
            t.order_strategy == OrderStrategy.FAVORABLE_DELAY_ENTRY for t in self._entry_triggers
        )

    def _has_adverse_delay_entry(self) -> bool:
        """檢查是否已有 AdverseDelayEntry"""
        return any(
            t.order_strategy == OrderStrategy.ADVERSE_DELAY_ENTRY for t in self._entry_triggers
        )

    # =========================================================================
    # Generic Trigger API (for full control)
    # =========================================================================

    def add_entry_trigger(
        self,
        name: str,
        conditions: List[pl.Expr],
        offset_expr: pl.Expr,
        order_strategy: Union[OrderStrategy, str],
        priority: int = 1,
        note: str = "",
    ) -> "BlueprintBuilder":
        """
        添加進場觸發器（通用方法）

        Parameters
        ----------
        name : str
            觸發器名稱
        conditions : list[pl.Expr]
            條件列表
        offset_expr : pl.Expr
            偏移量表達式。Rust 端根據 order_strategy 和 direction 計算實際價格。
        order_strategy : OrderStrategy | str
            訂單策略
        priority : int
            優先級（預設 1）
        note : str
            觸發器備註（預設空字串）

        Returns
        -------
        BlueprintBuilder
            支援 method chaining
        """
        return self._add_entry_trigger(
            name=name,
            conditions=conditions,
            offset_expr=offset_expr,
            order_strategy=order_strategy,
            priority=priority,
            note=note,
        )

    def add_exit_trigger(
        self,
        name: str,
        conditions: List[pl.Expr],
        offset_expr: pl.Expr,
        order_strategy: Union[OrderStrategy, str],
        priority: int = 1,
        note: str = "",
    ) -> "BlueprintBuilder":
        """
        添加出場觸發器（通用方法）

        Parameters
        ----------
        name : str
            觸發器名稱
        conditions : list[pl.Expr]
            條件列表
        offset_expr : pl.Expr
            偏移量表達式。Rust 端根據 order_strategy 和 direction 計算實際價格。
        order_strategy : OrderStrategy | str
            訂單策略
        priority : int
            優先級（預設 1）
        note : str
            觸發器備註（預設空字串）

        Returns
        -------
        BlueprintBuilder
            支援 method chaining
        """
        return self._add_exit_trigger(
            name=name,
            conditions=conditions,
            offset_expr=offset_expr,
            order_strategy=order_strategy,
            priority=priority,
            note=note,
        )

    # =========================================================================
    # Base Blueprint API
    # =========================================================================

    def add_immediate_entry_trigger(
        self, conditions: List[pl.Expr], note: str | None = None
    ) -> "BlueprintBuilder":
        """
        添加即時進場觸發器（用於 Base Blueprint）

        Parameters
        ----------
        conditions : list[pl.Expr]
            進場條件（全部為 True 才觸發）

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Notes
        -----
        進場價格固定為 pl.col("open")，符合 Open-Driven Design。
        offset_expr 會被 Rust 端忽略。

        Examples
        --------
        >>> builder.add_immediate_entry_trigger(
        ...     conditions=[
        ...         st.col().struct.field("direction") == -1,
        ...         pl.col("ts").dt.hour() == 21,
        ...     ],
        ... )
        """
        name = self._generate_name("entry", self._entry_triggers)
        return self._add_entry_trigger(
            name=name,
            conditions=conditions,
            offset_expr=pl.lit(0.0),  # Neutral 策略，offset 被忽略
            order_strategy=OrderStrategy.IMMEDIATE_ENTRY,
            priority=1,
            note=note or "",
        )

    def add_immediate_exit_trigger(
        self, conditions: List[pl.Expr], note: str | None = None
    ) -> "BlueprintBuilder":
        """
        添加即時出場觸發器（用於 Base Blueprint）

        Parameters
        ----------
        conditions : list[pl.Expr]
            出場條件（全部為 True 才觸發）

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Notes
        -----
        出場價格固定為 pl.col("open")，符合 Open-Driven Design。
        offset_expr 會被 Rust 端忽略。

        Examples
        --------
        >>> builder.add_immediate_exit_trigger(
        ...     conditions=[st.col().struct.field("direction") == 1],
        ... )
        """
        name = self._generate_name("exit", self._exit_triggers)
        return self._add_exit_trigger(
            name=name,
            conditions=conditions,
            offset_expr=pl.lit(0.0),  # Neutral 策略，offset 被忽略
            order_strategy=OrderStrategy.IMMEDIATE_EXIT,
            priority=1,
            note=note or "",
        )

    # =========================================================================
    # Advanced Blueprint API - Entry
    # =========================================================================

    def add_favorable_delay_entry_trigger(
        self, distance: pl.Expr, conditions: Optional[List[pl.Expr]] = None, note: str | None = None
    ) -> "BlueprintBuilder":
        """
        添加 Favorable 延遲進場觸發器

        等待價格回調至更有利的位置再進場。

        Parameters
        ----------
        distance : pl.Expr
            進場價格距離（偏移量）。
            Rust 端自動計算：
            - LONG: entry_price = base_entry - distance（等跌下來再買）
            - SHORT: entry_price = base_entry + distance（等漲上去再賣）
        conditions : list[pl.Expr] | None
            額外條件

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Raises
        ------
        ValueError
            如果已經添加過 AdverseDelayEntry trigger

        Examples
        --------
        >>> builder.add_favorable_delay_entry_trigger(distance=atr.col() * 0.4)
        """
        if self._has_adverse_delay_entry():
            raise ValueError(
                "Cannot mix FavorableDelayEntry and AdverseDelayEntry in the same blueprint. "
                "Use only one type of delay entry strategy."
            )

        name = self._generate_name("favorable_entry", self._entry_triggers)
        return self._add_entry_trigger(
            name=name,
            conditions=conditions or [],
            offset_expr=distance,  # Rust 端根據 direction 計算 base_entry ± distance
            order_strategy=OrderStrategy.FAVORABLE_DELAY_ENTRY,
            priority=1,
            note=note or "",
        )

    def add_adverse_delay_entry_trigger(
        self, distance: pl.Expr, conditions: Optional[List[pl.Expr]] = None, note: str | None = None
    ) -> "BlueprintBuilder":
        """
        添加 Adverse 延遲進場觸發器

        等待價格突破確認再進場。

        Parameters
        ----------
        distance : pl.Expr
            進場價格距離（偏移量）。
            Rust 端自動計算：
            - LONG: entry_price = base_entry + distance（突破確認再買）
            - SHORT: entry_price = base_entry - distance（跌破確認再賣）
        conditions : list[pl.Expr] | None
            額外條件

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Raises
        ------
        ValueError
            如果已經添加過 FavorableDelayEntry trigger

        Examples
        --------
        >>> builder.add_adverse_delay_entry_trigger(distance=atr.col() * 0.2)
        """
        if self._has_favorable_delay_entry():
            raise ValueError(
                "Cannot mix FavorableDelayEntry and AdverseDelayEntry in the same blueprint. "
                "Use only one type of delay entry strategy."
            )

        name = self._generate_name("adverse_entry", self._entry_triggers)
        return self._add_entry_trigger(
            name=name,
            conditions=conditions or [],
            offset_expr=distance,  # Rust 端根據 direction 計算 base_entry ± distance
            order_strategy=OrderStrategy.ADVERSE_DELAY_ENTRY,
            priority=1,
            note=note or "",
        )

    # =========================================================================
    # Advanced Blueprint API - Exit
    # =========================================================================

    def add_stoploss_trigger(
        self, distance: pl.Expr, conditions: Optional[List[pl.Expr]] = None, note: str | None = None
    ) -> "BlueprintBuilder":
        """
        添加停損觸發器

        Parameters
        ----------
        distance : pl.Expr
            停損距離（偏移量）。
            Rust 端自動計算：
            - LONG: exit_price = entry_price - distance
            - SHORT: exit_price = entry_price + distance
        conditions : list[pl.Expr] | None
            額外條件

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Examples
        --------
        >>> builder.add_stoploss_trigger(distance=atr.col() * 1.5)
        """
        name = self._generate_name("stoploss", self._exit_triggers)
        return self._add_exit_trigger(
            name=name,
            conditions=conditions or [],
            offset_expr=distance,  # Rust 端根據 direction 計算 entry_price ± distance
            order_strategy=OrderStrategy.STOP_LOSS,
            priority=1,
            note=note or "",
        )

    def add_takeprofit_trigger(
        self, distance: pl.Expr, conditions: Optional[List[pl.Expr]] = None, note: str | None = None
    ) -> "BlueprintBuilder":
        """
        添加停利觸發器

        Parameters
        ----------
        distance : pl.Expr
            停利距離（偏移量）。
            Rust 端自動計算：
            - LONG: exit_price = entry_price + distance
            - SHORT: exit_price = entry_price - distance
        conditions : list[pl.Expr] | None
            額外條件

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Examples
        --------
        >>> builder.add_takeprofit_trigger(distance=atr.col() * 2.0)
        """
        name = self._generate_name("takeprofit", self._exit_triggers)
        return self._add_exit_trigger(
            name=name,
            conditions=conditions or [],
            offset_expr=distance,  # Rust 端根據 direction 計算 entry_price ± distance
            order_strategy=OrderStrategy.TAKE_PROFIT,
            priority=2,
            note=note or "",
        )

    def add_breakeven_trigger(
        self,
        threshold: pl.Expr,
        distance: pl.Expr,
        conditions: Optional[List[pl.Expr]] = None,
        note: str | None = None,
    ) -> "BlueprintBuilder":
        """
        添加 Breakeven 觸發器

        當價格曾達到 entry ± threshold 時，將出場價設為 entry ± distance。
        只需提供 threshold 和 distance，內部已計算好觸發條件。

        Parameters
        ----------
        threshold : pl.Expr
            觸發距離（MFE 達到多少才啟動 BE）
        distance : pl.Expr
            出場價格距離（偏移量）。
            Rust 端自動計算：exit_price = entry_price + offset（方向無關）
        conditions : list[pl.Expr] | None
            額外條件

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Notes
        -----
        內部使用 `prev_highest_since_entry` / `prev_lowest_since_entry`（安全版本），
        確保條件判斷基於「到前一根 bar 為止」的最高/最低點，符合 Open-Driven Design。

        Examples
        --------
        >>> builder.add_breakeven_trigger(
        ...     threshold=atr.col() * 1.0,
        ...     distance=atr.col() * 0.1,
        ... )
        """
        direction = self._get_direction()
        entry_price = TradingContext.advanced_entry.entry_price

        if direction == TradeDirection.LONG:
            extreme = TradingContext.advanced_entry.prev_highest_since_entry
            trigger_cond = extreme > (entry_price + threshold)
        else:
            extreme = TradingContext.advanced_entry.prev_lowest_since_entry
            trigger_cond = extreme < (entry_price - threshold)

        all_conditions = [trigger_cond] + (conditions or [])

        name = self._generate_name("breakeven", self._exit_triggers)
        return self._add_exit_trigger(
            name=name,
            conditions=all_conditions,
            offset_expr=distance,  # Rust 端計算 entry_price + offset
            order_strategy=OrderStrategy.BREAKEVEN,
            priority=3,
            note=note or "",
        )

    def add_trailingstop_trigger(
        self, distance: pl.Expr, conditions: Optional[List[pl.Expr]] = None, note: str | None = None
    ) -> "BlueprintBuilder":
        """
        添加追蹤停損觸發器

        Parameters
        ----------
        distance : pl.Expr
            追蹤距離（偏移量）。
            Rust 端自動計算：
            - LONG: exit_price = prev_highest_since_entry - distance
            - SHORT: exit_price = prev_lowest_since_entry + distance
        conditions : list[pl.Expr] | None
            額外條件

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Notes
        -----
        Rust 端使用 `prev_highest_since_entry` / `prev_lowest_since_entry`，
        確保出場價格基於「到前一根 bar 為止」的最高/最低點，符合 Open-Driven Design。

        Examples
        --------
        >>> builder.add_trailingstop_trigger(distance=atr.col() * 1.0)
        """
        name = self._generate_name("trailingstop", self._exit_triggers)
        return self._add_exit_trigger(
            name=name,
            conditions=conditions or [],
            offset_expr=distance,  # Rust 端根據 direction 計算 prev_extreme ± distance
            order_strategy=OrderStrategy.TRAILING_STOP,
            priority=4,
            note=note or "",
        )

    def add_timeout_exit_trigger(
        self, bars: int, conditions: Optional[List[pl.Expr]] = None, note: str | None = None
    ) -> "BlueprintBuilder":
        """
        添加超時出場觸發器

        當持倉達到指定 bar 數時強制出場。

        Parameters
        ----------
        bars : int
            持倉最大 bar 數（達到後觸發出場）
        conditions : list[pl.Expr] | None
            額外條件

        Returns
        -------
        BlueprintBuilder
            支援 method chaining

        Notes
        -----
        TimeoutExit 是 Neutral 策略，offset_expr 會被 Rust 端忽略，
        出場價格固定為 pl.col("open")。

        Examples
        --------
        >>> builder.add_timeout_exit_trigger(bars=50)
        """
        timeout_condition = TradingContext.advanced_entry.bars_in_position >= bars

        builtin_conditions = [timeout_condition]
        all_conditions = builtin_conditions + (conditions or [])

        name = self._generate_name("timeout", self._exit_triggers)
        return self._add_exit_trigger(
            name=name,
            conditions=all_conditions,
            offset_expr=pl.lit(0.0),  # Neutral 策略，offset 被忽略
            order_strategy=OrderStrategy.TIMEOUT_EXIT,
            priority=10,
            note=note or "",
        )

    # =========================================================================
    # Build
    # =========================================================================

    def build(self) -> Blueprint:
        """
        建構最終 Blueprint 物件

        Returns
        -------
        Blueprint
            完整的 Blueprint 設定物件

        Raises
        ------
        ValueError
            如果未添加任何 trigger
        """
        if not self._entry_triggers and not self._exit_triggers:
            raise ValueError(
                f"Blueprint '{self.name}' must have at least one trigger. "
                f"Use helper methods like .add_immediate_entry_trigger() or "
                f".add_stoploss_trigger() before calling .build()"
            )

        return create_blueprint(
            name=self.name,
            direction=self.direction,
            entry_triggers=self._entry_triggers,
            exit_triggers=self._exit_triggers,
            trend_type=self.trend_type,
            entry_first=self.entry_first,
            note=self.note,
        )

    def __repr__(self) -> str:
        """字串表示"""
        return (
            f"BlueprintBuilder(name='{self.name}', "
            f"direction='{self.direction}', "
            f"entry_triggers={len(self._entry_triggers)}, "
            f"exit_triggers={len(self._exit_triggers)})"
        )

    # =========================================================================
    # Sweep API - Parameter Search
    # =========================================================================

    @staticmethod
    def sweep_delay_entry(
        direction: Union[TradeDirection, str],
        distance_expr: pl.Expr,
        multipliers: List[float],
        entry_type: Union[EntryType, str] = EntryType.FAVORABLE,
        conditions: Optional[List[pl.Expr]] = None,
        trend_type: Union[TrendType, str] = TrendType.TREND,
        name_prefix: str = "",
    ) -> List[Blueprint]:
        """
        產生多個不同 delay entry multiplier 的 Blueprint（僅進場，無出場）

        Parameters
        ----------
        direction : TradeDirection | str
            交易方向 (LONG/SHORT)
        distance_expr : pl.Expr
            距離表達式（例如 atr.col()）
        multipliers : list[float]
            要測試的 multiplier 列表，建議使用 frange() 產生
        entry_type : EntryType | str
            進場類型，預設 EntryType.FAVORABLE
        conditions : list[pl.Expr] | None
            額外進場條件
        trend_type : TrendType | str
            趨勢類型
        name_prefix : str
            名稱前綴（可選）

        Returns
        -------
        list[Blueprint]
            每個 multiplier 對應一個 Blueprint

        命名規則
        --------
        {prefix}L_fav_0.3 或 {prefix}S_adv_0.5
        - L/S = Long/Short
        - fav/adv = favorable/adverse
        - 數字 = multiplier

        Examples
        --------
        >>> from tradepose_client.builder import BlueprintBuilder, frange
        >>> from tradepose_models.enums import TradeDirection, EntryType
        >>>
        >>> # 使用預設 entry_type (favorable)
        >>> blueprints = BlueprintBuilder.sweep_delay_entry(
        ...     direction=TradeDirection.LONG,
        ...     distance_expr=atr.col(),
        ...     multipliers=frange(0.1, 0.5, 0.1),
        ... )
        >>>
        >>> # 指定 entry_type
        >>> blueprints = BlueprintBuilder.sweep_delay_entry(
        ...     direction=TradeDirection.LONG,
        ...     distance_expr=atr.col(),
        ...     multipliers=frange(0.1, 0.5, 0.1),
        ...     entry_type=EntryType.ADVERSE,  # 也可使用 "adverse"
        ... )
        """
        # Normalize entry_type to string value
        entry_type_str = entry_type.value if isinstance(entry_type, EntryType) else entry_type
        if entry_type_str not in ("favorable", "adverse"):
            raise ValueError(f"entry_type must be 'favorable' or 'adverse', got '{entry_type_str}'")

        # Direction prefix
        dir_enum = direction if isinstance(direction, TradeDirection) else TradeDirection(direction)
        dir_char = "L" if dir_enum == TradeDirection.LONG else "S"
        entry_char = "fav" if entry_type_str == "favorable" else "adv"

        blueprints = []
        for mult in multipliers:
            # Name: L_fav_0.3
            name = f"{name_prefix}{dir_char}_{entry_char}_{mult}"

            builder = BlueprintBuilder(
                name=name,
                direction=direction,
                trend_type=trend_type,
                note=f"{entry_type_str} DE={mult}",
            )

            # Add entry trigger
            if entry_type_str == "favorable":
                builder.add_favorable_delay_entry_trigger(
                    distance=distance_expr * mult,
                    conditions=conditions,
                )
            else:
                builder.add_adverse_delay_entry_trigger(
                    distance=distance_expr * mult,
                    conditions=conditions,
                )

            blueprints.append(builder.build())

        return blueprints

    @staticmethod
    def sweep_exit(
        base_blueprint: Blueprint,
        distance_expr: pl.Expr,
        sl: Optional[List[float]] = None,
        tp: Optional[List[float]] = None,
        ts: Optional[List[float]] = None,
        be: Optional[List[tuple]] = None,
        sl_conditions: Optional[List[pl.Expr]] = None,
        tp_conditions: Optional[List[pl.Expr]] = None,
        ts_conditions: Optional[List[pl.Expr]] = None,
        be_conditions: Optional[List[pl.Expr]] = None,
    ) -> List[Blueprint]:
        """
        複製 base blueprint 並加入不同出場參數組合

        Parameters
        ----------
        base_blueprint : Blueprint
            基礎 blueprint（通常已設定好 entry trigger）
        distance_expr : pl.Expr
            距離表達式（例如 atr.col()）
        sl : list[float] | None
            StopLoss multiplier 列表，建議使用 frange() 產生
        tp : list[float] | None
            TakeProfit multiplier 列表，建議使用 frange() 產生
        ts : list[float] | None
            TrailingStop multiplier 列表，建議使用 frange() 產生
        be : list[tuple[float, float]] | None
            Breakeven (threshold, distance) 列表
        sl_conditions : list[pl.Expr] | None
            StopLoss 額外條件
        tp_conditions : list[pl.Expr] | None
            TakeProfit 額外條件
        ts_conditions : list[pl.Expr] | None
            TrailingStop 額外條件
        be_conditions : list[pl.Expr] | None
            Breakeven 額外條件

        Returns
        -------
        list[Blueprint]
            所有參數組合的 Blueprint

        命名規則
        --------
        {base_name}__sl1.5_tp2.0_ts1.8_be0.5x0.1
        - sl1.5 = StopLoss multiplier
        - tp2.0 = TakeProfit multiplier
        - ts1.8 = TrailingStop multiplier
        - be0.5x0.1 = Breakeven threshold x distance
        - base = 所有參數皆為 None（無額外出場 trigger）

        Examples
        --------
        >>> from tradepose_client.builder import BlueprintBuilder, TradingContext, frange
        >>>
        >>> # 使用 frange 產生 multiplier 列表
        >>> blueprints = BlueprintBuilder.sweep_exit(
        ...     base_blueprint=entry_bp,
        ...     distance_expr=atr.col(),
        ...     sl=frange(1.0, 2.0, 0.5),  # [1.0, 1.5, 2.0]
        ...     tp=frange(1.5, 3.0, 0.5),  # [1.5, 2.0, 2.5, 3.0]
        ... )
        >>> # 產生 12 個 blueprints (3 SL × 4 TP)
        >>>
        >>> # 手動輸入 multiplier 列表
        >>> blueprints = BlueprintBuilder.sweep_exit(
        ...     base_blueprint=entry_bp,
        ...     distance_expr=atr.col(),
        ...     sl=[1.0, 1.5, 2.0],
        ...     tp=[2.0, 3.0],
        ... )
        >>>
        >>> # 使用 frange(none=True) 包含「無此 trigger」的組合
        >>> blueprints = BlueprintBuilder.sweep_exit(
        ...     base_blueprint=entry_bp,
        ...     distance_expr=atr.col(),
        ...     sl=frange(1.0, 2.0, 0.5),
        ...     ts=frange(1.0, 1.5, 0.5, none=True),  # [None, 1.0, 1.5] - None 表示不加 TS
        ... )
        >>>
        >>> # 使用 Breakeven：搭配 be_range 產生 (threshold, distance) 組合
        >>> from tradepose_client.builder import be_range
        >>>
        >>> blueprints = BlueprintBuilder.sweep_exit(
        ...     base_blueprint=entry_bp,
        ...     distance_expr=atr.col(),
        ...     sl=[1.5],
        ...     be=be_range((0.5, 1.0, 0.5), (0.1, 0.2, 0.1)),  # 4 組合
        ... )
        >>> # be_range 產生: [(0.5, 0.1), (0.5, 0.2), (1.0, 0.1), (1.0, 0.2)]
        >>>
        >>> # 帶條件的 sweep（僅在持倉超過 5 bars 後才觸發 TP）
        >>> blueprints = BlueprintBuilder.sweep_exit(
        ...     base_blueprint=entry_bp,
        ...     distance_expr=atr.col(),
        ...     sl=frange(1.0, 1.5, 0.5),
        ...     tp=frange(2.0, 3.0, 0.5),
        ...     tp_conditions=[TradingContext.advanced_entry.bars_in_position > 5],
        ... )
        """
        from itertools import product as itertools_product

        # Collect all parameter combinations
        param_lists = []
        param_names = []

        if sl is not None:
            param_lists.append(sl)
            param_names.append("sl")
        if tp is not None:
            param_lists.append(tp)
            param_names.append("tp")
        if ts is not None:
            param_lists.append(ts)
            param_names.append("ts")
        if be is not None:
            param_lists.append(be)
            param_names.append("be")

        if not param_lists:
            raise ValueError("At least one exit parameter (sl, tp, ts, be) must be provided")

        # Generate all combinations
        combinations = list(itertools_product(*param_lists))

        blueprints = []
        for combo in combinations:
            # Build name suffix (skip None values)
            name_parts = []
            params = dict(zip(param_names, combo))

            if "sl" in params and params["sl"] is not None:
                name_parts.append(f"sl{params['sl']}")
            if "tp" in params and params["tp"] is not None:
                name_parts.append(f"tp{params['tp']}")
            if "ts" in params and params["ts"] is not None:
                name_parts.append(f"ts{params['ts']}")
            if "be" in params and params["be"] is not None:
                th, dist = params["be"]
                name_parts.append(f"be{th}x{dist}")

            # Use "base" suffix when all values are None (no additional exit triggers)
            if not name_parts:
                suffix = "base"
            else:
                suffix = "_".join(name_parts)
            new_name = f"{base_blueprint.name}__{suffix}"

            # Create new builder with same entry config
            builder = BlueprintBuilder(
                name=new_name,
                direction=base_blueprint.direction,
                trend_type=base_blueprint.trend_type,
                entry_first=base_blueprint.entry_first,
                note=f"{base_blueprint.note} + {suffix}" if base_blueprint.note else suffix,
            )

            # Copy entry triggers from base
            builder._entry_triggers = list(base_blueprint.entry_triggers)

            # Add exit triggers based on params (skip None values)
            if "sl" in params and params["sl"] is not None:
                builder.add_stoploss_trigger(
                    distance=distance_expr * params["sl"],
                    conditions=sl_conditions,
                )
            if "tp" in params and params["tp"] is not None:
                builder.add_takeprofit_trigger(
                    distance=distance_expr * params["tp"],
                    conditions=tp_conditions,
                )
            if "ts" in params and params["ts"] is not None:
                builder.add_trailingstop_trigger(
                    distance=distance_expr * params["ts"],
                    conditions=ts_conditions,
                )
            if "be" in params and params["be"] is not None:
                th, dist = params["be"]
                builder.add_breakeven_trigger(
                    threshold=distance_expr * th,
                    distance=distance_expr * dist,
                    conditions=be_conditions,
                )

            blueprints.append(builder.build())

        return blueprints
