from __future__ import annotations

import math

import numpy as np
import pandas as pd


def _clean_series(series: pd.Series) -> pd.Series:
    """Normalize a numeric series by dropping invalid values.

    Parameters
    ----------
    series : pd.Series
        Input series.

    Returns
    -------
    pd.Series
        Float series without ``NaN`` or infinite values.
    """
    return series.astype(float).replace([np.inf, -np.inf], np.nan).dropna()


def _equity_returns(equity: pd.Series) -> pd.Series:
    """Compute percentage returns from an equity curve.

    Parameters
    ----------
    equity : pd.Series
        Equity time series.

    Returns
    -------
    pd.Series
        Per-period returns.
    """
    clean = _clean_series(equity)
    if len(clean) < 2:
        return pd.Series(dtype=float)
    return clean.pct_change().dropna()


def _annualization_factor(index: pd.Index) -> float:
    """Infer annualization factor from timestamp spacing.

    Parameters
    ----------
    index : pd.Index
        Time index for a return series.

    Returns
    -------
    float
        Periods per year approximation.
    """
    if not isinstance(index, pd.DatetimeIndex) or len(index) < 2:
        return 252.0

    deltas = index.to_series().diff().dropna()
    if deltas.empty:
        return 252.0

    median_delta = deltas.median()
    seconds = float(median_delta.total_seconds())
    if seconds <= 0:
        return 252.0
    return 31_536_000.0 / seconds


def total_return(equity: pd.Series) -> float:
    """Compute total return from first to last equity value.

    Parameters
    ----------
    equity : pd.Series
        Equity time series.

    Returns
    -------
    float
        Total return as decimal.
    """
    clean = _clean_series(equity)
    if len(clean) < 2 or clean.iloc[0] == 0:
        return 0.0
    return float(clean.iloc[-1] / clean.iloc[0] - 1.0)


def cagr(equity: pd.Series) -> float:
    """Compute compound annual growth rate from equity curve.

    Parameters
    ----------
    equity : pd.Series
        Equity time series indexed by datetime.

    Returns
    -------
    float
        CAGR as decimal.
    """
    clean = _clean_series(equity)
    if len(clean) < 2:
        return 0.0
    if not isinstance(clean.index, pd.DatetimeIndex):
        return 0.0
    initial = float(clean.iloc[0])
    final = float(clean.iloc[-1])
    if initial <= 0 or final <= 0:
        return 0.0

    years = (clean.index[-1] - clean.index[0]).total_seconds() / 31_536_000.0
    if years <= 0:
        return 0.0
    return float((final / initial) ** (1.0 / years) - 1.0)


def max_drawdown(equity: pd.Series) -> float:
    """Compute maximum drawdown from equity curve.

    Parameters
    ----------
    equity : pd.Series
        Equity time series.

    Returns
    -------
    float
        Most negative drawdown value.
    """
    clean = _clean_series(equity)
    if clean.empty:
        return 0.0
    peak = clean.cummax()
    drawdown = clean / peak - 1.0
    return float(drawdown.min())


def sharpe_ratio(equity: pd.Series, risk_free_rate: float = 0.0) -> float:
    """Compute annualized Sharpe ratio from equity curve.

    Parameters
    ----------
    equity : pd.Series
        Equity time series.
    risk_free_rate : float, default=0.0
        Annual risk-free rate.

    Returns
    -------
    float
        Annualized Sharpe ratio.
    """
    returns = _equity_returns(equity)
    if returns.empty:
        return 0.0

    ann = _annualization_factor(equity.index)
    rf_per_period = risk_free_rate / ann
    excess = returns - rf_per_period
    std = float(excess.std(ddof=0))
    if std == 0.0:
        return 0.0
    return float(math.sqrt(ann) * excess.mean() / std)


def sortino_ratio(equity: pd.Series, risk_free_rate: float = 0.0) -> float:
    """Compute annualized Sortino ratio from equity curve.

    Parameters
    ----------
    equity : pd.Series
        Equity time series.
    risk_free_rate : float, default=0.0
        Annual risk-free rate.

    Returns
    -------
    float
        Annualized Sortino ratio.
    """
    returns = _equity_returns(equity)
    if returns.empty:
        return 0.0

    ann = _annualization_factor(equity.index)
    rf_per_period = risk_free_rate / ann
    excess = returns - rf_per_period
    downside = excess[excess < 0]
    if downside.empty:
        return 0.0
    downside_std = float(downside.std(ddof=0))
    if downside_std == 0.0:
        return 0.0
    return float(math.sqrt(ann) * excess.mean() / downside_std)


def calmar_ratio(equity: pd.Series) -> float:
    """Compute Calmar ratio as CAGR divided by absolute max drawdown.

    Parameters
    ----------
    equity : pd.Series
        Equity time series.

    Returns
    -------
    float
        Calmar ratio.
    """
    mdd = abs(max_drawdown(equity))
    if mdd == 0.0:
        return 0.0
    return float(cagr(equity) / mdd)


def _realized_deltas(realized_pl: pd.Series) -> pd.Series:
    """Extract non-zero realized P/L increments from cumulative series.

    Parameters
    ----------
    realized_pl : pd.Series
        Cumulative realized P/L.

    Returns
    -------
    pd.Series
        Non-zero per-period realized P/L deltas.
    """
    clean = _clean_series(realized_pl)
    if clean.empty:
        return pd.Series(dtype=float)
    deltas = clean.diff().dropna()
    return deltas[deltas != 0.0]


def win_rate(realized_pl: pd.Series) -> float:
    """Compute winning-trade ratio from realized P/L deltas.

    Parameters
    ----------
    realized_pl : pd.Series
        Cumulative realized P/L.

    Returns
    -------
    float
        Fraction of positive realized deltas.
    """
    deltas = _realized_deltas(realized_pl)
    if deltas.empty:
        return 0.0
    return float((deltas > 0).mean())


def profit_factor(realized_pl: pd.Series) -> float:
    """Compute profit factor from realized P/L deltas.

    Parameters
    ----------
    realized_pl : pd.Series
        Cumulative realized P/L.

    Returns
    -------
    float
        Gross profit divided by gross loss.
    """
    deltas = _realized_deltas(realized_pl)
    if deltas.empty:
        return 0.0
    gross_profit = float(deltas[deltas > 0].sum())
    gross_loss = float(-deltas[deltas < 0].sum())
    if gross_loss == 0.0:
        return 0.0
    return gross_profit / gross_loss


def expectancy(realized_pl: pd.Series) -> float:
    """Compute average realized P/L per non-zero event.

    Parameters
    ----------
    realized_pl : pd.Series
        Cumulative realized P/L.

    Returns
    -------
    float
        Mean non-zero realized P/L delta.
    """
    deltas = _realized_deltas(realized_pl)
    if deltas.empty:
        return 0.0
    return float(deltas.mean())


def exposure(used_margin: pd.Series) -> float:
    """Compute time-in-market ratio from used margin series.

    Parameters
    ----------
    used_margin : pd.Series
        Used margin time series.

    Returns
    -------
    float
        Fraction of periods where used margin is positive.
    """
    clean = _clean_series(used_margin)
    if clean.empty:
        return 0.0
    return float((clean > 0).mean())


def turnover(open_buy_count: pd.Series, open_sell_count: pd.Series) -> float:
    """Compute normalized opening-event turnover proxy.

    Parameters
    ----------
    open_buy_count : pd.Series
        Open buy positions count over time.
    open_sell_count : pd.Series
        Open sell positions count over time.

    Returns
    -------
    float
        Positive open-count increments divided by number of periods.
    """
    buy = _clean_series(open_buy_count)
    sell = _clean_series(open_sell_count)
    if buy.empty or sell.empty:
        return 0.0
    idx = buy.index.intersection(sell.index)
    if idx.empty:
        return 0.0

    total_open_count = buy.loc[idx] + sell.loc[idx]
    open_events = total_open_count.diff().clip(lower=0).sum()
    periods = float(len(total_open_count))
    if periods == 0:
        return 0.0
    return float(open_events / periods)
