"""
Defines classes and data structures for figure variables.
"""

import datetime
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any

from fields_metadata import FieldMetadata


@dataclass(frozen=True)
class Aggregation:
    """
    Encapsulates the parameters of an aggregation variable.

    :param name: Output alias for the aggregated value.
    :type name: str
    :param function: Aggregation function identifier string.
    :type function: str
    :param field: Source field to aggregate. ``None`` for count-only aggregations.
    :type field: str | None
    """

    name: str
    function: str
    field: str | None = None

    @property
    def default_label(self) -> str:
        """
        Returns a human-readable label derived from the aggregation function and field.

        :return: Descriptive label such as ``"Average of revenue"`` or ``"Count"``.
        :rtype: str
        """
        fn_label = self.function.replace("_", " ").title()
        if self.field is not None:
            return f"{fn_label} of {self.field}"
        return fn_label


class ChartVariable(ABC):
    """
    Abstract base class for chart variables.
    """

    def __init__(
        self,
        name: str,
        description: str,
        map_to: str,
        icon: str | None = None,
    ) -> None:
        self._name = name
        self._description = description
        self._map_to = map_to
        self._icon = icon

    @abstractmethod
    def candidate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
    ) -> list[str]:
        """
        Returns a list of candidate fields for this variable.

        :param fields_metadata: Mapping of field paths to their metadata.
        :type fields_metadata: dict[str, FieldMetadata]
        :return: A list of candidate field names.
        :rtype: list[str]
        """

    def validate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
        *fields: str,
    ) -> None:
        """
        Validates a number of fields according to the variable's parameters.

        :param fields_metadata: Mapping of field paths to their metadata.
        :type fields_metadata: dict[str, FieldMetadata]
        :raises ValueError: When any of the fields are not valid candidates.
        """
        candidates = self.candidate_fields(fields_metadata)
        for field in fields:
            if field not in candidates:
                raise ValueError(f"Field {field} is not a valid candidate field for variable {self._name}")

    def describe(
        self,
        fields_metadata: dict[str, FieldMetadata],
    ) -> dict[str, Any]:
        """
        Provides a descriptive structure of the variable.

        :param fields_metadata: Mapping of field paths to their metadata.
        :type fields_metadata: dict[str, FieldMetadata]
        :return: A dictionary describing the variable.
        :rtype: dict[str, Any]
        """
        return {
            "type": self.__class__,
            "name": self._name,
            "description": self._description,
            "map_to": self._map_to,
            "icon": self._icon,
            "candidate_fields": self.candidate_fields(fields_metadata),
        }


class CategoricalVariable(ChartVariable):
    """
    Represents a (multi)categorical variable for charts.
    """

    def __init__(
        self,
        name: str,
        description: str,
        map_to: str,
        multicategorical: bool = False,
        ordered: bool = False,
        optional: bool = False,
        icon: str | None = None,
    ) -> None:
        """
        Constructor method.

        :param name: The name of the variable.
        :type name: str
        :param description: The description of the variable.
        :type description: str
        :param map_to: The internal parameter to map the variable to.
        :type map_to: str
        :param multicategorical: When ``True``, treats the variable as multicategorical.
        :type multicategorical: bool, optional
        :param ordered: When ``True``, renders as an ordered FieldList of single-select entries.
        :type ordered: bool, optional
        :param optional: When ``True``, prepends a "— none —" empty choice to the select field.
        :type optional: bool, optional
        :param icon: An optional SVG icon string. Defaults to ``None``.
        :type icon: str | None, optional
        """
        super().__init__(
            name=name,
            description=description,
            map_to=map_to,
            icon=icon,
        )
        self._multicategorical: bool = multicategorical
        self._ordered: bool = ordered
        self._optional: bool = optional

    def candidate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
    ) -> list[str]:
        return [field for field, metadata in fields_metadata.items() if metadata.categorical]

    def validate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
        *fields: str,
    ) -> None:
        """
        Validates a number of fields according to the variable's parameters.

        :param fields_metadata: Mapping of field paths to their metadata.
        :type fields_metadata: dict[str, FieldMetadata]
        :raises ValueError: When more than one field is provided and not multicategorical,
            or when a field is not a valid candidate.
        """
        if len(fields) != 1 and not self._multicategorical:
            raise ValueError("This variable only accepts a single field")
        super().validate_fields(fields_metadata, *fields)

    def describe(
        self,
        fields_metadata: dict[str, FieldMetadata],
    ) -> dict[str, Any]:
        base_description = super().describe(fields_metadata)
        base_description["multicategorical"] = self._multicategorical
        base_description["ordered"] = self._ordered
        base_description["optional"] = self._optional
        return base_description


class NumericalVariable(ChartVariable):
    """
    Represents a numerical variable for charts.
    """

    def __init__(
        self,
        name: str,
        description: str,
        map_to: str,
        icon: str | None = None,
    ) -> None:
        """
        Constructor method.

        :param name: The name of the variable.
        :type name: str
        :param description: The description of the variable.
        :type description: str
        :param map_to: The internal parameter to map the variable to.
        :type map_to: str
        :param icon: An optional SVG icon string. Defaults to ``None``.
        :type icon: str | None, optional
        """
        super().__init__(
            name=name,
            description=description,
            map_to=map_to,
            icon=icon,
        )

    def candidate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
    ) -> list[str]:
        return [field for field, metadata in fields_metadata.items() if metadata.numeric]

    def validate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
        *fields: str,
    ) -> None:
        """
        Validates a number of fields according to the variable's parameters.

        :param fields_metadata: Mapping of field paths to their metadata.
        :type fields_metadata: dict[str, FieldMetadata]
        :raises ValueError: When more than one field is provided.
        """
        if len(fields) != 1:
            raise ValueError("This variable only accepts a single field")
        super().validate_fields(fields_metadata, *fields)


class NonDerivedNumericalVariable(NumericalVariable):
    """
    Represents a non-derived numerical variable for charts.
    """

    def __init__(
        self,
        name: str,
        description: str,
        map_to: str,
        icon: str | None = None,
    ) -> None:
        """
        Constructor method.

        :param name: The name of the variable.
        :type name: str
        :param description: The description of the variable.
        :type description: str
        :param map_to: The internal parameter to map the variable to.
        :type map_to: str
        :param icon: An optional SVG icon string. Defaults to ``None``.
        :type icon: str | None, optional
        """
        super().__init__(
            name=name,
            description=description,
            map_to=map_to,
            icon=icon,
        )

    def candidate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
    ) -> list[str]:
        return [field for field, metadata in fields_metadata.items() if metadata.numeric and not metadata.derived]


class NumericalAggregationVariable(NonDerivedNumericalVariable):
    """
    Represents a numerical aggregation variable for charts.
    """

    def __init__(
        self,
        name: str,
        description: str,
        map_to: str,
        icon: str | None = None,
    ) -> None:
        """
        Constructor method.

        :param name: The name of the variable.
        :type name: str
        :param description: The description of the variable.
        :type description: str
        :param map_to: The internal parameter to map the variable to.
        :type map_to: str
        :param icon: An optional SVG icon string. Defaults to ``None``.
        :type icon: str | None, optional
        """
        super().__init__(
            name=name,
            description=description,
            map_to=map_to,
            icon=icon,
        )


class DateTimeVariable(ChartVariable):
    """
    Represents a datetime variable for charts.
    """

    def __init__(
        self,
        name: str,
        description: str,
        map_to: str,
        icon: str | None = None,
    ) -> None:
        """
        Constructor method.

        :param name: The name of the variable.
        :type name: str
        :param description: The description of the variable.
        :type description: str
        :param map_to: The internal parameter to map the variable to.
        :type map_to: str
        :param icon: An optional SVG icon string. Defaults to ``None``.
        :type icon: str | None, optional
        """
        super().__init__(
            name=name,
            description=description,
            map_to=map_to,
            icon=icon,
        )

    def candidate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
    ) -> list[str]:
        return [
            field
            for field, metadata in fields_metadata.items()
            if metadata.effective_type is not None and issubclass(metadata.effective_type, (datetime.date, datetime.datetime))
        ]

    def validate_fields(
        self,
        fields_metadata: dict[str, FieldMetadata],
        *fields: str,
    ) -> None:
        """
        Validates a number of fields according to the variable's parameters.

        :param fields_metadata: Mapping of field paths to their metadata.
        :type fields_metadata: dict[str, FieldMetadata]
        :raises ValueError: When more than one field is provided.
        """
        if len(fields) != 1:
            raise ValueError("This variable only accepts a single field")
        super().validate_fields(fields_metadata, *fields)


class CountryCodeVariable(ChartVariable):
    """
    Represents a variable accepting only fields tagged as ISO 3166-1 alpha-2 country codes.

    Fields qualify when their metadata carries
    ``extra["suggested_validation"] == "iso_a2"``.
    """

    def candidate_fields(self, fields_metadata: dict[str, FieldMetadata]) -> list[str]:
        """
        Returns fields whose metadata carries ``suggested_validation == "iso_a2"``.

        :param fields_metadata: Mapping of field paths to their metadata.
        :type fields_metadata: dict[str, FieldMetadata]
        :return: A list of candidate field names.
        :rtype: list[str]
        """
        return [field for field, metadata in fields_metadata.items() if metadata.extra.get("suggested_validation") == "iso_a2"]

    def validate_fields(self, fields_metadata: dict[str, FieldMetadata], *fields: str) -> None:
        """
        Validates that exactly one field is provided and that it is a valid candidate.

        :param fields_metadata: Mapping of field paths to their metadata.
        :type fields_metadata: dict[str, FieldMetadata]
        :raises ValueError: When the number of fields is not exactly one,
            or the field is not a valid candidate.
        """
        if len(fields) != 1:
            raise ValueError("This variable only accepts a single field")
        super().validate_fields(fields_metadata, *fields)


ChartVariablesMap = dict[str, ChartVariable]


__all__ = [
    "Aggregation",
    "ChartVariable",
    "CategoricalVariable",
    "CountryCodeVariable",
    "NumericalVariable",
    "NonDerivedNumericalVariable",
    "NumericalAggregationVariable",
    "DateTimeVariable",
    "ChartVariablesMap",
]
