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.tasks import AsyncTask, Task
from bayesline.api._src.types import DateLike


class PortfolioHoldingsReportSettings(ReportSettings):
    """Settings for a portfolio holdings report."""

    report_type: Literal["portfolio_holdings"] = Field(
        default="portfolio_holdings", description="Report type discriminator"
    )
    portfolio_hierarchy_settings: PortfolioHierarchySettings = Field()
    date_aggregation: Literal["last"] = Field("last")
    calendar: CalendarSettings = Field(default_factory=CalendarSettings)
    currency: str | None = Field(None)
    unpack: bool = Field(False)
    forward_fill: bool = Field(False)

    @model_validator(mode="after")
    def validate_currency_unpack_consistency(self) -> Self:
        if self.currency is None and self.unpack:
            raise ValueError("currency is required when unpack is True")
        return self


class PortfolioHoldingsReportAccessor(TypedReportAccessorApi):
    """Specific accessor for a portfolio holdings report."""

    scenario_types = [PortfolioHoldingsScenario]


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


class PortfolioHoldingsReportApi(
    ReportApi[
        [list[str] | None, DateLike | None, DateLike | None],
        PortfolioHoldingsReportAccessor,
        PortfolioHoldingsReportSettings,
    ]
):
    """API for a universe count report."""

    @abc.abstractmethod
    def get_maybe_cached(
        self,
        portfolio_names: list[str] | None,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> PortfolioHoldingsReportAccessor | None:
        """
        Get the cached portfolio holdings report accessor API if it is available.

        Parameters
        ----------
        portfolio_names: list[str] | None
            The names of the portfolios to calculate the report for. If None, all
            portfolios will be calculated. For nested portfolios, the names must be
            given in the format "portfolio:held_portfolio", with multiple separators
            indicating deeper nesting. If nested portfolios are given, unpack must be
            True in the report settings.
        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
        -------
        PortfolioHoldingsReportAccessor | None
            The cached portfolio holdings report accessor API, or None if the report is
            not cached.
        """

    @abc.abstractmethod
    def calculate(
        self,
        portfolio_names: list[str] | None,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> PortfolioHoldingsReportAccessor:
        """
        Calculate the portfolio holdings report.

        Parameters
        ----------
        portfolio_names: list[str] | None
            The names of the portfolios to calculate the report for. If None, all
            portfolios will be calculated. For nested portfolios, the names must be
            given in the format "portfolio:held_portfolio", with multiple separators
            indicating deeper nesting. If nested portfolios are given, unpack must be
            True in the report settings.
        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
        -------
        PortfolioHoldingsReportAccessor
            The portfolio holdings report accessor.
        """

    @abc.abstractmethod
    def calculate_as_task(
        self,
        portfolio_names: list[str] | None,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> Task[PortfolioHoldingsReportAccessor]:
        """
        Calculate the portfolio holdings report as a task.

        Parameters
        ----------
        portfolio_names: list[str] | None
            The names of the portfolios to calculate the report for. If None, all
            portfolios will be calculated. For nested portfolios, the names must be
            given in the format "portfolio:held_portfolio", with multiple separators
            indicating deeper nesting. If nested portfolios are given, unpack must be
            True in the report settings.
        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[PortfolioHoldingsReportAccessor]
            The task for the portfolio holdings report accessor.
        """


@docstrings_from_sync
class AsyncPortfolioHoldingsReportApi(
    AsyncReportApi[
        [list[str] | None, DateLike | None, DateLike | None],
        AsyncPortfolioHoldingsReportAccessor,
        PortfolioHoldingsReportSettings,
    ]
):

    @abc.abstractmethod
    async def get_maybe_cached(  # noqa: D102
        self,
        portfolio_names: list[str] | None,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> AsyncPortfolioHoldingsReportAccessor | None: ...

    @abc.abstractmethod
    async def calculate(  # noqa: D102
        self,
        portfolio_names: list[str] | None,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> AsyncPortfolioHoldingsReportAccessor: ...

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


class PortfolioHoldingsReportAccessorImpl(  # noqa: D101
    PortfolioHoldingsReportAccessor
):

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

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


class AsyncPortfolioHoldingsReportAccessorImpl(  # noqa: D101
    AsyncPortfolioHoldingsReportAccessor
):

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

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