"""Export 任務管理 API"""

import json
import time
import zipfile
from io import BytesIO
from pathlib import Path
from typing import Optional, Dict, List, Tuple, Union, Any

import polars as pl
import requests

from ..models import ExportTaskResponse, TaskStatus


class ExportAPI:
    """Export 任務管理 API Mixin"""

    
    def create_export(
        self,
        strategy_name: str,
        blueprint_name: str,
        export_type: str = "enhanced-ohlcv",
        start_date: Optional[str] = None,
        end_date: Optional[str] = None,
        strategy_ids: Optional[List[str]] = None,
    ) -> str:
        """創建 export 任務

        Args:
            strategy_name: 策略名稱
            blueprint_name: 藍圖名稱
            export_type: 導出類型 (enhanced-ohlcv, backtest-results, latest-trades)
            start_date: 起始日期 (ISO 8601 格式, 例如 "2020-01-01T00:00:00")
            end_date: 終止日期 (ISO 8601 格式)
            strategy_ids: 策略 ID 列表 (可選)

        Returns:
            Export task ID
        """
        payload = {"strategy_name": strategy_name, "blueprint_name": blueprint_name}

        if start_date:
            payload["start_date"] = start_date
        if end_date:
            payload["end_date"] = end_date
        if strategy_ids:
            payload["strategy_ids"] = strategy_ids

        response = requests.post(
            f"{self.api_url}/api/v1/backtest/export/{export_type}",
            json=payload,
            headers=self._get_headers(),
        )
        response.raise_for_status()

        result = response.json()
        return result["export_task_id"]

    def get_export_status(self, task_id: str) -> ExportTaskResponse:
        """查詢 export 任務狀態

        Args:
            task_id: Export task ID

        Returns:
            ExportTaskResponse: 任務狀態信息（Pydantic 模型）

        Example:
            >>> status = client.get_export_status(task_id)
            >>> print(f"狀態: {status.status}")
            >>> if status.status == TaskStatus.COMPLETED:
            ...     print(f"完成時間: {status.completed_at}")
        """
        response = requests.get(
            f"{self.api_url}/api/v1/backtest/export/{task_id}/status",
            headers=self._get_headers()
        )
        response.raise_for_status()
        return ExportTaskResponse.model_validate(response.json())

    def wait_for_export(
        self,
        task_id: str,
        timeout: int = 300,
        poll_interval: int = 2,
        verbose: bool = True,
    ) -> ExportTaskResponse:
        """等待 export 任務完成

        Args:
            task_id: Export task ID
            timeout: 超時時間（秒）
            poll_interval: 輪詢間隔（秒）
            verbose: 是否顯示進度信息

        Returns:
            ExportTaskResponse: 完成的任務狀態（Pydantic 模型）

        Raises:
            TimeoutError: 等待超時
            RuntimeError: 任務失敗

        Example:
            >>> result = client.wait_for_export(task_id, verbose=True)
            >>> print(f"執行策略: {result.executed_strategies}")
            >>> if result.result_summary:
            ...     print(f"總交易數: {result.result_summary.total_trades}")
        """
        start_time = time.time()

        while True:
            elapsed = time.time() - start_time
            if elapsed > timeout:
                raise TimeoutError(f"等待任務完成超時 ({timeout}秒)")

            status_info = self.get_export_status(task_id)
            status = status_info.status

            if verbose:
                print(f"[{elapsed:.1f}s] Status: {status.value}", end="\r")

            if status == TaskStatus.COMPLETED:
                if verbose:
                    print(f"\n✅ 任務完成！耗時 {elapsed:.1f}秒")
                return status_info

            elif status == TaskStatus.FAILED:
                error = status_info.error or "Unknown error"
                raise RuntimeError(f"任務失敗: {error}")

            time.sleep(poll_interval)

    def get_metadata_from_redis(self, task_id: str) -> Dict:
        """從 Redis 讀取任務 metadata

        注意: Redis 只存儲 metadata,實際數據需要通過 API 下載

        Args:
            task_id: Export task ID

        Returns:
            Metadata 字典
        """
        metadata_key = f"export:{task_id}:metadata"
        metadata_str = self.redis.get(metadata_key)

        if not metadata_str:
            raise ValueError(f"找不到任務 {task_id} 的元數據")

        return json.loads(metadata_str)

    def _extract_zip_to_memory(
        self, zip_data: Union[BytesIO, bytes]
    ) -> Dict[str, BytesIO]:
        """在內存中解壓 ZIP 文件

        Args:
            zip_data: ZIP 文件數據 (BytesIO 或 bytes)

        Returns:
            Dict[filename, BytesIO] - 文件名到內容的映射
        """
        if isinstance(zip_data, bytes):
            zip_data = BytesIO(zip_data)

        files = {}
        with zipfile.ZipFile(zip_data, "r") as zip_ref:
            for filename in zip_ref.namelist():
                files[filename] = BytesIO(zip_ref.read(filename))

        return files

    def wait_and_download(
        self,
        task_id: str,
        output_path: Optional[str] = None,
        timeout: int = 300,
        verbose: bool = True,
    ) -> pl.DataFrame:
        """等待任務完成並下載 Parquet 文件（推薦方法）

        Args:
            task_id: Export task ID
            output_path: 輸出文件路徑（如果為 None，自動生成）
            timeout: 超時時間（秒）
            verbose: 是否顯示進度

        Returns:
            Polars DataFrame
        """
        # 等待任務完成
        self.wait_for_export(task_id, timeout=timeout, verbose=verbose)

        # 下載 Parquet 文件
        parquet_path = self.download_parquet(task_id, output_path)

        # 讀取並應用 schema
        df = self.load_parquet_with_schema(parquet_path)

        return df

    def download_parquet(
        self, task_id: str, output_path: Optional[str] = None
    ) -> Union[Path, BytesIO]:
        """直接從 API 下載 Parquet 文件

        Args:
            task_id: Export task ID
            output_path: 輸出文件路徑（如果為 None，使用內存 BytesIO）

        Returns:
            下載的文件路徑或 BytesIO 對象

        Raises:
            RuntimeError: 下載失敗
        """
        # 發送下載請求
        url = f"{self.api_url}/api/v1/backtest/export/{task_id}/download"
        print(f"📥 開始下載: {url}")

        response = requests.get(url, stream=True, headers=self._get_headers())
        response.raise_for_status()

        # 如果未指定路徑，使用 BytesIO
        if output_path is None:
            buffer = BytesIO()
            total_size = 0

            for chunk in response.iter_content(chunk_size=8192):
                if chunk:
                    buffer.write(chunk)
                    total_size += len(chunk)

            buffer.seek(0)  # 重置到開頭以便讀取
            file_size_mb = len(buffer.getvalue()) / (1024 * 1024)
            print(f"✅ 下載完成（內存）")
            print(f"📦 數據大小: {file_size_mb:.2f} MB")

            return buffer

        # 保存到文件
        output_path = Path(output_path)
        output_path.parent.mkdir(parents=True, exist_ok=True)

        total_size = 0
        with open(output_path, "wb") as f:
            for chunk in response.iter_content(chunk_size=8192):
                if chunk:
                    f.write(chunk)
                    total_size += len(chunk)

        file_size_mb = output_path.stat().st_size / (1024 * 1024)
        print(f"✅ 下載完成: {output_path}")
        print(f"📦 文件大小: {file_size_mb:.2f} MB")

        return output_path

    def load_parquet_with_schema(
        self, file_path: Union[str, Path, BytesIO]
    ) -> pl.DataFrame:
        """讀取 Parquet 文件並應用正確的 schema

        Args:
            file_path: Parquet 文件路徑或 BytesIO 對象

        Returns:
            Polars DataFrame (已應用 schema)
        """
        if isinstance(file_path, BytesIO):
            print(f"📖 讀取 Parquet（從內存）")
        else:
            print(f"📖 讀取 Parquet: {file_path}")

        # 讀取文件
        df = pl.read_parquet(file_path)

        # print(df.head())

        # df = df.cast(enhanced_ohlcv_schema)

        print(f"✅ 讀取完成: {len(df):,} 行 × {len(df.columns)} 列")

        return df

    def save_to_parquet(
        self, df: pl.DataFrame, output_path: str, compression: str = "zstd"
    ) -> Path:
        """保存 DataFrame 為 Parquet 文件

        Args:
            df: Polars DataFrame
            output_path: 輸出文件路徑
            compression: 壓縮算法

        Returns:
            輸出文件路徑
        """
        output_path = Path(output_path)
        output_path.parent.mkdir(parents=True, exist_ok=True)

        df.write_parquet(output_path, compression=compression)

        file_size_mb = output_path.stat().st_size / (1024 * 1024)
        print(f"✅ 已保存: {output_path}")
        print(f"📦 文件大小: {file_size_mb:.2f} MB")
        print(f"📊 行數: {len(df):,} | 列數: {len(df.columns)}")

        return output_path

    def download_and_read_backtest_results(
        self,
        task_id: str,
        save_zip_path: Optional[str] = None,
        save_trades_path: Optional[str] = None,
        save_perf_path: Optional[str] = None,
    ) -> Tuple[pl.DataFrame, pl.DataFrame]:
        """下載並讀取 backtest-results（ZIP 格式）

        下載 ZIP 文件 → 內存解壓 → 讀取 trades + performance Parquet

        Args:
            task_id: Export task ID
            save_zip_path: ZIP 保存路徑（可選，不指定則純內存操作）
            save_trades_path: Trades Parquet 保存路徑（可選）
            save_perf_path: Performance Parquet 保存路徑（可選）

        Returns:
            (trades_df, performance_df): 兩個 Polars DataFrame

        Example:
            >>> # 純內存模式
            >>> trades_df, perf_df = client.download_and_read_backtest_results(task_id)
            >>>
            >>> # 保存到本地
            >>> trades_df, perf_df = client.download_and_read_backtest_results(
            ...     task_id,
            ...     save_zip_path="./results.zip",
            ...     save_trades_path="./trades.parquet",
            ...     save_perf_path="./performance.parquet"
            ... )
        """
        # 下載 ZIP
        print(f"📥 開始下載 backtest-results: {task_id}")
        zip_data = self.download_parquet(task_id, output_path=save_zip_path)

        # 解壓到內存
        print(f"📦 解壓 ZIP 文件...")
        if isinstance(zip_data, Path):
            with open(zip_data, "rb") as f:
                files = self._extract_zip_to_memory(f.read())
        else:
            files = self._extract_zip_to_memory(zip_data)

        # 找到 trades 和 performance 文件
        trades_file = None
        perf_file = None

        for filename, content in files.items():
            if "trades" in filename.lower() and filename.endswith(".parquet"):
                trades_file = content
            elif "performance" in filename.lower() and filename.endswith(".parquet"):
                perf_file = content

        if not trades_file or not perf_file:
            raise ValueError(
                f"ZIP 文件中找不到 trades 或 performance parquet 文件。文件列表: {list(files.keys())}"
            )

        # 讀取 trades
        print(f"📖 讀取 trades.parquet...")
        trades_df = pl.read_parquet(trades_file)
        print(f"   ✅ {len(trades_df):,} 行 × {len(trades_df.columns)} 列")

        # 讀取 performance
        print(f"📖 讀取 performance.parquet...")
        perf_df = pl.read_parquet(perf_file)
        print(f"   ✅ {len(perf_df):,} 行 × {len(perf_df.columns)} 列")

        # 可選：保存到本地
        if save_trades_path:
            self.save_to_parquet(trades_df, save_trades_path)

        if save_perf_path:
            self.save_to_parquet(perf_df, save_perf_path)

        return trades_df, perf_df

    def download_and_read_latest_trades(
        self,
        task_id: str,
        save_path: Optional[str] = None,
    ) -> pl.DataFrame:
        """下載並讀取 latest-trades（Parquet 格式）

        Args:
            task_id: Export task ID
            save_path: 保存路徑（可選，不指定則純內存操作）

        Returns:
            Polars DataFrame

        Example:
            >>> # 純內存模式
            >>> df = client.download_and_read_latest_trades(task_id)
            >>>
            >>> # 保存到本地
            >>> df = client.download_and_read_latest_trades(task_id, save_path="./trades.parquet")
        """
        # 下載 Parquet
        print(f"📥 開始下載 latest-trades: {task_id}")
        file_or_buffer = self.download_parquet(task_id, output_path=save_path)

        # 讀取
        print(f"📖 讀取 latest_trades.parquet...")
        df = pl.read_parquet(file_or_buffer)

        print(f"✅ 讀取完成: {len(df):,} 行 × {len(df.columns)} 列")

        return df

    def download_and_read_enhanced_ohlcv(
        self,
        task_id: str,
        save_path: Optional[str] = None,
    ) -> pl.DataFrame:
        """下載並讀取 enhanced-ohlcv（Parquet 格式）

        這是 wait_and_download 的別名，提供統一的接口

        Args:
            task_id: Export task ID
            save_path: 保存路徑（可選，不指定則純內存操作）

        Returns:
            Polars DataFrame
        """
        return self.wait_and_download(task_id, output_path=save_path, verbose=True)

    def quick_export(
        self,
        strategy_name: str,
        blueprint_name: Optional[str] = None,
        start_date: str = "2020-01-01T00:00:00",
        end_date: Optional[str] = None,
        save_path: Optional[str] = None,
    ) -> pl.DataFrame:
        """快速導出（一鍵完成所有步驟）

        從 API 下載 Parquet 文件並讀取。不依賴 PostgreSQL。

        Args:
            strategy_name: 策略名稱
            blueprint_name: 藍圖名稱（如果為 None，自動獲取）
            start_date: 起始日期
            end_date: 終止日期
            save_path: 保存路徑（如果為 None，自動生成臨時文件）

        Returns:
            Polars DataFrame

        Example:
            >>> client = TradeposeClient()
            >>> df = client.quick_export(
            ...     strategy_name="txf_1h_sma30_50",
            ...     save_path="./data/my_data.parquet"
            ... )
        """
        # 獲取 blueprint 名稱
        if blueprint_name is None:
            strategy_detail = self.get_strategy_detail(strategy_name)
            blueprint_name = strategy_detail["base_blueprint"]["name"]
            print(f"📋 使用 blueprint: {blueprint_name}")

        # 創建 export
        print(f"🚀 創建 export 任務...")
        task_id = self.create_export(
            strategy_name=strategy_name,
            blueprint_name=blueprint_name,
            start_date=start_date,
            end_date=end_date,
        )
        print(f"   Task ID: {task_id}")

        # 等待完成並下載
        print(f"⏳ 等待任務完成...")
        df = self.wait_and_download(task_id, output_path=save_path, verbose=True)

        return df

    def quick_backtest_results(
        self,
        strategy_ids: List[str],
        start_date: Optional[str] = None,
        end_date: Optional[str] = None,
        save_zip_path: Optional[str] = None,
        save_trades_path: Optional[str] = None,
        save_perf_path: Optional[str] = None,
    ) -> Tuple[pl.DataFrame, pl.DataFrame]:
        """快速導出回測結果（一鍵完成所有步驟）

        創建任務 → 等待完成 → 下載 ZIP → 解壓 → 讀取

        Args:
            strategy_ids: 策略 ID 列表
            start_date: 起始日期 (ISO 8601)
            end_date: 終止日期 (ISO 8601)
            save_zip_path: ZIP 保存路徑（可選）
            save_trades_path: Trades Parquet 保存路徑（可選）
            save_perf_path: Performance Parquet 保存路徑（可選）

        Returns:
            (trades_df, performance_df): 兩個 Polars DataFrame

        Example:
            >>> client = TradeposeClient()
            >>> trades_df, perf_df = client.quick_backtest_results(
            ...     strategy_ids=["txf_1h_sma30_50", "txf_1d_ema10_20"],
            ...     save_zip_path="./results.zip"
            ... )
        """
        # 創建任務
        print(f"🚀 創建 backtest-results 導出任務...")
        print(f"   策略列表: {strategy_ids}")

        task_id = self.create_export(
            strategy_name=strategy_ids[0] if strategy_ids else "",
            blueprint_name="",  # backtest-results 不需要 blueprint
            export_type="backtest-results",
            start_date=start_date,
            end_date=end_date,
            strategy_ids=strategy_ids,
        )
        print(f"   Task ID: {task_id}")

        # 等待完成
        print(f"⏳ 等待任務完成...")
        self.wait_for_export(task_id, verbose=True)

        # 下載並讀取
        return self.download_and_read_backtest_results(
            task_id=task_id,
            save_zip_path=save_zip_path,
            save_trades_path=save_trades_path,
            save_perf_path=save_perf_path,
        )

    def quick_latest_trades(
        self,
        strategy_ids: List[str],
        start_date: Optional[str] = None,
        end_date: Optional[str] = None,
        save_path: Optional[str] = None,
    ) -> pl.DataFrame:
        """快速導出最新交易（一鍵完成所有步驟）

        創建任務 → 等待完成 → 下載 Parquet → 讀取

        Args:
            strategy_ids: 策略 ID 列表
            start_date: 起始日期 (ISO 8601)
            end_date: 終止日期 (ISO 8601)
            save_path: Parquet 保存路徑（可選）

        Returns:
            Polars DataFrame

        Example:
            >>> client = TradeposeClient()
            >>> df = client.quick_latest_trades(
            ...     strategy_ids=["txf_1h_sma30_50"],
            ...     save_path="./latest_trades.parquet"
            ... )
        """
        # 創建任務
        print(f"🚀 創建 latest-trades 導出任務...")
        print(f"   策略列表: {strategy_ids}")

        task_id = self.create_export(
            strategy_name=strategy_ids[0] if strategy_ids else "",
            blueprint_name="",  # latest-trades 不需要 blueprint
            export_type="latest-trades",
            start_date=start_date,
            end_date=end_date,
            strategy_ids=strategy_ids,
        )
        print(f"   Task ID: {task_id}")

        # 等待完成
        print(f"⏳ 等待任務完成...")
        self.wait_for_export(task_id, verbose=True)

        # 下載並讀取
        return self.download_and_read_latest_trades(
            task_id=task_id, save_path=save_path
        )

    def quick_enhanced_ohlcv(
        self,
        strategy_name: str,
        blueprint_name: Optional[str] = None,
        start_date: str = "2020-01-01T00:00:00",
        end_date: Optional[str] = None,
        save_path: Optional[str] = None,
    ) -> pl.DataFrame:
        """快速導出 enhanced-ohlcv（一鍵完成所有步驟）

        創建任務 → 等待完成 → 下載 Parquet → 讀取

        Args:
            strategy_name: 策略名稱
            blueprint_name: 藍圖名稱（如果為 None，自動獲取）
            start_date: 起始日期 (ISO 8601)
            end_date: 終止日期 (ISO 8601)
            save_path: Parquet 保存路徑（可選）

        Returns:
            Polars DataFrame

        Example:
            >>> client = TradeposeClient()
            >>> df = client.quick_enhanced_ohlcv(
            ...     strategy_name="txf_1h_sma30_50",
            ...     blueprint_name="txf_base_trend",
            ...     save_path="./enhanced_ohlcv.parquet"
            ... )
        """
        # 獲取 blueprint 名稱
        if blueprint_name is None:
            strategy_detail = self.get_strategy_detail(strategy_name)
            blueprint_name = strategy_detail["base_blueprint"]["name"]
            print(f"📋 使用 blueprint: {blueprint_name}")

        # 創建任務
        print(f"🚀 創建 enhanced-ohlcv 導出任務...")
        print(f"   策略名稱: {strategy_name}")
        print(f"   Blueprint: {blueprint_name}")

        task_id = self.create_export(
            strategy_name=strategy_name,
            blueprint_name=blueprint_name,
            export_type="enhanced-ohlcv",
            start_date=start_date,
            end_date=end_date,
        )
        print(f"   Task ID: {task_id}")

        # 等待完成
        print(f"⏳ 等待任務完成...")
        self.wait_for_export(task_id, verbose=True)

        # 下載並讀取
        return self.download_and_read_enhanced_ohlcv(
            task_id=task_id, save_path=save_path
        )

    def create_on_demand_ohlcv_export(
        self,
        base_instrument: str,
        base_freq: str,
        indicator_specs: List[Dict[str, Any]],
        start_date: Optional[str] = None,
        end_date: Optional[str] = None,
    ) -> str:
        """創建 on-demand-ohlcv 導出任務

        特點：無需預先註冊策略，直接指定指標規格即可導出

        Args:
            base_instrument: 基礎商品 ID (例如 "TXFR1", "TXF_M1_SHIOAJI_FUTURE")
            base_freq: 基礎頻率 ("1min", "5min", "1h", "1D" 等)
            indicator_specs: 指標規格列表 (IndicatorSpec JSON 數組)
            start_date: 開始時間 (ISO 8601 格式, 例如 "2025-01-01T00:00:00")
            end_date: 結束時間 (ISO 8601 格式)

        Returns:
            Export task ID

        Example:
            >>> # 方式 1: 使用 dict
            >>> specs = [
            ...     {
            ...         "freq": "1h",
            ...         "shift": 1,
            ...         "indicator": {"type": "SMA", "period": 20, "column": "close"}
            ...     },
            ...     {
            ...         "freq": "1h",
            ...         "shift": 1,
            ...         "indicator": {"type": "ATR", "period": 14}
            ...     }
            ... ]
            >>> task_id = client.create_on_demand_ohlcv_export(
            ...     base_instrument="TXFR1",
            ...     base_freq="1min",
            ...     indicator_specs=specs
            ... )

            >>> # 方式 2: 使用 IndicatorSpec.model_dump()
            >>> from tradepose_client.models import create_indicator_spec, Indicator, Freq
            >>>
            >>> sma_spec = create_indicator_spec(
            ...     freq=Freq.HOUR_1,
            ...     indicator=Indicator.sma(period=20),
            ...     shift=1
            ... )
            >>> atr_spec = create_indicator_spec(
            ...     freq=Freq.HOUR_1,
            ...     indicator=Indicator.atr(period=14),
            ...     shift=1
            ... )
            >>>
            >>> task_id = client.create_on_demand_ohlcv_export(
            ...     base_instrument="TXFR1",
            ...     base_freq="1min",
            ...     indicator_specs=[
            ...         sma_spec.model_dump(exclude_none=True),
            ...         atr_spec.model_dump(exclude_none=True)
            ...     ]
            ... )
        """
        payload = {
            "base_instrument": base_instrument,
            "base_freq": base_freq,
            "indicator_specs": indicator_specs,
        }

        if start_date:
            payload["start_date"] = start_date
        if end_date:
            payload["end_date"] = end_date

        response = requests.post(
            f"{self.api_url}/api/v1/backtest/export/on-demand-ohlcv",
            json=payload,
            headers=self._get_headers(),
        )
        response.raise_for_status()

        result = response.json()
        return result["export_task_id"]

    def quick_on_demand_ohlcv(
        self,
        base_instrument: str,
        base_freq: str,
        indicator_specs: List[Dict[str, Any]],
        start_date: Optional[str] = None,
        end_date: Optional[str] = None,
        save_path: Optional[str] = None,
    ) -> pl.DataFrame:
        """快速導出 on-demand-ohlcv（一鍵完成所有步驟）

        創建任務 → 等待完成 → 下載 Parquet → 讀取

        特點：無需預先註冊策略，直接指定指標規格即可導出

        Args:
            base_instrument: 基礎商品 ID
            base_freq: 基礎頻率
            indicator_specs: 指標規格列表
            start_date: 開始時間 (ISO 8601)
            end_date: 結束時間 (ISO 8601)
            save_path: Parquet 保存路徑（可選）

        Returns:
            Polars DataFrame

        Example:
            >>> from tradepose_client import TradeposeClient
            >>> from tradepose_client.models import create_indicator_spec, Indicator, Freq
            >>>
            >>> client = TradeposeClient(
            ...     api_url="http://localhost:8080",
            ...     api_token="your_jwt_token"
            ... )
            >>>
            >>> # 定義指標
            >>> sma_20 = create_indicator_spec(Freq.HOUR_1, Indicator.sma(20), shift=1)
            >>> ema_50 = create_indicator_spec(Freq.HOUR_1, Indicator.ema(50), shift=1)
            >>> atr_14 = create_indicator_spec(Freq.HOUR_1, Indicator.atr(14), shift=1)
            >>>
            >>> # 快速導出（無需註冊策略）
            >>> df = client.quick_on_demand_ohlcv(
            ...     base_instrument="TXFR1",
            ...     base_freq="1min",
            ...     indicator_specs=[
            ...         sma_20.model_dump(exclude_none=True),
            ...         ema_50.model_dump(exclude_none=True),
            ...         atr_14.model_dump(exclude_none=True)
            ...     ],
            ...     start_date="2025-01-01T00:00:00",
            ...     end_date="2025-01-02T23:59:59",
            ...     save_path="./on_demand_data.parquet"
            ... )
            >>>
            >>> print(df.columns)
            >>> # ['ts', 'open', 'high', 'low', 'close', 'volume',
            >>> #  '1h_SMA|20.close', '1h_EMA|50.close', '1h_ATR|14']
        """
        # 創建任務
        print(f"🚀 創建 on-demand-ohlcv 導出任務...")
        print(f"   商品: {base_instrument}")
        print(f"   頻率: {base_freq}")
        print(f"   指標數: {len(indicator_specs)}")

        task_id = self.create_on_demand_ohlcv_export(
            base_instrument=base_instrument,
            base_freq=base_freq,
            indicator_specs=indicator_specs,
            start_date=start_date,
            end_date=end_date,
        )
        print(f"   Task ID: {task_id}")

        # 等待完成
        print(f"⏳ 等待任務完成...")
        self.wait_for_export(task_id, verbose=True)

        # 下載並讀取
        print(f"📥 下載並讀取數據...")
        file_or_buffer = self.download_parquet(task_id, output_path=save_path)
        df = pl.read_parquet(file_or_buffer)

        print(f"✅ 完成: {len(df):,} 行 × {len(df.columns)} 列")

        return df
