from collections.abc import Callable

import pandas as pd

from signalab.account import Account
from signalab.analytics import metrics


class AccountReport:
    """Report class to store and manage account-related backtest results."""

    def __init__(self, account: Account):
        """Initialize account report wrapper.

        Parameters
        ----------
        account : Account
            Account instance containing recorded run data.
        """
        self.account = account
        self.account_df = account.get_record_df()
        self._metrics_cache: dict[str, float] = {}

    def _cached_metric(self, name: str, compute: Callable[[], float]) -> float:
        """Return cached metric value, computing it once lazily.

        Parameters
        ----------
        name : str
            Cache key.
        compute : Callable[[], float]
            Metric compute callback.

        Returns
        -------
        float
            Metric value.
        """
        if name not in self._metrics_cache:
            self._metrics_cache[name] = float(compute())
        return self._metrics_cache[name]

    @property
    def dataframe(self) -> pd.DataFrame:
        """Return the account report as a DataFrame."""
        return self.account_df

    @property
    def balance(self) -> pd.Series:
        """Return the account balance over time as a Series."""
        return self.account_df["balance"]

    @property
    def equity(self) -> pd.Series:
        """Return the account equity over time as a Series."""
        return self.account_df["equity"]

    @property
    def realized_pl(self) -> pd.Series:
        """Return the account realized profit/loss over time as a Series."""
        return self.account_df["realized_pl"]

    @property
    def floating_pl(self) -> pd.Series:
        """Return the account floating profit/loss over time as a Series."""
        return self.account_df["floating_pl"]

    @property
    def open_buy_count(self) -> pd.Series:
        """Return the accumulative count of open buy positions over time as a Series."""
        return self.account_df["open_buy_count"]

    @property
    def open_sell_count(self) -> pd.Series:
        """Return the accumulative count of open sell positions over time as a Series."""
        return self.account_df["open_sell_count"]

    def total_return(self) -> float:
        """Return total return metric.

        Returns
        -------
        float
            Total return.
        """
        return self._cached_metric(
            "total_return", lambda: metrics.total_return(self.equity)
        )

    def cagr(self) -> float:
        """Return CAGR metric.

        Returns
        -------
        float
            Compound annual growth rate.
        """
        return self._cached_metric("cagr", lambda: metrics.cagr(self.equity))

    def max_drawdown(self) -> float:
        """Return maximum drawdown metric.

        Returns
        -------
        float
            Maximum drawdown.
        """
        return self._cached_metric(
            "max_drawdown", lambda: metrics.max_drawdown(self.equity)
        )

    def sharpe_ratio(self) -> float:
        """Return Sharpe ratio metric.

        Returns
        -------
        float
            Sharpe ratio.
        """
        return self._cached_metric(
            "sharpe_ratio", lambda: metrics.sharpe_ratio(self.equity)
        )

    def sortino_ratio(self) -> float:
        """Return Sortino ratio metric.

        Returns
        -------
        float
            Sortino ratio.
        """
        return self._cached_metric(
            "sortino_ratio", lambda: metrics.sortino_ratio(self.equity)
        )

    def calmar_ratio(self) -> float:
        """Return Calmar ratio metric.

        Returns
        -------
        float
            Calmar ratio.
        """
        return self._cached_metric(
            "calmar_ratio", lambda: metrics.calmar_ratio(self.equity)
        )

    def win_rate(self) -> float:
        """Return win rate metric.

        Returns
        -------
        float
            Win rate.
        """
        return self._cached_metric(
            "win_rate", lambda: metrics.win_rate(self.realized_pl)
        )

    def profit_factor(self) -> float:
        """Return profit factor metric.

        Returns
        -------
        float
            Profit factor.
        """
        return self._cached_metric(
            "profit_factor", lambda: metrics.profit_factor(self.realized_pl)
        )

    def expectancy(self) -> float:
        """Return expectancy metric.

        Returns
        -------
        float
            Expectancy.
        """
        return self._cached_metric(
            "expectancy", lambda: metrics.expectancy(self.realized_pl)
        )

    def exposure(self) -> float:
        """Return exposure metric.

        Returns
        -------
        float
            Exposure ratio.
        """
        return self._cached_metric(
            "exposure", lambda: metrics.exposure(self.account_df["used_margin"])
        )

    def turnover(self) -> float:
        """Return turnover metric.

        Returns
        -------
        float
            Turnover proxy.
        """
        return self._cached_metric(
            "turnover",
            lambda: metrics.turnover(self.open_buy_count, self.open_sell_count),
        )

    def metrics(self) -> dict[str, float]:
        """Return all account metrics.

        Returns
        -------
        dict[str, float]
            Metric name to value mapping.
        """
        return {
            "total_return": self.total_return(),
            "cagr": self.cagr(),
            "max_drawdown": self.max_drawdown(),
            "sharpe_ratio": self.sharpe_ratio(),
            "sortino_ratio": self.sortino_ratio(),
            "calmar_ratio": self.calmar_ratio(),
            "win_rate": self.win_rate(),
            "profit_factor": self.profit_factor(),
            "expectancy": self.expectancy(),
            "exposure": self.exposure(),
            "turnover": self.turnover(),
        }


class Report:
    """Report class to store and manage backtest results."""

    def __init__(
        self, indicators_df: pd.DataFrame, accounts_reports: dict[str, AccountReport]
    ):
        """Initialize global report container.

        Parameters
        ----------
        indicators_df : pd.DataFrame
            Combined market and indicator dataframe.
        accounts_reports : dict[str, AccountReport]
            Account reports keyed by label.
        """
        self.indicators_df = indicators_df
        self.account_reports = accounts_reports

    def get_account_report(self, account_label: str = "common") -> AccountReport:
        """Get an account report by account label.

        Parameters
        ----------
        account_label : str, default="common"
            Account label to retrieve. By default, retrieves the "common" account report.

        Returns
        -------
        AccountReport
            Account report object containing the account DataFrame.
        """
        if account_label not in self.account_reports:
            raise ValueError(f"Account report with label '{account_label}' not found.")
        return self.account_reports[account_label]
