from typing import Any, Dict, List, Optional, Union

from .report_title import ReportTitle
from .report_value import ReportValue
from .report_value_formatters import ReportValueFormatters


class ReportLayoutResponse(object):
    """Base class for structured report responses.

    Manages filters, insight, consolidation, hierarchy and data sections.
    Subclasses extend this with renderers and preparers via ``get_report_data()``.
    """

    def __init__(self):
        """Creates an empty ReportLayoutResponse."""
        self.filters = None
        self.empty = False

        self.insight = None
        self.insight_data = {
            "options": {},
            "title": None,
            "threshold": 0,
            "threshold_color": "ok",
            "total": 0,
            "value": None,
        }

        self.consolidation = None
        self.consolidation_data = {"options": {}}

        self.hierarchy = None
        self.hierarchy_data = {"headers": [], "nodes": {}}

        self.report_data = None
        self.source_data = None

    def set_empty(self, empty: bool):
        """Marks the report as empty, adding an ``empty`` flag to the response.

        Args:
            empty: Whether the report has no data.
        """
        self.empty = empty

    def set_filter_title(self, title: ReportTitle):
        """Sets the filter section title.

        Args:
            title: Title to display above the filters.
        """
        if self.filters is None:
            self.filters = {}
        self.filters["title"] = title.title_content

    def add_filter(self, title: ReportTitle):
        """Appends a filter field entry.

        Args:
            title: Filter field label.
        """
        if self.filters is None:
            self.filters = {}
        if "fields" not in self.filters:
            self.filters["fields"] = []
        self.filters["fields"].append({"title": title.title_content})

    def add_insight_option_value(self, insight_key: str, insight_sum: float, insight_title: Optional[Any] = None):
        """Accumulates a value into an insight option bucket.

        Args:
            insight_key: Identifier for the insight option.
            insight_sum: Value to add to the bucket total.
            insight_title: Display label for this option. Defaults to ``insight_key``.
        """
        if insight_key not in self.insight_data["options"]:
            self.insight_data["options"][insight_key] = {
                "title_params": insight_title if insight_title is not None else insight_key,
                "total": 0.0,
            }
        self.insight_data["options"][insight_key]["total"] += insight_sum
        self._add_insight_total(insight_sum)

    def set_insight_threshold(self, threshold: float, threshold_color: Optional[str] = "ok"):
        """Sets the insight threshold and its color.

        Args:
            threshold: Threshold value for coloring the insight.
            threshold_color: Color when above threshold (``"ok"`` or ``"alert"``).
        """
        self.insight_data["threshold"] += threshold
        self.insight_data["threshold_color"] = threshold_color

    def set_insight_title(self, title: ReportTitle):
        """Sets a custom title for the insight section.

        Args:
            title: Insight section title.
        """
        self.insight_data["title"] = title

    def set_insight_value(self, value: Optional[float]):
        """Sets an explicit insight value instead of computing it from options.

        Args:
            value: Percent value to display, or ``None`` to clear.
        """
        self.insight_data["value"] = (
            None if value is None else ReportValue(value=value, formatter=ReportValueFormatters.PERCENT)
        )

    def set_consolidation_option(
        self,
        consolidation_key: str,
        title: ReportTitle,
        initial_value: ReportValue,
        line: int,
        order: Optional[int] = None,
    ):
        """Registers a consolidation metric.

        Args:
            consolidation_key: Unique identifier for the metric.
            title: Display label.
            initial_value: Starting value (will be accumulated).
            line: Row index in the consolidation grid.
            order: Column order within the row.
        """
        self.consolidation_data["options"][consolidation_key] = {
            "title": title,
            "value": initial_value,
            "line": line,
            "order": order or 0,
        }

    def add_consolidation_option_value(self, consolidation_key: str, value: Any):
        """Accumulates a value into an existing consolidation metric.

        Args:
            consolidation_key: Identifier of the target metric.
            value: Value to add.
        """
        self.consolidation_data["options"][consolidation_key]["value"].add_to_value(value)

    def add_hierarchy_header(self, title: ReportTitle, value_id: str):
        """Appends a column header to the hierarchy table.

        Args:
            title: Column label.
            value_id: Identifier used to match node values to this column.
        """
        self.hierarchy_data["headers"].append({"title": title.title_content, "id": value_id})

    def add_hierarchy_node_value(self, node_id: str, value_key: str, value: ReportValue):
        """Accumulates a value into a hierarchy node, propagating up to parents.

        Args:
            node_id: Target node identifier.
            value_key: Column identifier.
            value: Value to accumulate.
        """
        if node_id not in self.hierarchy_data["nodes"]:
            self.hierarchy_data["nodes"][node_id] = {"valid": False, "values": {}}

        if value_key not in self.hierarchy_data["nodes"][node_id]["values"]:
            self.hierarchy_data["nodes"][node_id]["values"][value_key] = value
        else:
            self.hierarchy_data["nodes"][node_id]["values"][value_key].add_to_value(value)

        if (
            "parent_node_id" in self.hierarchy_data["nodes"][node_id]
            and self.hierarchy_data["nodes"][node_id]["parent_node_id"] is not None
        ):
            self.add_hierarchy_node_value(
                self.hierarchy_data["nodes"][node_id]["parent_node_id"], value_key, value.clone()
            )

    def add_hierarchy_node_values(self, node_id: str, values: Dict[str, ReportValue] = None):
        """Accumulates multiple values into a hierarchy node.

        Args:
            node_id: Target node identifier.
            values: Mapping of column identifier to value.
        """
        if values is not None:
            for key, value in values.items():
                self.add_hierarchy_node_value(node_id, key, value)

    def set_hierarchy_node_data(
        self,
        node_id: str,
        name: str,
        values: Dict[str, ReportValue] = None,
        parent_node_id: Optional[str] = None,
        group: Optional[bool] = False,
    ):
        """Registers or updates a hierarchy node.

        Args:
            node_id: Node identifier.
            name: Display name for the node.
            values: Initial values for this node.
            parent_node_id: Parent node identifier, if any.
            group: Whether this node is a group header.
        """
        previous_value = {}
        if node_id:
            if node_id in self.hierarchy_data["nodes"]:
                previous_value = self.hierarchy_data["nodes"][node_id]["values"]

            self.hierarchy_data["nodes"][node_id] = {
                "node_id": node_id,
                "valid": True,
                "name": name,
                "values": previous_value,
                "parent_node_id": parent_node_id,
                "group": group,
            }

            if previous_value is not None:
                for key, value in previous_value.items():
                    self.add_hierarchy_node_value(parent_node_id, key, value.clone())

            if values is not None:
                self.add_hierarchy_node_values(node_id, values)

    def set_report_data(self, report_data: Any):
        """Sets the main data payload of the report.

        Args:
            report_data: Serializable data to include in the response.
        """
        self.report_data = report_data

    def add_source_data(self, title: ReportTitle, value: Union[ReportTitle, ReportValue]):
        """Appends a source data entry (e.g. filter summary row).

        Args:
            title: Row label.
            value: Row value, either a title or a formatted value.
        """
        if self.source_data is None:
            self.source_data = []
        self.source_data.append(
            {
                "title": title.title_content,
                "value": value.title_content if isinstance(value, ReportTitle) else value.value_content,
            }
        )

    def get_report_data(self):
        """Builds and returns the full report response dict.

        Returns:
            Dict with filters, insight, consolidation, hierarchy, data and source_data sections.
        """
        self._build_headers_values()
        result = {
            "filters": self.filters,
            "insight": self.insight,
            "consolidation": self.consolidation,
            "hierarchy": self.hierarchy,
            "data": self.report_data,
            "source_data": self.source_data,
        }
        if self.empty:
            result["empty"] = True
        return result

    def _add_insight_total(self, sum_amount: float):
        self.insight_data["total"] += sum_amount

    def _build_headers_values(self):
        self._build_headers_insight()
        self._build_headers_consolidation()
        self._build_headers_hierarchy()

    def _build_headers_insight(self):
        value = None
        title = self.insight_data.get("title")
        percentage_value = 100.0

        if title is None:
            title = ReportTitle("$COMMONS_INSIGHT_TEXT")
        if self.insight_data["value"] is not None:
            value = self.insight_data["value"]
        elif len(self.insight_data["options"]) > 0:
            best_insight_option = None
            best_insight_option_value = None

            for key, opt in self.insight_data["options"].items():
                insight_total = opt["total"]
                insight_title = opt["title_params"]
                if best_insight_option_value is None or insight_total > best_insight_option_value:
                    best_insight_option = insight_title
                    best_insight_option_value = insight_total

            title.update_params(best_insight_option)

            if self.insight_data["total"] > 0:
                percentage_value = (best_insight_option_value / self.insight_data["total"]) * 100.0

            value = ReportValue(percentage_value, ReportValueFormatters.PERCENT)

        if value is not None:
            above_color = self.insight_data["threshold_color"]
            below_color = "alert" if self.insight_data["threshold_color"] == "ok" else "ok"
            self.insight = {
                "title": title.title_content,
                "value": value.value_content,
                "type": above_color if percentage_value >= self.insight_data["threshold"] else below_color,
            }

    def _build_headers_consolidation(self):
        if len(self.consolidation_data["options"]) > 0:
            max_lines = max([option["line"] for option in self.consolidation_data["options"].values()])
            self.consolidation = {"fields": []}
            for line_index in range(max_lines + 1):
                consolidation_line = [
                    {"title": option["title"].title_content, "value": option["value"].value_content}
                    for option in self.consolidation_data["options"].values()
                    if option["line"] == line_index
                ]
                self.consolidation["fields"].append(consolidation_line)

    def _build_headers_hierarchy(self):
        if len(self.hierarchy_data["nodes"]) > 0:
            self.hierarchy = {
                "headers": self.hierarchy_data["headers"],
                "data": self._build_headers_hierarchy_children(),
            }

    def _build_headers_hierarchy_children(self, parent_node_id: Optional[str] = None) -> Optional[List[Dict]]:
        if parent_node_id is None:
            nodes = [
                node
                for node in self.hierarchy_data["nodes"].values()
                if (
                    node.get("parent_node_id") is None
                    or node.get("parent_node_id") not in self.hierarchy_data["nodes"]
                    or not self.hierarchy_data["nodes"][node.get("parent_node_id")]["valid"]
                )
                and node["valid"]
            ]
        else:
            nodes = [
                node
                for node in self.hierarchy_data["nodes"].values()
                if node.get("parent_node_id") == parent_node_id and node["valid"]
            ]

        result = []
        for node in nodes:
            node_json = {
                "name": node["name"],
                "group": node["group"],
                "values": {key: value.value_content for key, value in node["values"].items()},
            }
            children = self._build_headers_hierarchy_children(node["node_id"])
            if children is not None and len(children) > 0:
                node_json["children"] = children
            result.append(node_json)
        return result
