"""
FigureView — wraps a WTForms form and a figure strategy for the web layer.
"""

from typing import Any

from babel.support import Translations
from fields_metadata import FieldMetadata

from orama.enums import GeoProjection, LineInterpolation, LinePlacement, LineSortOrder, SortOrder
from orama.options import GeoProjectionOption, LineInterpolationOption, LinePlacementOption, LineSortOrderOption, SortOrderOption
from orama.strategies.base import FigureStrategy, FigureStrategyDescription
from orama.theme import Theme
from orama.variables import Aggregation, NumericalAggregationVariable
from orama.web._color_builder import _color_rule_from_subform
from orama.web.form_factory import (
    LBL_PREFIX,
    OPT_PREFIX,
    VAR_PREFIX,
    create_figure_form_class,
    label_param_name,
)


def _serialization_to_wtforms_data(data: dict[str, Any]) -> dict[str, Any]:
    """
    Convert a serialized FigureView dict back to the WTForms-compatible data dict.

    :param data: Dict produced by :meth:`FigureView.serialize`.
    :return: Dict keyed by ``var__*`` / ``opt__*`` names plus ``color_rules`` list.
    :rtype: dict[str, Any]
    """
    result: dict[str, Any] = {}
    for map_to, value in data.get("variables", {}).items():
        if isinstance(value, dict):
            result[f"{VAR_PREFIX}{map_to}"] = value.get("field")
            result[f"{VAR_PREFIX}{map_to}__function"] = value.get("function")
        else:
            result[f"{VAR_PREFIX}{map_to}"] = value
    for map_to, value in data.get("options", {}).items():
        result[f"{OPT_PREFIX}{map_to}"] = value
    result["color_rules"] = data.get("color_rules", [])
    for key, value in data.get("labels", {}).items():
        result[f"{LBL_PREFIX}{key}"] = value
    return result


class FigureView:
    """
    Binds a WTForms form to a figure strategy class, providing instantiation,
    serialization, and deserialization.

    :param figure_strategy: The figure strategy class.
    :param fields_metadata: Mapping of field paths to their metadata.
    :param dataset_name: The name of the dataset this view is associated with.
    :param formdata: HTTP form data (from ``request.form``), used on POST requests.
    :param data: Pre-populated data dict (from deserialization or GET), used otherwise.
    :param tr: Optional translations instance for form labels. Defaults to ``None``.
    :type tr: Translations | None
    """

    def __init__(
        self,
        figure_strategy: type[FigureStrategy],
        fields_metadata: dict[str, FieldMetadata],
        dataset_name: str,
        formdata: Any | None = None,
        data: dict[str, Any] | None = None,
        tr: Translations | None = None,
    ) -> None:
        self._figure_strategy = figure_strategy
        self._fields_metadata = fields_metadata
        self._dataset_name = dataset_name
        self._description: FigureStrategyDescription = figure_strategy.describe(fields_metadata)

        form_cls = create_figure_form_class(figure_strategy, fields_metadata, tr)

        if formdata is not None:
            self.form = form_cls(formdata=formdata)
        else:
            color_rules_data: list[dict[str, Any]] = (data or {}).get("color_rules", [])
            ordered_var_data: dict[str, list[str]] = {}
            for var_desc in self._description.variables.values():
                if var_desc.get("ordered", False):
                    map_to = var_desc["map_to"]
                    fk = f"{VAR_PREFIX}{map_to}"
                    ordered_var_data[fk] = list((data or {}).get(fk, []))
            excluded = {"color_rules"} | set(ordered_var_data)
            regular_data: dict[str, Any] = {k: v for k, v in (data or {}).items() if k not in excluded}
            self.form = form_cls(data=regular_data)
            self._reload_color_rules(color_rules_data)
            for field_key, values in ordered_var_data.items():
                for val in values:
                    if val:
                        new_entry = getattr(self.form, field_key).append_entry()
                        new_entry.form.variable.data = val

    @property
    def description(self) -> FigureStrategyDescription:
        """
        Returns the strategy description used to build this view.

        :return: The figure strategy description.
        :rtype: FigureStrategyDescription
        """
        return self._description

    def _extract_color_rules_data(self) -> list[dict[str, Any]]:
        """
        Extract the current color rules as a list of serializable dicts.

        :return: List of color rule dicts compatible with :meth:`_reload_color_rules`.
        :rtype: list[dict[str, Any]]
        """
        rules: list[dict[str, Any]] = []
        for entry in self.form.color_rules:
            rule: dict[str, Any] = {
                "assigner_type": entry.assigner_type.data or "ByCategory",
                "selector_type": entry.selector_type.data or "All",
                "anchor_colors": [{"name": ac.form.name.data or "", "hex": ac.hex.data or ""} for ac in entry.anchor_colors],
            }
            if entry.selector_threshold.data is not None:
                rule["selector_threshold"] = entry.selector_threshold.data
            if (entry.selector_category.data or "").strip():
                rule["selector_category"] = entry.selector_category.data
            if entry.assigner_range_min.data is not None:
                rule["assigner_range_min"] = entry.assigner_range_min.data
            if entry.assigner_range_max.data is not None:
                rule["assigner_range_max"] = entry.assigner_range_max.data
            if entry.assigner_range_center.data is not None:
                rule["assigner_range_center"] = entry.assigner_range_center.data
            rules.append(rule)
        return rules

    def _reload_color_rules(self, rules: list[dict[str, Any]]) -> None:
        """
        Clear and repopulate the color_rules FieldList from a list of rule dicts.

        :param rules: List of color rule dicts as produced by :meth:`_extract_color_rules_data`.
        :type rules: list[dict[str, Any]]
        """
        while self.form.color_rules.entries:
            self.form.color_rules.pop_entry()
        for rule_data in rules:
            entry = self.form.color_rules.append_entry()
            entry.assigner_type.data = rule_data.get("assigner_type", "ByCategory")
            entry.selector_type.data = rule_data.get("selector_type", "All")
            entry.selector_threshold.data = rule_data.get("selector_threshold")
            entry.selector_category.data = rule_data.get("selector_category", "")
            entry.assigner_range_min.data = rule_data.get("assigner_range_min")
            entry.assigner_range_max.data = rule_data.get("assigner_range_max")
            entry.assigner_range_center.data = rule_data.get("assigner_range_center")
            for ac_data in rule_data.get("anchor_colors", []):
                ac_entry = entry.anchor_colors.append_entry()
                ac_entry.form.name.data = ac_data.get("name", "")
                ac_entry.hex.data = ac_data.get("hex", "")

    def add_color_rule(self, assigner_type: str = "ByCategory", selector_type: str = "All") -> None:
        """
        Append a new color rule entry with default values.

        :param assigner_type: The assigner type identifier. Defaults to ``"ByCategory"``.
        :type assigner_type: str
        :param selector_type: The selector type identifier. Defaults to ``"All"``.
        :type selector_type: str
        """
        self.form.color_rules.append_entry(data={"assigner_type": assigner_type, "selector_type": selector_type, "anchor_colors": []})

    def remove_color_rule(self, idx: int) -> None:
        """
        Remove the color rule at the given zero-based index.

        :param idx: Zero-based index of the color rule to remove.
        :type idx: int
        :raises IndexError: If ``idx`` is out of range.
        """
        rules = self._extract_color_rules_data()
        if not 0 <= idx < len(rules):
            raise IndexError(f"Color rule index {idx} is out of range")
        rules.pop(idx)
        self._reload_color_rules(rules)

    def instantiate_figure_strategy(self, theme: Theme) -> FigureStrategy:
        """
        Build and return a FigureStrategy instance from the current form data.

        :param theme: The theme used to resolve anchor colors in color rules.
        :type theme: Theme
        :return: A configured FigureStrategy instance.
        :rtype: FigureStrategy
        """
        kwargs: dict[str, Any] = {"fields_metadata": self._fields_metadata}

        for var_desc in self._description.variables.values():
            map_to: str = var_desc["map_to"]
            var_type: type = var_desc["type"]
            if var_type == NumericalAggregationVariable:
                kwargs[map_to] = Aggregation(
                    name=map_to,
                    function=getattr(self.form, f"{VAR_PREFIX}{map_to}__function").data or "",
                    field=getattr(self.form, f"{VAR_PREFIX}{map_to}").data or None,
                )
            elif var_desc.get("ordered", False):
                kwargs[map_to] = [
                    entry.form.variable.data for entry in getattr(self.form, f"{VAR_PREFIX}{map_to}") if entry.form.variable.data
                ]
            elif var_desc.get("multicategorical", False):
                raw = getattr(self.form, f"{VAR_PREFIX}{map_to}").data
                kwargs[map_to] = raw if isinstance(raw, list) else ([raw] if raw else [])
            else:
                raw = getattr(self.form, f"{VAR_PREFIX}{map_to}").data
                kwargs[map_to] = raw if raw else None

        for opt_desc in self._description.options.values():
            map_to = opt_desc["map_to"]
            raw_data = getattr(self.form, f"{OPT_PREFIX}{map_to}").data
            if opt_desc["type"] == SortOrderOption:
                kwargs[map_to] = SortOrder(int(raw_data)) if raw_data is not None else SortOrder.NONE
            elif opt_desc["type"] == LinePlacementOption:
                kwargs[map_to] = LinePlacement(raw_data) if raw_data is not None else LinePlacement.OVERLAPPING
            elif opt_desc["type"] == LineInterpolationOption:
                kwargs[map_to] = LineInterpolation(raw_data) if raw_data is not None else LineInterpolation.LINEAR
            elif opt_desc["type"] == LineSortOrderOption:
                kwargs[map_to] = LineSortOrder(raw_data) if raw_data is not None else LineSortOrder.NONE
            elif opt_desc["type"] == GeoProjectionOption:
                kwargs[map_to] = GeoProjection(raw_data) if raw_data else GeoProjection.NATURAL_EARTH
            else:
                kwargs[map_to] = raw_data

        kwargs["color_rules"] = [_color_rule_from_subform(entry, theme) for entry in self.form.color_rules] or None

        for lbl_key in ("title", "subtitle", "caption", "description", "storytelling_context"):
            val = (getattr(self.form, f"{LBL_PREFIX}{lbl_key}").data or "").strip() or None
            if val:
                kwargs[lbl_key] = val

        seen: set[str] = set()
        for var_desc in self._description.variables.values():
            label_param = label_param_name(var_desc)
            if label_param in seen:
                continue
            seen.add(label_param)
            field_name = f"{LBL_PREFIX}{label_param}"
            if field_name in self.form._fields:
                val = (getattr(self.form, field_name).data or "").strip() or None
                if val:
                    kwargs[label_param] = val

        return self._figure_strategy(**kwargs)

    def serialize(self) -> dict[str, Any]:
        """
        Produce a JSON-serializable dict of the current form state.

        :return: Serialized representation including strategy name, variables, options and color rules.
        :rtype: dict[str, Any]
        """
        variables: dict[str, Any] = {}
        for var_desc in self._description.variables.values():
            map_to: str = var_desc["map_to"]
            var_type: type = var_desc["type"]
            if var_type == NumericalAggregationVariable:
                variables[map_to] = {
                    "field": getattr(self.form, f"{VAR_PREFIX}{map_to}").data,
                    "function": getattr(self.form, f"{VAR_PREFIX}{map_to}__function").data,
                }
            elif var_desc.get("ordered", False):
                variables[map_to] = [
                    entry.form.variable.data for entry in getattr(self.form, f"{VAR_PREFIX}{map_to}") if entry.form.variable.data
                ]
            else:
                variables[map_to] = getattr(self.form, f"{VAR_PREFIX}{map_to}").data

        options: dict[str, Any] = {}
        for opt_desc in self._description.options.values():
            map_to = opt_desc["map_to"]
            options[map_to] = getattr(self.form, f"{OPT_PREFIX}{map_to}").data

        labels: dict[str, str] = {}
        for lbl_key in ("title", "subtitle", "caption", "description", "storytelling_context"):
            val = (getattr(self.form, f"{LBL_PREFIX}{lbl_key}").data or "").strip()
            if val:
                labels[lbl_key] = val
        seen: set[str] = set()
        for var_desc in self._description.variables.values():
            label_param = label_param_name(var_desc)
            if label_param in seen:
                continue
            seen.add(label_param)
            field_name = f"{LBL_PREFIX}{label_param}"
            if field_name in self.form._fields:
                val = (getattr(self.form, field_name).data or "").strip()
                if val:
                    labels[label_param] = val

        return {
            "strategy": self._figure_strategy.__name__,
            "module": self._figure_strategy.__module__,
            "dataset_name": self._dataset_name,
            "variables": variables,
            "options": options,
            "color_rules": self._extract_color_rules_data(),
            "labels": labels,
        }

    @staticmethod
    def deserialize(
        data: dict[str, Any],
        strategy_registry: dict[str, type[FigureStrategy]],
        fields_metadata_registry: dict[str, dict[str, FieldMetadata]],
        tr: Translations | None = None,
    ) -> "FigureView":
        """
        Reconstruct a FigureView from a serialized dict.

        :param data: Dict produced by :meth:`serialize`.
        :param strategy_registry: Mapping of strategy class names to strategy classes.
        :param fields_metadata_registry: Mapping of dataset names to their field metadata.
        :param tr: Optional translations instance for form labels. Defaults to ``None``.
        :type tr: Translations | None
        :return: A restored FigureView.
        :rtype: FigureView
        """
        strategy_cls = strategy_registry[data["strategy"]]
        fields_metadata = fields_metadata_registry[data["dataset_name"]]
        wtf_data = _serialization_to_wtforms_data(data)
        return FigureView(strategy_cls, fields_metadata, data["dataset_name"], data=wtf_data, tr=tr)


__all__ = [
    "FigureView",
]
