"""
Utility functions for jbqlab.

This module provides helper functions used across the package.
"""

import hashlib
from datetime import datetime
from typing import Any

import numpy as np
import pandas as pd


def generate_run_id(strategy: str, params: dict[str, Any]) -> str:
    """Generate a unique run ID for a backtest.

    Args:
        strategy: Strategy name.
        params: Strategy parameters.

    Returns:
        Short hash string (8 characters).
    """
    content = f"{strategy}:{sorted(params.items())}:{datetime.now().isoformat()}"
    return hashlib.md5(content.encode()).hexdigest()[:8]


def format_number(value: float, style: str = "default") -> str:
    """Format a number for display.

    Args:
        value: Number to format.
        style: Format style - "percent", "currency", "ratio", or "default".

    Returns:
        Formatted string.
    """
    if pd.isna(value) or np.isinf(value):
        return "N/A"

    if style == "percent":
        return f"{value * 100:+.2f}%"
    elif style == "currency":
        return f"${value:,.2f}"
    elif style == "ratio":
        return f"{value:.2f}"
    else:
        return f"{value:.4f}"


def ensure_datetime_index(df: pd.DataFrame) -> pd.DataFrame:
    """Ensure DataFrame has a DatetimeIndex.

    Args:
        df: DataFrame, potentially with 'date' column.

    Returns:
        DataFrame with DatetimeIndex.
    """
    if isinstance(df.index, pd.DatetimeIndex):
        return df

    df = df.copy()

    if "date" in df.columns:
        df["date"] = pd.to_datetime(df["date"])
        df = df.set_index("date")
    elif "Date" in df.columns:
        df["Date"] = pd.to_datetime(df["Date"])
        df = df.set_index("Date")
        df.index.name = "date"

    return df


def resample_to_frequency(
    df: pd.DataFrame,
    freq: str = "W",
    agg_method: str = "last",
) -> pd.DataFrame:
    """Resample price data to a different frequency.

    Args:
        df: DataFrame with 'close' column and DatetimeIndex.
        freq: Target frequency ('D', 'W', 'M', 'Q', 'Y').
        agg_method: Aggregation method for close ('last', 'mean', 'first').

    Returns:
        Resampled DataFrame.
    """
    df = ensure_datetime_index(df)

    if agg_method == "last":
        resampled = df.resample(freq).last()
    elif agg_method == "mean":
        resampled = df.resample(freq).mean()
    elif agg_method == "first":
        resampled = df.resample(freq).first()
    else:
        raise ValueError(f"Unknown agg_method: {agg_method}")

    return resampled.dropna()


def calculate_returns(
    prices: pd.Series | pd.DataFrame,
    method: str = "simple",
) -> pd.Series | pd.DataFrame:
    """Calculate returns from price series.

    Args:
        prices: Price series or DataFrame with 'close' column.
        method: 'simple' for arithmetic returns, 'log' for log returns.

    Returns:
        Returns series/DataFrame.
    """
    if isinstance(prices, pd.DataFrame):
        prices = prices["close"]

    if method == "simple":
        returns = prices.pct_change()
    elif method == "log":
        returns = np.log(prices / prices.shift(1))
    else:
        raise ValueError(f"Unknown method: {method}")

    return returns.fillna(0)


def rolling_sharpe(
    returns: pd.Series,
    window: int = 252,
    risk_free_rate: float = 0.0,
) -> pd.Series:
    """Calculate rolling Sharpe ratio.

    Args:
        returns: Returns series.
        window: Rolling window size.
        risk_free_rate: Annual risk-free rate.

    Returns:
        Rolling Sharpe ratio series.
    """
    daily_rf = risk_free_rate / 252
    excess_returns = returns - daily_rf

    rolling_mean = excess_returns.rolling(window=window).mean()
    rolling_std = excess_returns.rolling(window=window).std()

    daily_sharpe = rolling_mean / rolling_std
    annualized_sharpe = daily_sharpe * np.sqrt(252)

    return annualized_sharpe


def rolling_volatility(
    returns: pd.Series,
    window: int = 21,
    annualize: bool = True,
) -> pd.Series:
    """Calculate rolling volatility.

    Args:
        returns: Returns series.
        window: Rolling window size.
        annualize: Whether to annualize the volatility.

    Returns:
        Rolling volatility series.
    """
    vol = returns.rolling(window=window).std()

    if annualize:
        vol = vol * np.sqrt(252)

    return vol


def drawdown_series(equity: pd.Series) -> pd.Series:
    """Calculate drawdown series from equity curve.

    Args:
        equity: Equity curve series.

    Returns:
        Drawdown series (positive values represent drawdowns).
    """
    running_max = equity.cummax()
    drawdown = (running_max - equity) / running_max
    return drawdown.fillna(0)


def underwater_periods(equity: pd.Series) -> pd.DataFrame:
    """Identify underwater (drawdown) periods.

    Args:
        equity: Equity curve series.

    Returns:
        DataFrame with start, end, duration, and max drawdown for each period.
    """
    dd = drawdown_series(equity)

    # Find periods where we're in drawdown
    in_drawdown = dd > 0

    # Identify start and end of each drawdown period
    starts = in_drawdown & ~in_drawdown.shift(1).fillna(False)
    ends = ~in_drawdown & in_drawdown.shift(1).fillna(False)

    periods = []
    current_start = None

    for i, (is_start, is_end) in enumerate(zip(starts, ends, strict=False)):
        if is_start:
            current_start = dd.index[i]
        if is_end and current_start is not None:
            end_date = dd.index[i]
            period_dd = dd[current_start:end_date]
            periods.append({
                "start": current_start,
                "end": end_date,
                "duration_days": (end_date - current_start).days,
                "max_drawdown": period_dd.max(),
            })
            current_start = None

    return pd.DataFrame(periods)
