"""Resolvers for market-specific ticker transformations and helpers."""

from __future__ import annotations

import logging
import re
from typing import TYPE_CHECKING

import pandas as pd

from xbbg import const

if TYPE_CHECKING:
    from xbbg.core.domain.context import BloombergContext

logger = logging.getLogger(__name__)

# Month code to month number mapping for futures contracts
MONTH_CODE_MAP = {
    "F": 1,
    "G": 2,
    "H": 3,
    "J": 4,
    "K": 5,
    "M": 6,
    "N": 7,
    "Q": 8,
    "U": 9,
    "V": 10,
    "X": 11,
    "Z": 12,
}


def _parse_generic_ticker(gen_ticker: str) -> tuple[str, int, str]:
    """Parse a generic futures ticker into components.

    Args:
        gen_ticker: Generic ticker like 'ES1 Index', 'CL2 Comdty', '7203 1 JT Equity'

    Returns:
        Tuple of (root, n, asset_type) where:
        - root: The ticker root (e.g., 'ES', 'CL', '7203')
        - n: The contract number (1 = front month, 2 = second, etc.)
        - asset_type: The asset type (e.g., 'Index', 'Comdty', 'JT Equity')

    Raises:
        ValueError: If the ticker format is not recognized.
    """
    t_info = gen_ticker.split()
    asset = t_info[-1]

    if asset in ["Index", "Curncy", "Comdty"]:
        ticker = " ".join(t_info[:-1])
        root = ticker[:-1]
        n = int(ticker[-1])
        asset_type = asset
    elif asset == "Equity":
        ticker = t_info[0]
        root = ticker[:-1]
        n = int(ticker[-1])
        asset_type = " ".join(t_info[1:])
    else:
        raise ValueError(f"Unknown asset type for generic ticker: {gen_ticker}")

    return root, n, asset_type


def _get_cycle_months(gen_ticker: str) -> str:
    """Get the contract cycle months from Bloomberg.

    Args:
        gen_ticker: Generic futures ticker like 'ES1 Index'

    Returns:
        String of month codes (e.g., 'HMUZ' for quarterly, 'FGHJKMNQUVXZ' for monthly)
    """
    from xbbg.api.reference import bdp  # noqa: PLC0415

    result = bdp(gen_ticker, "FUT_GEN_MONTH")
    if result.empty:
        logger.warning("Could not get FUT_GEN_MONTH for %s", gen_ticker)
        return ""
    return result.iloc[0, 0]


def _construct_contract_ticker(
    root: str, month_code: str, year: int, asset_type: str, use_single_digit_year: bool = False
) -> str:
    """Construct a specific futures contract ticker.

    Args:
        root: Ticker root (e.g., 'ES', 'CL')
        month_code: Month code (e.g., 'H', 'M', 'U', 'Z')
        year: Full year (e.g., 2024)
        asset_type: Asset type (e.g., 'Index', 'Comdty')
        use_single_digit_year: If True, use single digit year (e.g., 'ESH5' instead of 'ESH25')

    Returns:
        Contract ticker (e.g., 'ESH24 Index')
    """
    year_str = str(year)[-1] if use_single_digit_year else str(year)[-2:]
    return f"{root}{month_code}{year_str} {asset_type}"


def active_futures(ticker: str, dt, **kwargs) -> str:
    """Active futures contract.

    Determines the most actively traded futures contract by comparing volume
    between the front-month and second-month contracts.

    Args:
        ticker: Generic futures ticker, i.e., UX1 Index, ESA Index, Z A Index, CLA Comdty, etc.
            Must be a generic contract (e.g., UX1 Index), not a specific contract (e.g., UXZ5 Index).
        dt: date
        **kwargs: Passed through to downstream resolvers (e.g., logging).

    Returns:
        str: ticker name

    Raises:
        ValueError: If ticker is a specific contract instead of a generic one.
    """
    # Check if ticker is already a specific contract (contains month codes)
    month_codes = set(const.Futures.values())  # {'F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z'}
    ticker_base = ticker.rsplit(" ", 1)[0]  # Remove asset type (Index, Comdty, etc.)

    # Generic tickers end with just a number (1, 2, etc.) like UX1, ESA1, ZA1
    # Specific contracts end with month code + year digits like UXZ5, UXZ24, ESAM24
    # Pattern: ends with [month_code][1-2 digits] where month_code is immediately before digits
    month_code_pattern = rf"[{re.escape(''.join(month_codes))}]"
    # Match pattern: [anything][month_code][1-2 digits] at the end
    match = re.search(rf"(.+)({month_code_pattern})(\d{{1,2}})$", ticker_base)
    if match:
        prefix, month_char, digits = match.groups()
        # If it ends with [month_code][2 digits] it's definitely specific
        # If it ends with [month_code][1 digit], check length: very short (3 chars) is likely generic
        if len(digits) == 2:
            # Two digit year = definitely specific contract
            raise ValueError(
                f"'{ticker}' appears to be a specific futures contract (ends with month code + 2-digit year), "
                f"not a generic one. Please use a generic ticker (e.g., 'UX1 Index' instead of 'UXZ24 Index'). "
                f"Generic tickers end with a number (1, 2, etc.) before the asset type."
            )
        # Single digit: could be generic (UX1) or specific (UXZ5)
        # Check length: very short (3 chars) with single digit is likely generic
        if len(digits) == 1 and len(ticker_base) > 3:
            # Longer ticker ending in [month_code][digit] is likely specific (e.g., "UXZ5", "ESAM4")
            raise ValueError(
                f"'{ticker}' appears to be a specific futures contract, not a generic one. "
                f"Please use a generic ticker (e.g., 'UX1 Index' instead of 'UXZ5 Index'). "
                f"Generic tickers end with a number (1, 2, etc.) before the asset type."
            )

    # Import directly from API modules to avoid circular dependency
    from xbbg.api.reference import bdp  # noqa: PLC0415

    t_info = ticker.split()
    prefix, asset = " ".join(t_info[:-1]), t_info[-1]

    # Construct the generic tickers for front and second month
    f1, f2 = f"{prefix[:-1]}1 {asset}", f"{prefix[:-1]}2 {asset}"

    # Get specific contracts using Bloomberg-based resolution
    fut_1 = fut_ticker(gen_ticker=f1, dt=dt, **kwargs)
    fut_2 = fut_ticker(gen_ticker=f2, dt=dt, **kwargs)

    if not fut_1:
        logger.error("Failed to resolve front-month contract for %s", f1)
        return ""

    if not fut_2:
        # If we can't get second month, just return front month
        return fut_1

    fut_tk = bdp(tickers=[fut_1, fut_2], flds="last_tradeable_dt")

    if fut_tk.empty or "last_tradeable_dt" not in fut_tk.columns:
        return fut_1

    first_matu = pd.Timestamp(fut_tk["last_tradeable_dt"].iloc[0])
    if pd.Timestamp(dt).month < first_matu.month:
        return fut_1

    dts = pd.bdate_range(end=dt, periods=10)
    from xbbg.api.historical import bdh  # noqa: PLC0415

    volume = bdh(tickers=list(fut_tk.index), flds="volume", start_date=dts[0], end_date=dts[-1])
    if volume.empty:
        return fut_1
    return volume.iloc[-1].idxmax()[0]


def fut_ticker(gen_ticker: str, dt, **kwargs) -> str:
    """Get specific futures contract ticker from generic ticker.

    Uses Bloomberg's FUT_GEN_MONTH field to determine the contract cycle,
    then constructs candidate contracts and queries their expiration dates.

    Args:
        gen_ticker: Generic ticker (e.g., 'ES1 Index', 'CL2 Comdty')
        dt: Date to resolve for
        **kwargs: Passed through to Bloomberg calls (e.g., timeout).

    Returns:
        Specific contract ticker (e.g., 'ESH24 Index')
    """
    from xbbg.api.reference import bdp  # noqa: PLC0415

    dt = pd.Timestamp(dt)

    # Parse the generic ticker
    try:
        root, n, asset_type = _parse_generic_ticker(gen_ticker)
    except ValueError as e:
        logger.error(str(e))
        return ""

    # Get cycle months from Bloomberg
    cycle_months = _get_cycle_months(gen_ticker)
    if not cycle_months:
        logger.error("Could not determine contract cycle for %s", gen_ticker)
        return ""

    if logger.isEnabledFor(logging.DEBUG):
        logger.debug("Contract cycle for %s: %s", gen_ticker, cycle_months)

    # Determine if we should use single-digit year (for current year contracts)
    pre_dt = pd.bdate_range(end="today", periods=1)[-1]
    use_single_digit_year = (pre_dt.month == dt.month) and (pre_dt.year == dt.year)

    # Generate candidate contracts for a range of years
    start_year = dt.year - 1
    end_year = dt.year + 3

    candidates = []
    for year in range(start_year, end_year + 1):
        for month_code in cycle_months:
            ticker = _construct_contract_ticker(root, month_code, year, asset_type, use_single_digit_year)
            candidates.append(ticker)

    if logger.isEnabledFor(logging.DEBUG):
        logger.debug("Generated %d candidate contracts for %s", len(candidates), gen_ticker)

    # Query expiration dates from Bloomberg
    try:
        exp_dates = bdp(tickers=candidates, flds="last_tradeable_dt")
    except Exception as e:
        logger.error("Failed to query expiration dates for %s: %s", gen_ticker, e)
        return ""

    if exp_dates.empty or "last_tradeable_dt" not in exp_dates.columns:
        logger.warning("No valid futures contracts found for %s", gen_ticker)
        return ""

    # Filter and sort by expiration date
    exp_dates = exp_dates.dropna(subset=["last_tradeable_dt"])
    exp_dates["last_tradeable_dt"] = pd.to_datetime(exp_dates["last_tradeable_dt"])
    exp_dates = exp_dates.sort_values("last_tradeable_dt")

    # Filter contracts expiring after dt
    future_contracts = exp_dates[exp_dates["last_tradeable_dt"] > dt]

    if len(future_contracts) < n:
        logger.warning(
            "Not enough contracts expiring after %s for %s (need %d, found %d)",
            dt.date(),
            gen_ticker,
            n,
            len(future_contracts),
        )
        return ""

    # Return the Nth contract (1-indexed, so n-1 for 0-indexed)
    result = future_contracts.index[n - 1]

    if logger.isEnabledFor(logging.DEBUG):
        logger.debug("Resolved %s @ %s -> %s", gen_ticker, dt.date(), result)

    return result


def cdx_ticker(
    gen_ticker: str,
    dt,
    ctx: BloombergContext | None = None,
    **kwargs,
) -> str:
    """Resolve generic CDX 5Y ticker (e.g., 'CDX IG CDSI GEN 5Y Corp') to concrete series.

    Uses Bloomberg fields:
      - rolling_series: returns current on-the-run series number
      - on_the_run_current_bd_indicator: 'Y' if on-the-run
      - cds_first_accrual_start_date: start date of current series trading

    Args:
        gen_ticker: Generic CDX ticker.
        dt: Date to resolve for.
        ctx: Bloomberg context (infrastructure kwargs only). If None, will be
            extracted from kwargs for backward compatibility.
        **kwargs: Legacy kwargs support. If ctx is provided, kwargs are ignored.

    Returns:
        Resolved ticker string.
    """
    # Logger is module-level
    from xbbg.core.domain.context import split_kwargs

    dt = pd.Timestamp(dt)
    # Import directly from API modules to avoid circular dependency
    from xbbg.api.reference import bdp  # lazy

    # Extract context - prefer explicit ctx, otherwise extract from kwargs
    if ctx is None:
        split = split_kwargs(**kwargs)
        ctx = split.infra

    # Convert context to kwargs for bdp call
    safe_kwargs = ctx.to_kwargs()

    try:
        info = bdp(
            tickers=gen_ticker,
            flds=["rolling_series", "on_the_run_current_bd_indicator", "cds_first_accrual_start_date"],
            **safe_kwargs,
        )
    except Exception as e:
        logger.error("Failed to fetch CDX metadata for generic ticker %s: %s", gen_ticker, e)
        return ""

    if info.empty or "rolling_series" not in info:
        logger.warning("No rolling series configuration found for CDX ticker: %s", gen_ticker)
        return ""

    series = info.loc[gen_ticker, "rolling_series"]
    try:
        series = int(series)
    except Exception:
        series = series

    tokens = gen_ticker.split()
    if "GEN" not in tokens:
        logger.warning("Generic ticker %s does not contain expected GEN token for CDX resolution", gen_ticker)
        return ""
    tokens[tokens.index("GEN")] = f"S{series}"
    resolved = " ".join(tokens)

    # If dt is before first accrual date of current series, use prior series
    faccr_col = "cds_first_accrual_start_date"
    try:
        start_dt = pd.Timestamp(info.loc[gen_ticker, faccr_col]) if faccr_col in info else None
    except Exception:
        start_dt = None

    if (start_dt is not None) and (dt < start_dt) and isinstance(series, int) and series > 1:
        tokens[tokens.index(f"S{series}")] = f"S{series - 1}"
        resolved = " ".join(tokens)

    return resolved


def active_cdx(
    gen_ticker: str,
    dt,
    lookback_days: int = 10,
    ctx: BloombergContext | None = None,
    **kwargs,
) -> str:
    """Choose active CDX series for a date, preferring on-the-run unless it's not started yet.

    If ambiguous, prefer the series with recent non-empty PX_LAST over the lookback window.

    Args:
        gen_ticker: Generic CDX ticker.
        dt: Date to resolve for.
        lookback_days: Number of days to look back for activity.
        ctx: Bloomberg context (infrastructure kwargs only). If None, will be
            extracted from kwargs for backward compatibility.
        **kwargs: Legacy kwargs support. If ctx is provided, kwargs are ignored.

    Returns:
        Active ticker string.
    """
    from xbbg.core.domain.context import split_kwargs

    # Extract context - prefer explicit ctx, otherwise extract from kwargs
    if ctx is None:
        split = split_kwargs(**kwargs)
        ctx = split.infra

    cur = cdx_ticker(gen_ticker=gen_ticker, dt=dt, ctx=ctx)
    if not cur:
        return ""

    # Compute previous series candidate
    parts = cur.split()
    prev = ""
    for i, tok in enumerate(parts):
        if tok.startswith("S") and tok[1:].isdigit():
            s = int(tok[1:])
            if s > 1:
                parts[i] = f"S{s - 1}"
                prev = " ".join(parts)
            break

    # If no prev candidate, return current
    if not prev:
        return cur

    # Import directly from API modules to avoid circular dependency
    from xbbg.api.historical import bdh  # lazy
    from xbbg.api.reference import bdp  # lazy

    # Convert context to kwargs for bdp/bdh calls
    safe_kwargs = ctx.to_kwargs()
    # If dt is before accrual start, prefer prev
    try:
        cur_meta = bdp(cur, ["cds_first_accrual_start_date"], **safe_kwargs)
        cur_start = pd.Timestamp(cur_meta.iloc[0, 0]) if not cur_meta.empty else None
    except Exception:
        cur_start = None

    if (cur_start is not None) and (pd.Timestamp(dt) < cur_start):
        return prev

    # Otherwise, pick one with recent activity (PX_LAST availability)
    end = pd.Timestamp(dt)
    start = end - pd.Timedelta(days=lookback_days)
    try:
        px = bdh([cur, prev], ["PX_LAST"], start_date=start, end_date=end, **safe_kwargs)
        if px.empty:
            return cur
        last_non_na = px.xs("PX_LAST", axis=1, level=1).ffill().iloc[-1]
        return last_non_na.dropna().index[0] if last_non_na.notna().any() else cur
    except Exception:
        return cur
