"""
ferro_ta.portfolio — Portfolio and multi-asset analytics.

Compute-intensive portfolio metrics (correlation, volatility, beta, drawdown)
are implemented in Rust; this module provides the Python-facing API.

Functions
---------
correlation_matrix(returns_df_or_array)
    Compute the pairwise Pearson correlation matrix for a returns table.

portfolio_volatility(returns, weights)
    Compute portfolio volatility sqrt(w' Σ w) from a returns table and
    weights (or pass a covariance matrix directly).

beta(asset_returns, benchmark_returns, *, window=None)
    Compute beta of one asset vs a benchmark, full-sample or rolling.

drawdown(equity, *, as_series=True)
    Compute the drawdown series and max drawdown for an equity curve.

Rust backend
------------
All compute delegates to::

    ferro_ta._ferro_ta.correlation_matrix
    ferro_ta._ferro_ta.portfolio_volatility
    ferro_ta._ferro_ta.beta_full
    ferro_ta._ferro_ta.rolling_beta
    ferro_ta._ferro_ta.drawdown_series
"""

from __future__ import annotations

from typing import Any, Optional, Union

import numpy as np
from numpy.typing import ArrayLike, NDArray

from ferro_ta._ferro_ta import beta_full as _rust_beta_full
from ferro_ta._ferro_ta import correlation_matrix as _rust_corr
from ferro_ta._ferro_ta import drawdown_series as _rust_drawdown
from ferro_ta._ferro_ta import portfolio_volatility as _rust_port_vol
from ferro_ta._ferro_ta import rolling_beta as _rust_rolling_beta
from ferro_ta._utils import _to_f64

__all__ = [
    "correlation_matrix",
    "portfolio_volatility",
    "beta",
    "drawdown",
]


# ---------------------------------------------------------------------------
# correlation_matrix
# ---------------------------------------------------------------------------


def correlation_matrix(returns: Any) -> Any:
    """Compute the pairwise Pearson correlation matrix.

    Parameters
    ----------
    returns : pandas.DataFrame or 2-D array-like, shape (n_bars, n_assets)
        Returns per bar and asset.  Assets are columns.

    Returns
    -------
    numpy.ndarray of shape (n_assets, n_assets), or pandas.DataFrame
    with same column/index names if a DataFrame was passed.

    Examples
    --------
    >>> import numpy as np
    >>> from ferro_ta.analysis.portfolio import correlation_matrix
    >>> rng = np.random.default_rng(0)
    >>> r = rng.normal(0, 0.01, (100, 3))
    >>> corr = correlation_matrix(r)
    >>> corr.shape
    (3, 3)
    >>> abs(corr[0, 0] - 1.0) < 1e-10
    True
    """
    try:
        import pandas as pd

        if isinstance(returns, pd.DataFrame):
            cols = returns.columns.tolist()
            arr = returns.values.astype(np.float64, copy=False)
            arr = np.ascontiguousarray(arr)
            result = _rust_corr(arr)
            return pd.DataFrame(result, index=cols, columns=cols)  # type: ignore[arg-type]
    except ImportError:
        pass
    arr = np.ascontiguousarray(returns, dtype=np.float64)
    return _rust_corr(arr)


# ---------------------------------------------------------------------------
# portfolio_volatility
# ---------------------------------------------------------------------------


def portfolio_volatility(
    returns: Any,
    weights: ArrayLike,
    *,
    annualise: Optional[float] = None,
) -> float:
    """Compute portfolio volatility sqrt(w' Σ w).

    Parameters
    ----------
    returns : pandas.DataFrame or 2-D array-like, shape (n_bars, n_assets)
        Returns per bar/asset.  The covariance matrix is computed from this.
    weights : array-like of length n_assets
        Portfolio weights (do not need to sum to 1).
    annualise : float, optional
        If given, the result is multiplied by ``sqrt(annualise)`` (e.g.
        ``252`` for daily returns annualised to yearly).

    Returns
    -------
    float

    Examples
    --------
    >>> import numpy as np
    >>> from ferro_ta.analysis.portfolio import portfolio_volatility
    >>> rng = np.random.default_rng(1)
    >>> r = rng.normal(0, 0.01, (252, 3))
    >>> vol = portfolio_volatility(r, weights=[1/3, 1/3, 1/3])
    >>> vol > 0
    True
    """
    try:
        import pandas as pd

        if isinstance(returns, pd.DataFrame):
            arr = returns.values.astype(np.float64, copy=False)
        else:
            arr = np.asarray(returns, dtype=np.float64)
    except ImportError:
        arr = np.asarray(returns, dtype=np.float64)

    arr = np.ascontiguousarray(arr)
    cov = np.cov(arr.T)
    if cov.ndim == 0:
        cov = np.array([[float(cov)]])
    cov = np.ascontiguousarray(cov)
    w = np.ascontiguousarray(np.asarray(weights, dtype=np.float64))
    vol = _rust_port_vol(cov, w)
    if annualise is not None:
        vol *= float(annualise) ** 0.5
    return vol


# ---------------------------------------------------------------------------
# beta
# ---------------------------------------------------------------------------


def beta(
    asset_returns: ArrayLike,
    benchmark_returns: ArrayLike,
    *,
    window: Optional[int] = None,
) -> Union[float, NDArray[np.float64]]:
    """Compute beta of an asset vs a benchmark.

    Parameters
    ----------
    asset_returns, benchmark_returns : array-like
        Fractional returns per bar (equal length, >= 2 elements).
    window : int, optional
        If given, compute rolling beta over a sliding window of this size.
        Returns a 1-D array with ``NaN`` for the first ``window-1`` bars.
        If ``None`` (default), return the full-sample scalar beta.

    Returns
    -------
    float or numpy.ndarray

    Examples
    --------
    >>> import numpy as np
    >>> from ferro_ta.analysis.portfolio import beta
    >>> rng = np.random.default_rng(2)
    >>> bench = rng.normal(0, 0.01, 100)
    >>> asset = 1.2 * bench + rng.normal(0, 0.001, 100)
    >>> abs(beta(asset, bench) - 1.2) < 0.05
    True
    """
    a = _to_f64(asset_returns)
    b = _to_f64(benchmark_returns)
    if window is not None:
        return _rust_rolling_beta(a, b, int(window))
    return _rust_beta_full(a, b)


# ---------------------------------------------------------------------------
# drawdown
# ---------------------------------------------------------------------------


def drawdown(
    equity: ArrayLike,
    *,
    as_series: bool = True,
) -> Union[tuple[NDArray[np.float64], float], float]:
    """Compute the drawdown series and maximum drawdown.

    Parameters
    ----------
    equity : array-like
        Equity or price series (e.g. portfolio equity curve).
    as_series : bool
        If ``True`` (default), return ``(drawdown_array, max_drawdown)``.
        If ``False``, return only the scalar max_drawdown.

    Returns
    -------
    (numpy.ndarray, float) when *as_series* is True;
    float when *as_series* is False.

    Examples
    --------
    >>> import numpy as np
    >>> from ferro_ta.analysis.portfolio import drawdown
    >>> eq = np.array([100.0, 110.0, 105.0, 90.0, 95.0])
    >>> dd, max_dd = drawdown(eq)
    >>> round(max_dd, 4)
    -0.1818
    """
    eq = _to_f64(equity)
    dd_arr, max_dd = _rust_drawdown(eq)
    if as_series:
        return dd_arr, max_dd
    return max_dd
