"""PlaybookRegistry — 全域策略註冊中心。"""

from __future__ import annotations

import logging
from collections.abc import Callable
from decimal import Decimal
from pathlib import Path
from typing import TypeAlias

from tradepose_models.enums import (
    AccountSource,
    BrokerType,
    Currency,
    Freq,
    TradeDirection,
    TrendType,
)
from tradepose_models.strategy import Blueprint, StrategyConfig
from tradepose_models.strategy.portfolio import BlueprintSelection, Portfolio

from .discovery import ModuleDiscovery
from .types import BpFactoryRecord, BpFunc

PortfolioFunc: TypeAlias = Callable[[], list[tuple[str, str]]]

logger = logging.getLogger(__name__)


_GITIGNORE_CONTENT = """\
__pycache__/
*.pyc
*.pyo
.DS_Store
"""

_INIT_CONTENT = """\
"""


class PlaybookRegistry:
    """全域策略註冊中心。

    管理所有已註冊的 strategies、blueprints、portfolios。
    支援 auto-discover、reload、sweep、以及接軌 BatchTester。

    Args:
        root: playbook 根目錄路徑（相對於 cwd 或絕對路徑，預設 "playbook"）

    Example:
        registry = PlaybookRegistry()
        strategies = registry.to_strategies("us100_balanced")
    """

    def __init__(self, root: str | Path = "playbook") -> None:
        self._root = Path(root).resolve()
        self._strategies: dict[str, StrategyConfig] = {}
        self._bp_factories: dict[str, BpFactoryRecord] = {}
        self._portfolios: dict[str, Portfolio] = {}
        self._discovery = ModuleDiscovery(self._root)
        self._ensure_directories()

    # ------------------------------------------------------------------
    # 註冊（由 decorator / template 呼叫）
    # ------------------------------------------------------------------

    def register_strategy(self, config: StrategyConfig) -> None:
        """註冊策略。若同名已存在則跳過。"""
        if config.name in self._strategies:
            logger.debug("Strategy already registered: %s", config.name)
            return
        self._strategies[config.name] = config
        logger.debug("Registered strategy: %s", config.name)

    def append_blueprints(self, strategy_name: str, blueprints: list[Blueprint]) -> None:
        """附加 advanced blueprints 到已存在的策略。"""
        if strategy_name not in self._strategies:
            msg = f"Strategy not found: {strategy_name}"
            raise KeyError(msg)
        config = self._strategies[strategy_name]
        existing = list(config.advanced_blueprints)
        existing_names = {bp.name for bp in existing}
        new_bps = [bp for bp in blueprints if bp.name not in existing_names]
        if new_bps:
            self._strategies[strategy_name] = config.with_adv_blueprints(existing + new_bps)
            logger.debug("Appended %d blueprints to %s", len(new_bps), strategy_name)

    def register_portfolio(self, portfolio: Portfolio) -> None:
        """註冊 portfolio。若同名已存在則覆蓋。"""
        self._portfolios[portfolio.name] = portfolio
        logger.debug("Registered portfolio: %s", portfolio.name)

    def register_bp_factory(self, record: BpFactoryRecord) -> None:
        """註冊 blueprint factory record（供 reload 使用）。"""
        self._bp_factories[record.bp_func_name] = record

    # ------------------------------------------------------------------
    # Decorator
    # ------------------------------------------------------------------

    def blueprint(self, config: StrategyConfig) -> Callable[[BpFunc], BpFunc]:
        """Decorator：自動註冊 strategy + 附加 blueprints。

        1. 若 config.name 尚未註冊 → 自動 register_strategy(config)
        2. 呼叫被裝飾函式取得 list[Blueprint]
        3. append_blueprints(config.name, blueprints)
        4. 儲存 func reference 供 reload 使用

        Example:
            @registry.blueprint(config)
            def h11_favorable_03() -> list[Blueprint]:
                return BlueprintBuilder.sweep(config, entry_multipliers=[0.3], ...)
        """

        def decorator(func: BpFunc) -> BpFunc:
            self.register_strategy(config)
            blueprints: list[Blueprint] = func()
            if blueprints:
                self.append_blueprints(config.name, blueprints)
            self.register_bp_factory(
                BpFactoryRecord(
                    config=config,
                    bp_func=func,
                    bp_func_name=func.__name__,
                )
            )
            return func

        return decorator

    def portfolio(
        self,
        name: str,
        capital: Decimal,
        *,
        currency: Currency = Currency.USD,
        account_source: AccountSource | None = None,
        broker_type: BrokerType | None = None,
    ) -> Callable[[PortfolioFunc], PortfolioFunc]:
        """Decorator：將 portfolio 定義函式註冊到 registry。

        被裝飾函式回傳 list[(strategy_name, blueprint_name)]，
        自動建立 Portfolio 並註冊。

        Example:
            @registry.portfolio("us100_balanced", capital=Decimal("100000"))
            def us100_balanced() -> list[tuple[str, str]]:
                return [
                    (config.name, "L_fav_0.3__sl4.25_ts7_tp11"),
                    (config.name, "L_adv_0.5__sl5_ts6.75"),
                ]
        """

        def decorator(func: PortfolioFunc) -> PortfolioFunc:
            selections = func()
            p = Portfolio(
                name=name,
                selections=[
                    BlueprintSelection(strategy_name=s, blueprint_name=b) for s, b in selections
                ],
                capital=capital,
                currency=currency,
                account_source=account_source,
                broker_type=broker_type,
            )
            self.register_portfolio(p)
            return func

        return decorator

    # ------------------------------------------------------------------
    # 查詢
    # ------------------------------------------------------------------

    def __contains__(self, strategy_name: str) -> bool:
        return strategy_name in self._strategies

    def get_strategy(self, name: str) -> StrategyConfig:
        """取得已註冊的策略。"""
        if name not in self._strategies:
            msg = f"Strategy not found: {name}"
            raise KeyError(msg)
        return self._strategies[name]

    def get_portfolio(self, name: str) -> Portfolio:
        """取得已註冊的 portfolio。"""
        if name not in self._portfolios:
            msg = f"Portfolio not found: {name}"
            raise KeyError(msg)
        return self._portfolios[name]

    def list_strategies(self) -> list[str]:
        """列出所有已註冊的策略名稱。"""
        return list(self._strategies.keys())

    def list_portfolios(self) -> list[str]:
        """列出所有已註冊的 portfolio 名稱。"""
        return list(self._portfolios.keys())

    def search(
        self,
        *,
        instrument: str | list[str] | None = None,
        tz: str | list[str] | None = None,
        base_freq: Freq | str | list[Freq | str] | None = None,
        direction: TradeDirection | str | None = None,
        trend_type: TrendType | str | list[TrendType | str] | None = None,
    ) -> list[StrategyConfig]:
        """搜尋已註冊的策略，支援多值條件與 blueprint 層級過濾。

        **搜尋語意**

        - ``instrument`` / ``tz`` / ``base_freq``：策略層級篩選。支援單值或 list，list
          時取聯集（符合任一值即通過）。``instrument`` 為 partial match；``tz`` 和
          ``base_freq`` 為 exact match。
        - ``direction`` / ``trend_type``：blueprint 層級篩選。每個符合策略的
          ``advanced_blueprints`` 會被過濾，只保留同時符合 direction、trend_type 的
          blueprint；base_blueprint 不符合則整個策略被排除。

        **業務目標**

        透過多維度篩選快速定位目標策略與 blueprint 組合，避免手動遍歷：

        - 找出特定 broker（instrument partial match）下所有 LONG 策略
        - 找出特定時區 + Trend 類型的 blueprint
        - 找出特定 freq 的所有策略（e.g. ``Freq.MIN_15``、``Freq.DAY_1``）
        - 跨策略對比相同 direction/trend_type 的 blueprint 清單

        **測試目標**

        - instrument list：["FTMO", "PEPPER"] → 兩家 broker 的策略都要回傳
        - direction=LONG → 排除 SHORT advanced_blueprints；base_blueprint 是 SHORT 的
          整個策略被排除
        - trend_type list：["Trend", "Range"] → 兩種 trend_type 的 blueprint 都保留
        - 多條件 AND：instrument="FTMO" + direction="Long" → 只有 FTMO 且含 LONG bp 的策略

        Args:
            instrument: 交易標的關鍵字（partial match），e.g. ``"FTMO"`` 或
                ``["FTMO", "PEPPER"]``
            direction: 交易方向（blueprint 層級），e.g. ``"Long"`` 或
                ``TradeDirection.LONG``。只支援單值，因為 LONG/SHORT 互斥。
            trend_type: 趨勢類型（blueprint 層級），e.g. ``"Trend"`` 或
                ``["Trend", "Range"]``
            tz: 時區字串（exact match），e.g. ``"Europe/Athens"`` 或
                ``["Europe/Athens", "Asia/Tokyo"]``
            base_freq: 基礎頻率（exact match），e.g. ``Freq.MIN_15`` 或
                ``[Freq.MIN_15, Freq.DAY_1]`` 或字串 ``["15min", "1D"]``

        Returns:
            符合所有條件的 StrategyConfig 列表。當 direction 或 trend_type 有指定時，
            每個 StrategyConfig 的 advanced_blueprints 只包含通過 blueprint 層級篩選的項目。
        """
        results = list(self._strategies.values())

        # --- strategy-level filters ---

        if instrument is not None:
            instruments = [instrument] if isinstance(instrument, str) else instrument
            results = [c for c in results if any(i in c.base_instrument for i in instruments)]

        if tz is not None:
            tzs = [tz] if isinstance(tz, str) else tz
            results = [c for c in results if c.tz in tzs]

        if base_freq is not None:
            raw_freqs = [base_freq] if not isinstance(base_freq, list) else base_freq
            freqs = [Freq(f) if isinstance(f, str) else f for f in raw_freqs]
            results = [c for c in results if c.base_freq in freqs]

        # --- blueprint-level filters ---

        need_bp_filter = direction is not None or trend_type is not None

        if need_bp_filter:
            td: TradeDirection | None = None
            if direction is not None:
                td = (
                    TradeDirection(direction.capitalize())
                    if isinstance(direction, str)
                    else direction
                )

            tts: list[TrendType] | None = None
            if trend_type is not None:
                raw = [trend_type] if not isinstance(trend_type, list) else trend_type
                tts = [TrendType(t.capitalize()) if isinstance(t, str) else t for t in raw]

            def _bp_matches(bp: Blueprint) -> bool:
                if td is not None and bp.direction != td:
                    return False
                if tts is not None and bp.trend_type not in tts:
                    return False
                return True

            filtered: list[StrategyConfig] = []
            for config in results:
                if not _bp_matches(config.base_blueprint):
                    continue
                matching_adv = [bp for bp in config.advanced_blueprints if _bp_matches(bp)]
                filtered.append(config.with_adv_blueprints(matching_adv))
            results = filtered

        return results

    # ------------------------------------------------------------------
    # 操作
    # ------------------------------------------------------------------

    def to_strategies(self, portfolio_name: str) -> list[StrategyConfig]:
        """從 portfolio 取出所有 strategies，可直接餵給 BatchTester.submit()。

        每個 (strategy_name, blueprint_name) 組合會對應到一個 StrategyConfig，
        其中 advanced_blueprints 只包含該 selection 指定的 blueprint。

        Args:
            portfolio_name: Portfolio 名稱

        Returns:
            list[StrategyConfig]: 可直接餵給 tester.submit() 的策略列表
        """
        portfolio = self.get_portfolio(portfolio_name)
        result: list[StrategyConfig] = []

        # 按 strategy_name 分組
        grouped: dict[str, list[BlueprintSelection]] = {}
        for sel in portfolio.selections:
            grouped.setdefault(sel.strategy_name, []).append(sel)

        for strategy_name, selections in grouped.items():
            config = self.get_strategy(strategy_name)
            bp_names = {sel.blueprint_name for sel in selections}
            selected_bps = [bp for bp in config.advanced_blueprints if bp.name in bp_names]
            result.append(config.with_adv_blueprints(selected_bps))

        return result

    # ------------------------------------------------------------------
    # 生命週期
    # ------------------------------------------------------------------

    def discover(self) -> list[str]:
        """掃描 blueprints/ + portfolios/ 下所有 .py，觸發 import 執行 decorator 註冊。

        必須在 registry singleton 已設定後才呼叫。blueprint 模組通常在 module level
        呼叫 get_registry()，若 discover() 在 singleton 賦值前執行，會觸發重入建立
        新的 PlaybookRegistry，造成無限遞迴。get_registry() 保證先設定 singleton
        再呼叫此方法。

        Returns:
            list[str]: 已載入的模組名稱列表
        """
        return self._discovery.discover()

    def reload(self) -> list[str]:
        """清除所有已註冊項目，重新載入所有模組。

        處理 Python import 快取：對已知模組 importlib.reload()，掃描新增模組 import。
        開發階段可隨時呼叫以載入最新 code。

        Returns:
            list[str]: 已重新載入的模組名稱列表
        """
        self._clear()
        return self._discovery.reload()

    def _clear(self) -> None:
        """清除所有已註冊的 strategies、bp_factories、portfolios。"""
        self._strategies.clear()
        self._bp_factories.clear()
        self._portfolios.clear()

    def _ensure_directories(self) -> None:
        """確保 strategies/、blueprints/、portfolios/ 目錄及 __init__.py 存在。"""
        for subdir in ("strategies", "blueprints", "portfolios"):
            dir_path = self._root / subdir
            dir_path.mkdir(parents=True, exist_ok=True)
            init_file = dir_path / "__init__.py"
            if not init_file.exists():
                init_file.write_text(_INIT_CONTENT)

        # .gitignore
        gitignore = self._root / ".gitignore"
        if not gitignore.exists():
            gitignore.write_text(_GITIGNORE_CONTENT)

        # Root __init__.py
        root_init = self._root / "__init__.py"
        if not root_init.exists():
            root_init.write_text(_INIT_CONTENT)
