import abc
from typing import Any, Literal, Self

from pydantic import Field, model_validator

from bayesline.api._src._utils import docstrings_from_sync
from bayesline.api._src.equity.calendar_settings import CalendarSettings
from bayesline.api._src.equity.portfoliohierarchy_settings import (
    PortfolioHierarchySettings,
)
from bayesline.api._src.equity.portfolioreport import (
    AsyncReportAccessorApi,
    ReportAccessorApi,
)
from bayesline.api._src.equity.report.accessor import (
    AsyncTypedReportAccessorApi,
    TypedReportAccessorApi,
)
from bayesline.api._src.equity.report.api import AsyncReportApi, ReportApi
from bayesline.api._src.equity.report.scenarios import (
    PortfolioHoldingsScenario,
)
from bayesline.api._src.equity.report.settings import ReportSettings
from bayesline.api._src.equity.riskmodels_settings import FactorRiskModelSettings
from bayesline.api._src.tasks import AsyncTask, Task
from bayesline.api._src.types import DateLike


class ReturnAttributionReportSettings(ReportSettings):
    """Settings for a return attribution drilldown report."""

    report_type: Literal["return_attribution"] = Field(
        default="return_attribution", description="Report type discriminator"
    )
    portfolio_hierarchy_settings: PortfolioHierarchySettings = Field()
    factor_model_settings: FactorRiskModelSettings = Field()
    calendar: CalendarSettings = Field(default_factory=CalendarSettings)
    currency: str | None = Field(None)
    rescale_bench: bool = Field(True)
    normalize_holdings: bool = Field(True)
    return_aggregation_type: Literal["arithmetic", "geometric"] = Field("geometric")

    @model_validator(mode="after")
    def validate_geometric_requires_normalized(self) -> Self:
        if self.return_aggregation_type == "geometric" and not self.normalize_holdings:
            raise ValueError(
                "Geometric compounding requires normalize_holdings=True. "
                "PnL values cannot be geometrically compounded."
            )
        return self


class ReturnAttributionReportAccessor(TypedReportAccessorApi):
    """Specific accessor for a return attribution report."""

    scenario_types = [PortfolioHoldingsScenario]


@docstrings_from_sync
class AsyncReturnAttributionReportAccessor(AsyncTypedReportAccessorApi):
    scenario_types = [PortfolioHoldingsScenario]


class ReturnAttributionReportApi(
    ReportApi[
        [DateLike | None, DateLike | None],
        ReturnAttributionReportAccessor,
        ReturnAttributionReportSettings,
    ]
):
    """API for a return attribution report."""

    @abc.abstractmethod
    def get_maybe_cached(
        self,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> ReturnAttributionReportAccessor | None:
        """
        Get the cached return attribution report accessor API if it is available.

        Parameters
        ----------
        start_date: DateLike | None
            The start date of the report.
        end_date: DateLike | None
            The end date of the report.
        **kwargs: Any
            Additional keyword arguments.

        Returns
        -------
        ReturnAttributionReportAccessor | None
            The cached return attribution report accessor API, or None if the report is
            not cached.
        """

    @abc.abstractmethod
    def calculate(
        self,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> ReturnAttributionReportAccessor:
        """
        Calculate the return attribution report.

        Parameters
        ----------
        start_date: DateLike | None
            The start date of the report.
        end_date: DateLike | None
            The end date of the report.
        **kwargs: Any
            Additional keyword arguments.

        Returns
        -------
        ReturnAttributionReportAccessor
            The return attribution report accessor.
        """

    @abc.abstractmethod
    def calculate_as_task(
        self,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> Task[ReturnAttributionReportAccessor]:
        """
        Calculate the return attribution report as a task.

        Parameters
        ----------
        start_date: DateLike | None
            The start date of the report.
        end_date: DateLike | None
            The end date of the report.
        **kwargs: Any
            Additional keyword arguments.

        Returns
        -------
        Task[ReturnAttributionReportAccessor]
            The task for the return attribution report accessor.
        """


@docstrings_from_sync
class AsyncReturnAttributionReportApi(
    AsyncReportApi[
        [DateLike | None, DateLike | None],
        AsyncReturnAttributionReportAccessor,
        ReturnAttributionReportSettings,
    ]
):

    @abc.abstractmethod
    async def get_maybe_cached(  # noqa: D102
        self,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> AsyncReturnAttributionReportAccessor | None: ...

    @abc.abstractmethod
    async def calculate(  # noqa: D102
        self,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> AsyncReturnAttributionReportAccessor: ...

    @abc.abstractmethod
    async def calculate_as_task(  # noqa: D102
        self,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> AsyncTask[AsyncReturnAttributionReportAccessor]: ...


class ReturnAttributionReportAccessorImpl(  # noqa: D101
    ReturnAttributionReportAccessor
):

    def __init__(self, accessor: ReportAccessorApi):
        self._accessor = accessor

    @property
    def accessor(self) -> ReportAccessorApi:  # noqa: D102
        return self._accessor


class AsyncReturnAttributionReportAccessorImpl(  # noqa: D101
    AsyncReturnAttributionReportAccessor
):

    def __init__(self, accessor: AsyncReportAccessorApi):
        self._accessor = accessor

    @property
    def accessor(self) -> AsyncReportAccessorApi:  # noqa: D102
        return self._accessor
