import abc
import datetime as dt
from typing import Any, Literal, cast

import polars as pl
from pydantic import Field

from bayesline.api._src._utils import docstrings_from_sync
from bayesline.api._src.equity.exposure_settings import ExposureSettings
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.settings import ReportSettings
from bayesline.api._src.equity.universe_settings import UniverseSettings
from bayesline.api._src.tasks import AsyncTask, Task
from bayesline.api._src.types import DateLike, DNFFilterExpressions


class ExposureReportSettings(ReportSettings):
    """Settings for a exposure report."""

    report_type: Literal["exposure"] = Field(
        default="exposure", description="Report type discriminator"
    )
    universe_settings: UniverseSettings = Field()
    exposure_settings: ExposureSettings = Field()
    standardize_universe_settings: UniverseSettings | None = Field()


class ExposureReportAccessor(TypedReportAccessorApi):
    """Specific accessor for a exposure report."""

    @abc.abstractmethod
    def get_dates(self) -> list[dt.date]:
        """
        Get the dates for the report.

        Returns
        -------
        list[dt.date]
            The dates for the report.
        """

    @abc.abstractmethod
    def get(
        self, start_date: DateLike | None = None, end_date: DateLike | None = None
    ) -> pl.DataFrame:
        """
        Get the exposures for the report.

        Parameters
        ----------
        start_date: DateLike | None, default=None
            The start date of the report, inclusive.
        end_date: DateLike | None, default=None
            The end date of the report, inclusive.

        Returns
        -------
        pl.DataFrame
            The exposures for the report, with a date column and all factors as columns.
            The factor column names are of the form `factor_group^factor`.
        """


@docstrings_from_sync
class AsyncExposureReportAccessor(AsyncTypedReportAccessorApi):

    @abc.abstractmethod
    async def get_dates(self) -> list[dt.date]: ...  # noqa: D102

    @abc.abstractmethod
    async def get(  # noqa: D102
        self, start_date: DateLike | None = None, end_date: DateLike | None = None
    ) -> pl.DataFrame: ...


class ExposureReportApi(
    ReportApi[
        [DateLike | None, DateLike | None],
        ExposureReportAccessor,
        ExposureReportSettings,
    ]
):
    """API for a exposure report."""

    @abc.abstractmethod
    def get_maybe_cached(
        self,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> ExposureReportAccessor | None:
        """
        Get the cached exposure 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
        -------
        ExposureReportAccessor | None
            The cached exposure 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,
    ) -> ExposureReportAccessor:
        """
        Calculate the exposure 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
        -------
        ExposureReportAccessor
            The exposure report accessor.
        """

    @abc.abstractmethod
    def calculate_as_task(
        self,
        start_date: DateLike | None,
        end_date: DateLike | None,
        **kwargs: Any,
    ) -> Task[ExposureReportAccessor]:
        """
        Calculate the exposure 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[ExposureReportAccessor]
            The task for the exposure report accessor.
        """


@docstrings_from_sync
class AsyncExposureReportApi(
    AsyncReportApi[
        [DateLike | None, DateLike | None],
        AsyncExposureReportAccessor,
        ExposureReportSettings,
    ]
):

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

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

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


class ExposureReportAccessorImpl(ExposureReportAccessor):  # noqa: D101

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

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

    def get_dates(self) -> list[dt.date]:  # noqa: D102
        return self.accessor.get_level_values(("date",)).to_series().dt.date().to_list()

    def get(  # noqa: D102
        self, start_date: DateLike | None = None, end_date: DateLike | None = None
    ) -> pl.DataFrame:
        filters: DNFFilterExpressions = []
        if start_date is not None:
            filters.append(("date", ">=", start_date))
        if end_date is not None:
            filters.append(("date", "<=", end_date))

        df = self.accessor.get_data(
            [],
            expand=("date", "asset_id"),
            value_cols=("exposure",),
            pivot_cols=("factor_group", "factor"),
            filters=filters or None,
        )
        return df.rename(lambda x: x.removesuffix("^exposure"))


class AsyncExposureReportAccessorImpl(AsyncExposureReportAccessor):  # noqa: D101

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

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

    async def get_dates(self) -> list[dt.date]:  # noqa: D102
        df = await self.accessor.get_level_values(("date",))
        return cast(list[dt.date], df.to_series().dt.date().to_list())

    async def get(  # noqa: D102
        self, start_date: DateLike | None = None, end_date: DateLike | None = None
    ) -> pl.DataFrame:
        filters: DNFFilterExpressions = []
        if start_date is not None:
            filters.append(("date", ">=", start_date))
        if end_date is not None:
            filters.append(("date", "<=", end_date))

        df = await self.accessor.get_data(
            [],
            expand=("date", "asset_id"),
            value_cols=("exposure",),
            pivot_cols=("factor_group", "factor"),
            filters=filters or None,
        )
        return df.rename(lambda x: x.removesuffix("^exposure"))
