"""AkShare-based futures derived/report data provider.

基于 AkShare 的期货衍生/报表数据提供方（Track A），
对接基差、展期收益率、持仓排名、仓单日报与库存等数据。
"""

from __future__ import annotations

from collections.abc import Iterable

import akshare as ak
import pandas as pd

from ..cache import cache
from .derived_base import FuturesDerivedDataProvider, FuturesExchange, WarehouseExchange

# ---------------------------------------------------------------------------
# Monkey-patch: fix akshare pandas 3.x compatibility
# akshare's _check_information assigns float to str-dtype columns, which
# fails on pandas >= 3.0 (strict type enforcement). Converting to object
# dtype before processing avoids the issue.
# TODO: remove once akshare fixes this upstream.
# ---------------------------------------------------------------------------
try:
    import akshare.futures.futures_basis as _fb

    _original_check_information = _fb._check_information

    def _patched_check_information(
        df_data: pd.DataFrame, date: object
    ) -> pd.DataFrame:
        """Wrapper that relaxes str-typed columns to object before processing."""
        return _original_check_information(df_data.astype(object), date)

    _fb._check_information = _patched_check_information
except Exception:
    pass


class AkShareFuturesDerivedProvider(FuturesDerivedDataProvider):
    """AkShare-based futures derived/report data provider.

    基于 AkShare 的期货衍生/报表数据提供方（Track A）。
    """

    @cache(
        "futures_basis_cache",
        key=lambda self, date, symbol: f"akshare_futures_basis_{date}_{symbol}",
    )
    def get_basis(self, date: str, symbol: str) -> pd.DataFrame:
        """Fetch spot price and basis for a given date and variety.

        获取指定交易日与品种的现货价格与基差。

        Args:
            date: Trade date, format 'YYYYMMDD' or 'YYYY-MM-DD'.
            symbol: Variety code (e.g., 'RB', 'AL').

        Returns:
            pd.DataFrame with basis columns.

        Raises:
            ValueError: If request fails.
        """
        try:
            trade_date, out_date = self._normalize_dates(date)
            symbol_upper = symbol.upper().strip()

            raw_df = ak.futures_spot_price(date=trade_date)
            if raw_df.empty:
                return self._empty_df(_BASIS_COLUMNS)

            df = raw_df.copy()
            if "symbol" in df.columns:
                df["symbol"] = df["symbol"].astype(str).str.upper()
                df = df[df["symbol"] == symbol_upper].reset_index(drop=True)
            else:
                raise ValueError("Column 'symbol' not found in futures_spot_price output.")

            if df.empty:
                return self._empty_df(_BASIS_COLUMNS)

            df = df.rename(columns={
                "near_contract_price": "near_price",
                "dominant_contract": "main_contract",
                "dominant_contract_price": "main_price",
                "dom_basis": "main_basis",
                "dom_basis_rate": "main_basis_rate",
            })
            df["date"] = out_date

            for col in ["spot_price", "near_price", "main_price",
                        "near_basis", "main_basis", "near_basis_rate", "main_basis_rate"]:
                if col in df.columns:
                    df[col] = pd.to_numeric(df[col], errors="coerce")

            return df[[c for c in _BASIS_COLUMNS if c in df.columns]].reset_index(drop=True)
        except Exception as e:
            raise ValueError(f"Failed to fetch futures basis: {e}") from e

    @cache(
        "futures_roll_yield_cache",
        key=lambda self, date, symbol: f"akshare_futures_roll_yield_{date}_{symbol}",
    )
    def get_roll_yield(self, date: str, symbol: str) -> pd.DataFrame:
        """Fetch roll yield for a given date and variety.

        获取指定交易日与品种的展期收益率。

        Args:
            date: Trade date.
            symbol: Variety code (e.g., 'RB', 'AL').

        Returns:
            pd.DataFrame with roll yield columns.

        Raises:
            ValueError: If request fails.
        """
        try:
            trade_date, out_date = self._normalize_dates(date)
            symbol_upper = symbol.upper().strip()

            ry = ak.get_roll_yield(date=trade_date, var=symbol_upper)
            if not ry or ry is False:
                return self._empty_df(_ROLL_YIELD_COLUMNS)

            roll_yield, near_by, deferred = ry
            df = pd.DataFrame([{
                "date": out_date,
                "symbol": symbol_upper,
                "roll_yield": roll_yield,
                "near_contract": near_by,
                "deferred_contract": deferred,
            }])
            df["roll_yield"] = pd.to_numeric(df["roll_yield"], errors="coerce")
            return df.reset_index(drop=True)
        except Exception as e:
            raise ValueError(f"Failed to fetch futures roll yield: {e}") from e

    @cache(
        "futures_rank_cache",
        key=lambda self, date, symbol, exchange: f"akshare_rank_{exchange}_{date}_{symbol}",
    )
    def get_position_ranking(
        self, date: str, symbol: str, exchange: FuturesExchange
    ) -> pd.DataFrame:
        """Fetch member position ranking for a given exchange.

        获取指定交易日、品种与交易所的持仓排名。

        Args:
            date: Trade date.
            symbol: Variety code (e.g., 'RB', 'AL').
            exchange: Exchange code.

        Returns:
            pd.DataFrame with position ranking columns.

        Raises:
            ValueError: If request fails.
            NotImplementedError: If exchange is not supported.
        """
        try:
            trade_date, out_date = self._normalize_dates(date)
            symbol_upper = symbol.upper().strip()

            rank_data: dict[str, pd.DataFrame] | None = None
            if exchange == "shfe":
                rank_data = ak.get_shfe_rank_table(date=trade_date, vars_list=[symbol_upper])
            elif exchange == "dce":
                rank_data = ak.get_dce_rank_table(date=trade_date, vars_list=[symbol_upper])
            elif exchange == "cffex":
                rank_data = ak.get_cffex_rank_table(date=trade_date, vars_list=[symbol_upper])
            elif exchange == "czce":
                rank_data = ak.get_rank_table_czce(date=trade_date)
            elif exchange == "gfex":
                raise NotImplementedError("GFEX position ranking not available in current AkShare.")
            else:
                raise ValueError(f"Unsupported exchange: {exchange}")

            if not rank_data:
                return self._empty_df(_POSITION_RANKING_COLUMNS)

            frames = [df for df in rank_data.values()
                      if isinstance(df, pd.DataFrame) and not df.empty]
            if not frames:
                return self._empty_df(_POSITION_RANKING_COLUMNS)

            df_all = pd.concat(frames, ignore_index=True)
            df_all = df_all.rename(columns={
                "var": "variety",
                "symbol": "contract",
                "vol_party_name": "volume_member",
                "vol": "volume",
                "vol_chg": "volume_change",
                "long_party_name": "long_member",
                "long_open_interest_chg": "long_open_interest_change",
                "short_party_name": "short_member",
                "short_open_interest_chg": "short_open_interest_change",
            })

            if "variety" in df_all.columns:
                df_all["variety"] = df_all["variety"].astype(str).str.upper()
                df_all = df_all[df_all["variety"] == symbol_upper].reset_index(drop=True)

            if "date" in df_all.columns:
                df_all["date"] = (
                    pd.to_datetime(df_all["date"], errors="coerce")
                    .dt.date.astype(str).fillna(out_date)
                )
            else:
                df_all.insert(0, "date", out_date)

            df_all.insert(1, "exchange", exchange)

            for col in ["rank", "volume", "volume_change",
                        "long_open_interest", "long_open_interest_change",
                        "short_open_interest", "short_open_interest_change"]:
                if col in df_all.columns:
                    df_all[col] = pd.to_numeric(df_all[col], errors="coerce")

            return df_all[[c for c in _POSITION_RANKING_COLUMNS
                           if c in df_all.columns]].reset_index(drop=True)
        except NotImplementedError:
            raise
        except Exception as e:
            raise ValueError(f"Failed to fetch position ranking: {e}") from e

    @cache(
        "futures_warehouse_cache",
        key=lambda self, date, exchange: f"akshare_warehouse_{exchange}_{date}",
    )
    def get_warehouse_receipt(self, date: str, exchange: WarehouseExchange) -> pd.DataFrame:
        """Fetch warehouse receipt report.

        获取指定交易日与交易所的仓单日报。

        Args:
            date: Trade date.
            exchange: Exchange code ('shfe', 'dce', 'czce', 'gfex').

        Returns:
            pd.DataFrame with warehouse receipt columns.

        Raises:
            ValueError: If request fails.
        """
        try:
            trade_date, out_date = self._normalize_dates(date)

            raw: pd.DataFrame | dict[str, pd.DataFrame]
            if exchange == "dce":
                raw = ak.futures_warehouse_receipt_dce(date=trade_date)
            elif exchange == "czce":
                raw = ak.futures_warehouse_receipt_czce(date=trade_date)
            elif exchange == "shfe":
                raw = ak.futures_shfe_warehouse_receipt(date=trade_date)
            elif exchange == "gfex":
                raw = ak.futures_gfex_warehouse_receipt(date=trade_date)
            else:
                raise ValueError(f"Unsupported exchange: {exchange}")

            frames: list[pd.DataFrame] = []
            if isinstance(raw, pd.DataFrame):
                frames.append(self._normalize_warehouse_piece(raw, symbol_fallback=None))
            elif isinstance(raw, dict):
                for key, piece in raw.items():
                    if isinstance(piece, pd.DataFrame) and not piece.empty:
                        frames.append(
                            self._normalize_warehouse_piece(piece, symbol_fallback=str(key))
                        )
            else:
                return self._empty_df(_WAREHOUSE_COLUMNS)

            if not frames:
                return self._empty_df(_WAREHOUSE_COLUMNS)

            df_all = pd.concat(frames, ignore_index=True)
            df_all.insert(0, "date", out_date)
            df_all.insert(1, "exchange", exchange)

            return df_all[[c for c in _WAREHOUSE_COLUMNS
                           if c in df_all.columns]].reset_index(drop=True)
        except Exception as e:
            raise ValueError(f"Failed to fetch warehouse receipt: {e}") from e

    @cache(
        "futures_inventory_cache",
        key=lambda self, symbol: f"akshare_futures_inventory_{symbol}",
    )
    def get_inventory(self, symbol: str) -> pd.DataFrame:
        """Fetch futures inventory data.

        获取指定品种的库存数据（东方财富来源）。

        Args:
            symbol: Variety code or Chinese name.

        Returns:
            pd.DataFrame with columns: date, symbol, inventory, inventory_change

        Raises:
            ValueError: If request fails.
        """
        try:
            raw_df = ak.futures_inventory_em(symbol=symbol)
            if raw_df.empty:
                return self._empty_df(["date", "symbol", "inventory", "inventory_change"])

            df = raw_df.rename(
                columns={"日期": "date", "库存": "inventory", "增减": "inventory_change"}
            )
            df["date"] = pd.to_datetime(df["date"], errors="coerce").dt.date.astype(str)
            df["symbol"] = symbol
            df["inventory"] = pd.to_numeric(df["inventory"], errors="coerce")
            df["inventory_change"] = pd.to_numeric(df["inventory_change"], errors="coerce")

            return df[["date", "symbol", "inventory", "inventory_change"]].reset_index(drop=True)
        except Exception as e:
            raise ValueError(f"Failed to fetch futures inventory: {e}") from e

    @staticmethod
    def _normalize_dates(date: str) -> tuple[str, str]:
        """Normalize date for upstream call (YYYYMMDD) and output (ISO).

        Args:
            date: Input date.

        Returns:
            tuple: (trade_date_yyyymmdd, out_date_iso)

        Raises:
            ValueError: If date cannot be parsed.
        """
        try:
            dt = pd.to_datetime(date, errors="raise")
        except Exception as e:
            raise ValueError(f"Invalid date format: {date}") from e
        return dt.strftime("%Y%m%d"), dt.date().isoformat()

    @staticmethod
    def _empty_df(columns: Iterable[str]) -> pd.DataFrame:
        """Create empty DataFrame with specified columns.

        Args:
            columns: Column names.

        Returns:
            pd.DataFrame: Empty DataFrame.
        """
        return pd.DataFrame(columns=list(columns))

    @staticmethod
    def _pick_column(df: pd.DataFrame, candidates: list[str]) -> str | None:
        """Pick first existing column from candidates.

        Args:
            df: Input DataFrame.
            candidates: Candidate column names.

        Returns:
            str | None: Selected column or None.
        """
        for col in candidates:
            if col in df.columns:
                return col
        return None

    def _normalize_warehouse_piece(
        self, df: pd.DataFrame, symbol_fallback: str | None
    ) -> pd.DataFrame:
        """Normalize a single warehouse receipt DataFrame piece.

        Args:
            df: Raw DataFrame.
            symbol_fallback: Fallback symbol if df lacks symbol column.

        Returns:
            pd.DataFrame with columns: symbol, warehouse, receipt_prev, receipt, receipt_change
        """
        symbol_col = self._pick_column(
            df, ["品种代码", "symbol", "VARNAME", "品种", "品种名称"]
        )
        warehouse_col = self._pick_column(
            df, ["仓库/分库", "仓库", "WHABBRNAME", "仓库名称"]
        )
        prev_col = self._pick_column(
            df, ["昨日仓单量（手）", "昨日仓单量", "LASTWRTWGHTS", "LASTWBQTY", "昨日", "昨仓单"]
        )
        curr_col = self._pick_column(
            df, ["今日仓单量（手）", "今日仓单量", "WRTWGHTS", "WBQTY", "今日", "仓单"]
        )
        chg_col = self._pick_column(
            df, ["增减（手）", "增减", "WBDIFF", "regWbillQty", "变化"]
        )

        out = pd.DataFrame({
            "symbol": df[symbol_col] if symbol_col else symbol_fallback,
            "warehouse": df[warehouse_col] if warehouse_col else None,
            "receipt_prev": df[prev_col] if prev_col else None,
            "receipt": df[curr_col] if curr_col else None,
            "receipt_change": df[chg_col] if chg_col else None,
        })

        for col in ["receipt_prev", "receipt", "receipt_change"]:
            out[col] = pd.to_numeric(out[col], errors="coerce")

        if "symbol" in out.columns:
            out["symbol"] = out["symbol"].astype(str).str.strip()

        return out[["symbol", "warehouse", "receipt_prev", "receipt",
                     "receipt_change"]].reset_index(drop=True)


_BASIS_COLUMNS = [
    "date", "symbol", "spot_price", "near_contract", "near_price",
    "main_contract", "main_price", "near_basis", "main_basis",
    "near_basis_rate", "main_basis_rate",
]

_ROLL_YIELD_COLUMNS = [
    "date", "symbol", "roll_yield", "near_contract", "deferred_contract",
]

_POSITION_RANKING_COLUMNS = [
    "date", "exchange", "variety", "contract", "rank",
    "volume_member", "volume", "volume_change",
    "long_member", "long_open_interest", "long_open_interest_change",
    "short_member", "short_open_interest", "short_open_interest_change",
]

_WAREHOUSE_COLUMNS = [
    "date", "exchange", "symbol", "warehouse",
    "receipt_prev", "receipt", "receipt_change",
]
