"""Abstract and base classes for predictions.

This module provides classes for managing and processing prediction data in a flexible, configurable manner.
It includes classes to handle configurations, record structures, sequences, and containers for prediction data,
enabling efficient storage, retrieval, and manipulation of prediction records.

This module is designed for use in predictive modeling workflows, facilitating the organization, serialization,
and manipulation of configuration and prediction data in a clear, scalable, and structured manner.
"""

from typing import List, Optional

from loguru import logger
from pydantic import Field, computed_field

from akkudoktoreos.core.coreabc import MeasurementMixin
from akkudoktoreos.core.dataabc import (
    DataABC,
    DataContainer,
    DataImportProvider,
    DataProvider,
    DataRecord,
    DataSequence,
)
from akkudoktoreos.utils.datetimeutil import DateTime, Duration, to_duration


class PredictionABC(DataABC, MeasurementMixin):
    """Base class for handling prediction data.

    Enables access to EOS configuration data (attribute `config`) and EOS measurement data
    (attribute `measurement`).
    """

    pass


class PredictionRecord(DataRecord):
    """Base class for prediction records, enabling dynamic access to fields defined in derived classes.

    Fields can be accessed and mutated both using dictionary-style access (`record['field_name']`)
    and attribute-style access (`record.field_name`).

    Attributes:
        date_time (Optional[AwareDatetime]): Aware datetime indicating when the prediction record applies.

    Configurations:
        - Allows mutation after creation.
        - Supports non-standard data types like `datetime`.
    """

    pass


class PredictionSequence(DataSequence):
    """A managed sequence of PredictionRecord instances with list-like behavior.

    The PredictionSequence class provides an ordered, mutable collection of PredictionRecord
    instances, allowing list-style access for adding, deleting, and retrieving records. It also
    supports advanced data operations such as JSON serialization, conversion to Pandas Series,
    and sorting by timestamp.

    Attributes:
        records (List[PredictionRecord]): A list of PredictionRecord instances representing
                                          individual prediction data points.
        record_keys (Optional[List[str]]): A list of field names (keys) expected in each
                                           PredictionRecord.

    Note:
        Derived classes have to provide their own records field with correct record type set.

    Usage:
        .. code-block:: python

            # Example of creating, adding, and using PredictionSequence
            class DerivedSequence(PredictionSquence):
                records: List[DerivedPredictionRecord] = Field(default_factory=list, json_schema_extra={ "description": "List of prediction records" })

            seq = DerivedSequence()
            seq.insert(DerivedPredictionRecord(date_time=datetime.now(), temperature=72))
            seq.insert(DerivedPredictionRecord(date_time=datetime.now(), temperature=75))

            # Convert to JSON and back
            json_data = seq.to_json()
            new_seq = DerivedSequence.from_json(json_data)

            # Convert to Pandas Series
            series = seq.key_to_series('temperature')

    """

    # To be overloaded by derived classes.
    records: List[PredictionRecord] = Field(
        default_factory=list, json_schema_extra={"description": "List of prediction records"}
    )


class PredictionStartEndKeepMixin(PredictionABC):
    """A mixin to manage start, end, and historical retention datetimes for prediction data.

    The starting datetime for prediction data generation is provided by the energy management
    system. Predictions cannot be computed if this value is `None`.
    """

    def historic_hours_min(self) -> int:
        """Return the minimum historic prediction hours for specific data.

        To be implemented by derived classes if default 0 is not appropriate.
        """
        return 0

    # Computed field for end_datetime and keep_datetime
    @computed_field  # type: ignore[prop-decorator]
    @property
    def end_datetime(self) -> Optional[DateTime]:
        """Compute the end datetime based on the `start_datetime` and `hours`.

        Ajusts the calculated end time if DST transitions occur within the prediction window.

        Returns:
            Optional[DateTime]: The calculated end datetime, or `None` if inputs are missing.
        """
        if self.ems_start_datetime and self.config.prediction.hours:
            end_datetime = self.ems_start_datetime + to_duration(
                f"{self.config.prediction.hours} hours"
            )
            dst_change = end_datetime.offset_hours - self.ems_start_datetime.offset_hours
            logger.debug(
                f"Pre: {self.ems_start_datetime}..{end_datetime}: DST change: {dst_change}"
            )
            if dst_change < 0:
                end_datetime = end_datetime + to_duration(f"{abs(int(dst_change))} hours")
            elif dst_change > 0:
                end_datetime = end_datetime - to_duration(f"{abs(int(dst_change))} hours")
            logger.debug(
                f"Pst: {self.ems_start_datetime}..{end_datetime}: DST change: {dst_change}"
            )
            return end_datetime
        return None

    @computed_field  # type: ignore[prop-decorator]
    @property
    def keep_datetime(self) -> Optional[DateTime]:
        """Compute the keep datetime for historical data retention.

        Returns:
            Optional[DateTime]: The calculated retention cutoff datetime, or `None` if inputs are missing.
        """
        if self.ems_start_datetime is None:
            return None
        historic_hours = self.historic_hours_min()
        if (
            self.config.prediction.historic_hours
            and self.config.prediction.historic_hours > historic_hours
        ):
            historic_hours = int(self.config.prediction.historic_hours)
        return self.ems_start_datetime - to_duration(f"{historic_hours} hours")

    @computed_field  # type: ignore[prop-decorator]
    @property
    def total_hours(self) -> Optional[int]:
        """Compute the hours from `start_datetime` to `end_datetime`.

        Returns:
            Optional[pendulum.period]: The duration hours, or `None` if either datetime is unavailable.
        """
        end_dt = self.end_datetime
        if end_dt is None:
            return None
        duration = end_dt - self.ems_start_datetime
        return int(duration.total_hours())

    @computed_field  # type: ignore[prop-decorator]
    @property
    def keep_hours(self) -> Optional[int]:
        """Compute the hours from `keep_datetime` to `start_datetime`.

        Returns:
            Optional[pendulum.period]: The duration hours, or `None` if either datetime is unavailable.
        """
        keep_dt = self.keep_datetime
        if keep_dt is None:
            return None
        duration = self.ems_start_datetime - keep_dt
        return int(duration.total_hours())


class PredictionProvider(PredictionStartEndKeepMixin, DataProvider):
    """Abstract base class for prediction providers with singleton thread-safety and configurable prediction parameters.

    This class serves as a base for managing prediction data, providing an interface for derived
    classes to maintain a single instance across threads. It offers attributes for managing
    prediction and historical data retention.

    Note:
        Derived classes have to provide their own records field with correct record type set.
    """

    def db_keep_datetime(self) -> Optional[DateTime]:
        """Earliest datetime from which database records should be retained.

        Used when removing old records from database to free space.

        Subclasses may override this method to provide a domain-specific default.

        Returns:
            Datetime or None.
        """
        return self.keep_datetime

    def db_initial_time_window(self) -> Optional[Duration]:
        """Return the initial time window used for database loading.

        This window defines the initial symmetric time span around a target datetime
        that should be loaded from the database when no explicit search time window
        is specified. It serves as a loading hint and may be expanded by the caller
        if no records are found within the initial range.

        Subclasses may override this method to provide a domain-specific default.

        Returns:
            The initial loading time window as a Duration, or ``None`` to indicate
            that no initial window constraint should be applied.
        """
        hours = max(self.config.prediction.hours, self.config.prediction_historic_hours, 24)
        return to_duration(hours * 3600)

    async def update_data(
        self,
        force_enable: Optional[bool] = False,
        force_update: Optional[bool] = False,
    ) -> None:
        """Update prediction parameters and call the custom update function.

        Updates the configuration, deletes outdated records, and performs the custom update logic.

        Args:
            force_enable (bool, optional): If True, forces the update even if the provider is disabled.
            force_update (bool, optional): If True, forces the provider to update the data even if still cached.
        """
        # Check after configuration is updated.
        if not force_enable and not self.enabled():
            return

        # Delete outdated records before updating
        await self.delete_by_datetime(end_datetime=self.keep_datetime)

        # Call the custom update logic
        await self._update_data(force_update=force_update)


class PredictionImportProvider(PredictionProvider, DataImportProvider):
    """Abstract base class for prediction providers that import prediction data.

    This class is designed to handle prediction data provided in the form of a key-value dictionary.

    - **Keys**: Represent identifiers from the record keys of a specific prediction.
    - **Values**: Are lists of prediction values starting at a specified `start_datetime`, where
        each value corresponds to a subsequent time interval (e.g., hourly).

    Subclasses must implement the logic for managing prediction data based on the imported records.
    """

    pass


class PredictionContainer(PredictionStartEndKeepMixin, DataContainer):
    """A container for managing multiple PredictionProvider instances.

    This class enables access to data from multiple prediction providers, supporting retrieval and
    aggregation of their data as Pandas Series objects. It acts as a dictionary-like structure
    where each key represents a specific data field, and the value is a Pandas Series containing
    combined data from all PredictionProvider instances for that key.

    Note:
        Derived classes have to provide their own providers field with correct provider type set.
    """

    # To be overloaded by derived classes.
    providers: List[PredictionProvider] = Field(
        default_factory=list, json_schema_extra={"description": "List of prediction providers"}
    )
