import typing
from importlib import metadata

import pandas as pd

try:
    __version__ = metadata.version("akquant")
except metadata.PackageNotFoundError:
    __version__ = "0.0.0+unknown"

__engine_rule_version__ = "1.0.0"  # Increment on behavior-changing updates

from . import akquant as _akquant
from . import talib
from .akquant import *  # noqa: F403
from .akquant import ATR, EMA, MACD, RSI, SMA, BollingerBands
from .analyzer_plugin import AnalyzerManager, AnalyzerTemplate
from .backtest import (  # type: ignore
    BacktestResult,
    BacktestStreamEvent,
    run_backtest,
    run_warm_start,
)
from .checkpoint import save_snapshot, warm_start
from .config import BacktestConfig, InstrumentConfig, StrategyConfig, strategy_config
from .data import DataLoader
from .feed_adapter import (
    CSVFeedAdapter,
    DataFeedAdapter,
    FeedSlice,
    ParquetFeedAdapter,
    ReplayFeedAdapter,
    ResampledFeedAdapter,
)
from .indicator import Indicator, IndicatorSet
from .log import get_logger, register_logger
from .optimize import OptimizationResult, run_grid_search, run_walk_forward
from .params import (
    BoolParam,
    ChoiceParam,
    DateRange,
    DateRangeParam,
    FloatParam,
    IntParam,
    ParamModel,
)
from .params_adapter import (
    build_param_grid_from_search_space,
    extract_runtime_kwargs,
    get_strategy_param_schema,
    resolve_param_model,
    validate_strategy_params,
)
from .plot import plot_result
from .sizer import AllInSizer, FixedSize, PercentSizer, Sizer
from .strategy import Strategy, StrategyRuntimeConfig
from .strategy_loader import register_strategy_loader, resolve_strategy_input
from .utils import fetch_akshare_symbol, load_bar_from_df, prepare_dataframe

__doc__ = _akquant.__doc__
if hasattr(_akquant, "__all__"):  # noqa: F405
    __all__ = _akquant.__all__ + [  # noqa: F405
        "load_bar_from_df",
        "prepare_dataframe",
        "fetch_akshare_symbol",
        "Sizer",
        "FixedSize",
        "PercentSizer",
        "AllInSizer",
        "Strategy",
        "StrategyRuntimeConfig",
        "register_strategy_loader",
        "resolve_strategy_input",
        "DataLoader",
        "DataFeedAdapter",
        "FeedSlice",
        "CSVFeedAdapter",
        "ParquetFeedAdapter",
        "ResampledFeedAdapter",
        "ReplayFeedAdapter",
        "get_logger",
        "register_logger",
        "strategy_config",
        "Indicator",
        "IndicatorSet",
        "run_backtest",
        "run_warm_start",
        "plot_result",
        "BacktestResult",
        "BacktestStreamEvent",
        "run_grid_search",
        "run_walk_forward",
        "OptimizationResult",
        "BacktestConfig",
        "StrategyConfig",
        "InstrumentConfig",
        "save_snapshot",
        "warm_start",
        "SMA",
        "EMA",
        "MACD",
        "RSI",
        "BollingerBands",
        "ATR",
        "AnalyzerManager",
        "AnalyzerTemplate",
        "ParamModel",
        "DateRange",
        "IntParam",
        "FloatParam",
        "BoolParam",
        "ChoiceParam",
        "DateRangeParam",
        "resolve_param_model",
        "get_strategy_param_schema",
        "validate_strategy_params",
        "extract_runtime_kwargs",
        "build_param_grid_from_search_space",
        "talib",
    ]
else:
    __all__ = [
        "load_bar_from_df",
        "prepare_dataframe",
        "fetch_akshare_symbol",
        "Sizer",
        "FixedSize",
        "PercentSizer",
        "AllInSizer",
        "Strategy",
        "StrategyRuntimeConfig",
        "register_strategy_loader",
        "resolve_strategy_input",
        "DataLoader",
        "DataFeedAdapter",
        "FeedSlice",
        "CSVFeedAdapter",
        "ParquetFeedAdapter",
        "ResampledFeedAdapter",
        "ReplayFeedAdapter",
        "get_logger",
        "register_logger",
        "strategy_config",
        "Indicator",
        "IndicatorSet",
        "run_backtest",
        "run_warm_start",
        "plot_result",
        "BacktestResult",
        "BacktestStreamEvent",
        "run_grid_search",
        "run_walk_forward",
        "OptimizationResult",
        "BacktestConfig",
        "StrategyConfig",
        "InstrumentConfig",
        "save_snapshot",
        "warm_start",
        "SMA",
        "EMA",
        "MACD",
        "RSI",
        "BollingerBands",
        "ATR",
        "AnalyzerManager",
        "AnalyzerTemplate",
        "ParamModel",
        "DateRange",
        "IntParam",
        "FloatParam",
        "BoolParam",
        "ChoiceParam",
        "DateRangeParam",
        "resolve_param_model",
        "get_strategy_param_schema",
        "validate_strategy_params",
        "extract_runtime_kwargs",
        "build_param_grid_from_search_space",
        "talib",
    ]


def create_bar(
    timestamp: int,
    open_px: float,
    high_px: float,
    low_px: float,
    close_px: float,
    volume: float,
    symbol: str,
) -> Bar:  # noqa: F405
    """创建 Bar 对象的辅助函数."""
    return Bar(timestamp, open_px, high_px, low_px, close_px, volume, symbol)  # noqa: F405


def _engine_set_timezone_name(self: Engine, tz_name: str) -> None:  # noqa: F405
    """
    通过时区名称设置引擎时区.

    :param tz_name: 时区名称，例如 "Asia/Shanghai", "UTC", "US/Eastern"
    """
    import datetime

    try:
        import zoneinfo

        tz: typing.Union[datetime.tzinfo, typing.Any] = zoneinfo.ZoneInfo(tz_name)
    except ImportError:
        import pytz

        tz = pytz.timezone(tz_name)

    # Get offset for current time (approximate is usually fine for constant
    # offset zones, but for DST aware zones, we might want a specific date.
    # For simplicity and standard market hours, we use current date or a fixed date)
    now = datetime.datetime.now(tz)
    utc_offset = now.utcoffset()
    if utc_offset is None:
        offset = 0
    else:
        offset = int(utc_offset.total_seconds())
    self.set_timezone(offset)


# Patch Engine class
Engine.set_timezone_name = _engine_set_timezone_name  # type: ignore # noqa: F405


def _engine_get_orders_dataframe(self: Engine) -> "pd.DataFrame":  # noqa: F405
    """
    Get all orders as a pandas DataFrame.

    :return: pd.DataFrame with order details.
    """
    import pandas as pd

    orders = self.orders
    if not orders:
        return pd.DataFrame()

    data = []
    for o in orders:
        # Convert nanosecond timestamp to datetime
        created_at_dt = pd.Timestamp(o.created_at, unit="ns", tz="UTC")
        updated_at_dt = pd.Timestamp(o.updated_at, unit="ns", tz="UTC")

        data.append(
            {
                "order_id": str(o.id),  # UUID usually
                "symbol": o.symbol,
                "side": str(o.side),
                "status": str(o.status),
                "price": o.price,
                "quantity": o.quantity,
                "filled": o.filled_quantity,
                "avg_price": o.average_filled_price,
                "created_at": created_at_dt,
                "updated_at": updated_at_dt,
                "reject_reason": o.reject_reason or "",
            }
        )
    return pd.DataFrame(data)


def _engine_configure_risk(
    self: Engine,  # noqa: F405
    max_position_pct: typing.Optional[float] = None,
    sector_concentration: typing.Optional[
        typing.Tuple[float, typing.Dict[str, str]]
    ] = None,
) -> None:
    """
    Configure risk management rules (Shortcut).

    :param max_position_pct: Max position size as a percentage of total equity
                             (e.g. 0.10 for 10%)
    :param sector_concentration: Tuple of (limit, sector_map) e.g.
                                 (0.15, {"600519": "Consumer"})
    """
    # Get current risk manager (might be a copy)
    rm = self.risk_manager

    if max_position_pct is not None:
        rm.add_max_position_percent_rule(max_position_pct)

    if sector_concentration is not None:
        limit, sector_map = sector_concentration
        rm.add_sector_concentration_rule(limit, sector_map)

    # Set it back
    self.risk_manager = rm


Engine.get_orders_dataframe = _engine_get_orders_dataframe  # type: ignore # noqa: F405
Engine.configure_risk = _engine_configure_risk  # type: ignore # noqa: F405
