"""
Helpers for loading NetWeaver execution results and viewer metadata.
"""

from __future__ import annotations

from typing import Any, Dict, List, Optional

import pandas as pd

from . import debug_logger
from .bridge_loader import NetWeaverBridge
from .models import WorkflowStep
from .results_manager import ExecutionStatus, ResultsManager


class NetWeaverResultsService:
    """Loads the latest NetWeaver execution and related metadata."""

    def __init__(
        self,
        netweaver_engine_path: str = "",
        results_manager: Optional[ResultsManager] = None,
    ):
        self.netweaver_engine_path = netweaver_engine_path
        self.results_manager = results_manager or ResultsManager()

    def get_latest_completed_execution(self, workflow_step_id: str) -> Optional[str]:
        """Return the latest completed execution ID for a workflow step."""
        executions = self.results_manager.get_workflow_executions(workflow_step_id)
        for execution in executions:
            if execution.status == ExecutionStatus.COMPLETED:
                return execution.execution_id
        return None

    def load_viewer_payload(self, workflow_step: WorkflowStep) -> Dict[str, Any]:
        """Build a payload suitable for the NetWeaver results dialog."""
        execution_id = self.get_latest_completed_execution(workflow_step.id)
        if not execution_id:
            raise ValueError("No completed NetWeaver execution found for this workflow step")

        results_df = self.results_manager.get_execution_results_dataframe(execution_id)
        if results_df is None or results_df.empty:
            raise ValueError("Execution results are empty")

        parameters = dict(workflow_step.parameters or {})
        workflow_metadata = dict(getattr(workflow_step, "metadata", {}) or {})
        results_metadata = dict(getattr(getattr(workflow_step, "results", None), "metadata", {}) or {})

        topic_names = self._get_topic_names(workflow_step, results_df)
        goal_field_map = self._get_goal_field_map(workflow_step, results_df)
        topic_column_map = self._build_topic_column_map(topic_names, results_df.columns.tolist(), goal_field_map)
        goal_layer_names = self._get_goal_layer_names(parameters, workflow_metadata, results_metadata)
        topic_layer_map = self._build_topic_layer_map(topic_column_map, goal_layer_names)

        output_table_name = (
            parameters.get("output_table_name")
            or workflow_metadata.get("output_table_name")
            or results_metadata.get("table_name")
            or results_metadata.get("layer_name")
            or ""
        )
        output_layer_name = (
            parameters.get("output_layer_name")
            or workflow_metadata.get("output_layer_name")
            or results_metadata.get("layer_name")
            or output_table_name
        )

        normalized_topic_lookup = {
            self._normalize_name(topic_name): topic_name
            for topic_name in topic_column_map
        }
        normalized_column_lookup = {
            self._normalize_name(column_name): column_name
            for column_name in results_df.columns.tolist()
        }
        normalized_layer_lookup = {
            self._normalize_name(layer_name): layer_name
            for layer_name in goal_layer_names
        }

        topic_action_payloads = {
            topic_name: {
                "topic_name": topic_name,
                "normalized_topic": self._normalize_name(topic_name),
                "column_name": column_name,
                "layer_name": topic_layer_map.get(topic_name, ""),
                "output_table_name": output_table_name,
                "output_layer_name": output_layer_name,
                "database_path": parameters.get("database_path", ""),
                "workflow_step_id": workflow_step.id,
            }
            for topic_name, column_name in topic_column_map.items()
        }

        # A1: Attempt to load the topic hierarchy from the engine bridge.
        model_file = parameters.get("model_file", "")
        topic_tree = self._try_get_topic_tree(model_file)

        # Q2: the node-network viewer needs the engine and the task's
        # persisted input mappings (model variable -> source column) so
        # evaluate_row resolves exactly the columns the run used.
        field_mappings: Dict[str, str] = {}
        for mapping in parameters.get("field_mappings", []) or []:
            try:
                target = str(mapping.get("target_variable", "")).strip()
                source = str(mapping.get("source_column", "")).strip()
            except AttributeError:
                continue
            if target and source and target not in field_mappings:
                field_mappings[target] = source

        # B2: Row key field for map↔table linking.
        row_key_field = "ogc_fid"
        if row_key_field not in results_df.columns:
            # Fallback to any integer-like primary key column
            for candidate in ("fid", "FID", "OBJECTID", "objectid", "id", "ID"):
                if candidate in results_df.columns:
                    row_key_field = candidate
                    break

        debug_logger.debug("Loaded NetWeaver viewer payload", {
            "workflow_step_id": workflow_step.id,
            "execution_id": execution_id,
            "topic_count": len(topic_names),
            "goal_field_count": len(goal_field_map),
            "goal_layer_count": len(goal_layer_names),
            "topic_tree_nodes": len(topic_tree),
            "result_columns": list(results_df.columns),
        })

        return {
            "workflow_step_id": workflow_step.id,
            "workflow_name": workflow_step.name,
            "execution_id": execution_id,
            "results_df": results_df,
            "topic_names": topic_names,
            "topic_column_map": topic_column_map,
            "goal_field_map": goal_field_map,
            "goal_fields": list(goal_field_map.values()),
            "goal_layer_names": goal_layer_names,
            "topic_layer_map": topic_layer_map,
            "normalized_topic_lookup": normalized_topic_lookup,
            "normalized_column_lookup": normalized_column_lookup,
            "normalized_layer_lookup": normalized_layer_lookup,
            "topic_action_payloads": topic_action_payloads,
            "output_table_name": output_table_name,
            "output_layer_name": output_layer_name,
            "database_path": parameters.get("database_path", ""),
            "model_file": model_file,
            # Q2 — node-network viewer inputs
            "engine_path": self.netweaver_engine_path,
            "field_mappings": field_mappings,
            "selected_topics": parameters.get("selected_topics", []),
            "map_topics": parameters.get("map_topics", []),
            "display_field": row_key_field,
            "primary_key": row_key_field,
            # A1
            "topic_tree": topic_tree,
            # B2
            "row_key_field": row_key_field,
        }

    # ------------------------------------------------------------------
    # A1 — topic hierarchy from bridge
    # ------------------------------------------------------------------

    def _try_get_topic_tree(self, model_file: str) -> List[Dict[str, Any]]:
        """Load the topic hierarchy via the engine bridge; return [] on any failure."""
        if not model_file or not self.netweaver_engine_path:
            return []
        try:
            bridge = NetWeaverBridge(self.netweaver_engine_path)
            return bridge.get_topic_tree(model_file) or []
        except Exception as exc:
            debug_logger.warning("Could not load NetWeaver topic tree", {
                "model_file": model_file,
                "engine_path": self.netweaver_engine_path,
                "exception": str(exc),
            })
            return []

    # ------------------------------------------------------------------
    # Internal helpers
    # ------------------------------------------------------------------

    def _get_topic_names(self, workflow_step: WorkflowStep, results_df: pd.DataFrame) -> List[str]:
        """Resolve the topic list from workflow metadata or result columns."""
        parameters = workflow_step.parameters or {}
        topic_candidates = (
            parameters.get("selected_topics", [])
            or parameters.get("map_topics", [])
            or self._get_goal_field_map(workflow_step, results_df).keys()
        )
        selected_topics = [
            str(item).strip()
            for item in topic_candidates
            if str(item).strip()
        ]
        if selected_topics:
            return selected_topics

        mapping_items = parameters.get("field_mappings", [])
        mapped_topics = []
        for mapping in mapping_items:
            target_variable = str(mapping.get("target_variable", "")).strip()
            if target_variable:
                mapped_topics.append(target_variable)
        if mapped_topics:
            return mapped_topics

        topic_names = []
        for column in results_df.columns:
            if str(column).lower().startswith("nw_"):
                topic_names.append(self._column_to_topic_name(str(column)))
        return topic_names

    def _get_goal_field_map(
        self,
        workflow_step: WorkflowStep,
        results_df: pd.DataFrame,
    ) -> Dict[str, str]:
        """Resolve selected NetWeaver goals to concrete output columns."""
        parameters = workflow_step.parameters or {}
        workflow_metadata = getattr(workflow_step, "metadata", {}) or {}
        results_metadata = getattr(getattr(workflow_step, "results", None), "metadata", {}) or {}

        stored_map = (
            workflow_metadata.get("goal_field_map")
            or results_metadata.get("goal_field_map")
            or {}
        )
        if isinstance(stored_map, dict) and stored_map:
            normalized_columns = {
                self._normalize_name(column_name): str(column_name)
                for column_name in results_df.columns.tolist()
            }
            resolved_map: Dict[str, str] = {}
            for topic_name, column_name in stored_map.items():
                direct_match = normalized_columns.get(self._normalize_name(str(column_name)))
                if direct_match:
                    resolved_map[str(topic_name)] = direct_match
            if resolved_map:
                return resolved_map

        requested_topics = [
            str(item).strip()
            for item in (parameters.get("map_topics") or parameters.get("selected_topics") or [])
            if str(item).strip()
        ]
        normalized_columns = {
            self._normalize_name(column_name): str(column_name)
            for column_name in results_df.columns.tolist()
        }

        goal_field_map: Dict[str, str] = {}
        for topic_name in requested_topics:
            normalized_topic = self._normalize_name(topic_name)
            direct_match = normalized_columns.get(normalized_topic)
            if direct_match:
                goal_field_map[topic_name] = direct_match
                continue

            nw_match = normalized_columns.get(f"nw_{normalized_topic}")
            if nw_match:
                goal_field_map[topic_name] = nw_match

        if goal_field_map:
            return goal_field_map

        for column_name in results_df.columns.tolist():
            column_text = str(column_name)
            if column_text.lower().startswith("nw_"):
                label = self._column_to_topic_name(column_text) or column_text
                goal_field_map[label] = column_text
        return goal_field_map

    def _build_topic_column_map(
        self,
        topic_names: List[str],
        result_columns: List[str],
        goal_field_map: Dict[str, str],
    ) -> Dict[str, str]:
        """Map display topic names to DataFrame columns."""
        if goal_field_map:
            topic_map = dict(goal_field_map)
        else:
            topic_map = {}

        normalized_columns = {
            self._normalize_name(column): column
            for column in result_columns
        }

        for topic_name in topic_names:
            if topic_name in topic_map:
                continue

            normalized_topic = self._normalize_name(topic_name)
            direct_match = normalized_columns.get(normalized_topic)
            if direct_match:
                topic_map[topic_name] = direct_match
                continue

            nw_candidate = f"nw_{normalized_topic}"
            if nw_candidate in normalized_columns:
                topic_map[topic_name] = normalized_columns[nw_candidate]

        return topic_map

    @staticmethod
    def _get_goal_layer_names(
        parameters: Dict[str, Any],
        workflow_metadata: Dict[str, Any],
        results_metadata: Dict[str, Any],
    ) -> List[str]:
        """Resolve persisted goal-layer names, if any."""
        candidates = (
            parameters.get("goal_layers")
            or workflow_metadata.get("goal_layers")
            or results_metadata.get("goal_layers")
            or []
        )
        return [
            str(layer_name).strip()
            for layer_name in candidates
            if str(layer_name).strip()
        ]

    def _build_topic_layer_map(
        self,
        topic_column_map: Dict[str, str],
        goal_layer_names: List[str],
    ) -> Dict[str, str]:
        """Best-effort mapping from topic names to goal-specific layer names."""
        if not goal_layer_names or not topic_column_map:
            return {}

        topic_names = list(topic_column_map.keys())
        topic_layer_map: Dict[str, str] = {}

        if len(goal_layer_names) == len(topic_names):
            for topic_name, layer_name in zip(topic_names, goal_layer_names):
                topic_layer_map[topic_name] = layer_name

        normalized_layers = {
            self._normalize_name(layer_name): layer_name
            for layer_name in goal_layer_names
        }
        for topic_name in topic_names:
            if topic_name in topic_layer_map:
                continue

            normalized_topic = self._normalize_name(topic_name)
            direct_match = normalized_layers.get(normalized_topic)
            if direct_match:
                topic_layer_map[topic_name] = direct_match
                continue

            suffix_match = self._find_layer_by_topic_suffix(goal_layer_names, normalized_topic)
            if suffix_match:
                topic_layer_map[topic_name] = suffix_match

        return topic_layer_map

    def _find_layer_by_topic_suffix(self, goal_layer_names: List[str], normalized_topic: str) -> str:
        """Match '<workflow name> - <topic>' layer names back to a topic."""
        for layer_name in goal_layer_names:
            normalized_layer = self._normalize_name(layer_name)
            if normalized_layer.endswith(f"_{normalized_topic}") or normalized_layer.endswith(f"-{normalized_topic}"):
                return layer_name
            if normalized_layer.endswith(normalized_topic):
                return layer_name
        return ""

    @staticmethod
    def _column_to_topic_name(column_name: str) -> str:
        """Turn a results column into a user-facing topic name."""
        if column_name.lower().startswith("nw_"):
            column_name = column_name[3:]
        return column_name.replace("_", " ").strip()

    @staticmethod
    def _normalize_name(name: str) -> str:
        """Normalize topic, column, and layer names for comparison."""
        normalized = str(name).strip().lower().replace(" ", "_")
        while "__" in normalized:
            normalized = normalized.replace("__", "_")
        return normalized.replace("-", "_")
