"""
QGIS Layer Creation from Workflow Results

Creates and manages GIS layers from workflow execution results.

Author: EMDS QGIS Port POC
Date: 2025-11-07
"""

from pathlib import Path
from typing import Optional, List, Dict, Any, Tuple
import pandas as pd
from qgis.core import (
    QgsVectorLayer, QgsFields, QgsField, QgsFeature, QgsGeometry,
    QgsPoint, QgsProject, QgsLayerTreeLayer, QgsWkbTypes,
    QgsSymbol, QgsRendererRange, QgsGraduatedSymbolRenderer,
    QgsRendererCategory, QgsCategorizedSymbolRenderer
)
from .qt_compat import QVariant, QColor
from .netweaver_scheme import NETWEAVER_CLASS_BREAKS, NO_DATA_LABEL, color_for_label
from qgis.core import QgsRasterLayer


class ResultsLayerCreator:
    """Creates QGIS layers from workflow results"""

    def __init__(self, project: QgsProject = None):
        """
        Initialize layer creator

        Args:
            project: QGIS project (uses active project if None)
        """
        self.project = project or QgsProject.instance()

    def create_vector_layer_from_results(
        self,
        results_df: pd.DataFrame,
        layer_name: str,
        input_layer: 'QgsVectorLayer' = None,
        output_path: Optional[str] = None,
        add_to_project: bool = True,
    ) -> Optional[QgsVectorLayer]:
        """
        Create vector layer from results DataFrame

        Args:
            results_df: Results DataFrame with goal/variable values
            layer_name: Name for the new layer
            input_layer: Optional input layer to copy geometry from
            output_path: Optional path to save as shapefile/geopackage

        Returns:
            Created QgsVectorLayer or None
        """
        try:
            # Determine if we have geometry
            has_geometry = input_layer is not None

            # Create vector layer
            if has_geometry:
                # Copy geometry from input layer
                return self._create_layer_with_geometry(
                    results_df, layer_name, input_layer, output_path, add_to_project
                )
            else:
                # Create attribute-only layer
                return self._create_attribute_layer(
                    results_df, layer_name, output_path, add_to_project
                )

        except Exception as e:
            print(f"Error creating vector layer: {e}")
            return None

    def _create_layer_with_geometry(
        self,
        results_df: pd.DataFrame,
        layer_name: str,
        input_layer: 'QgsVectorLayer',
        output_path: Optional[str] = None,
        add_to_project: bool = True,
    ) -> Optional[QgsVectorLayer]:
        """
        Create vector layer with geometry from input layer

        Args:
            results_df: Results DataFrame
            layer_name: Layer name
            input_layer: Input layer with geometry
            output_path: Optional output file path

        Returns:
            Created QgsVectorLayer
        """
        # Get CRS from input layer
        crs = input_layer.crs()
        geometry_name = QgsWkbTypes.displayString(input_layer.wkbType()) or "Point"

        # Create new vector layer
        vl = QgsVectorLayer(f"{geometry_name}?crs={crs.authid()}", layer_name, "memory")
        provider = vl.dataProvider()

        # Preserve the source attributes, then append any new engine output fields.
        fields = QgsFields()
        source_field_names: List[str] = []
        for source_field in input_layer.fields():
            fields.append(source_field)
            source_field_names.append(source_field.name())
        for result_field in self._create_fields_from_dataframe(results_df):
            if result_field.name() not in source_field_names:
                fields.append(result_field)
        provider.addAttributes(fields)
        vl.updateFields()
        field_indexes = {field.name(): index for index, field in enumerate(vl.fields())}
        input_features, result_rows_by_key, join_key = self._ordered_features_and_results(
            input_layer,
            results_df,
        )

        # Copy features from input layer, adding results as attributes
        features = []
        for idx, feature in enumerate(input_features):
            if idx >= len(results_df):
                break

            new_feature = QgsFeature(vl.fields())

            # Copy geometry
            new_feature.setGeometry(feature.geometry())
            new_feature.setAttributes(feature.attributes())

            # Merge engine output fields onto the source attributes.
            result_row = None
            if result_rows_by_key and join_key:
                feature_key = self._stable_join_value(feature[join_key])
                result_row = result_rows_by_key.get(feature_key)
            if result_row is None:
                result_row = results_df.iloc[idx]
            for col_name in results_df.columns:
                field_index = field_indexes.get(col_name)
                if field_index is None:
                    continue
                new_feature.setAttribute(field_index, self._coerce_attribute_value(result_row[col_name]))

            features.append(new_feature)

        # Add features to layer
        provider.addFeatures(features)

        # Save to file if specified
        if output_path:
            self._save_layer_to_file(vl, output_path)

        self._apply_attribute_table_sort(vl)

        if add_to_project:
            self.project.addMapLayer(vl)

        return vl

    @staticmethod
    def _preferred_stable_key_column(columns: List[str]) -> str:
        lookup = {str(column).strip().lower(): str(column) for column in columns}
        for candidate in (
            "input_objectid",
            "objectid",
            "source_objectid",
            "emds_source_feature_id",
            "input_fid_attr",
            "ogc_fid",
            "fid",
        ):
            if candidate in lookup:
                return lookup[candidate]
        return ""

    @staticmethod
    def _stable_join_value(value: Any) -> str:
        if value is None:
            return ""
        try:
            if hasattr(value, "item"):
                value = value.item()
        except Exception:
            pass
        text = str(value).strip()
        if not text:
            return ""
        try:
            numeric = float(text)
            if numeric.is_integer():
                return str(int(numeric))
        except Exception:
            pass
        return text

    def _ordered_features_and_results(
        self,
        input_layer: 'QgsVectorLayer',
        results_df: pd.DataFrame,
    ) -> Tuple[List['QgsFeature'], Dict[str, Any], str]:
        """Return input features in stable-key order and keyed result rows when possible."""
        features = list(input_layer.getFeatures())
        field_names = [field.name() for field in input_layer.fields()]
        join_key = self._preferred_stable_key_column(field_names)
        if not join_key or join_key not in results_df.columns:
            return features, {}, ""

        result_rows_by_key: Dict[str, Any] = {}
        for _, result_row in results_df.iterrows():
            key_value = self._stable_join_value(result_row.get(join_key))
            if key_value and key_value not in result_rows_by_key:
                result_rows_by_key[key_value] = result_row

        if len(result_rows_by_key) != len(results_df.index):
            return features, {}, ""

        try:
            ordered_features = sorted(
                features,
                key=lambda feature: (
                    float(self._stable_join_value(feature[join_key])),
                    self._stable_join_value(feature[join_key]),
                ),
            )
        except Exception:
            ordered_features = sorted(
                features,
                key=lambda feature: self._stable_join_value(feature[join_key]),
            )

        return ordered_features, result_rows_by_key, join_key

    def _apply_attribute_table_sort(self, layer: QgsVectorLayer) -> None:
        """Ask QGIS to show result attribute tables in stable-key order when available."""
        key_column = self._preferred_stable_key_column(layer.fields().names())
        if not key_column or not hasattr(layer, "attributeTableConfig"):
            return
        try:
            config = layer.attributeTableConfig()
            if hasattr(config, "setSortExpression"):
                config.setSortExpression(f'"{key_column}"')
            if hasattr(config, "setSortOrder"):
                config.setSortOrder(0)
            if hasattr(layer, "setAttributeTableConfig"):
                layer.setAttributeTableConfig(config)
        except Exception:
            pass

    def _create_attribute_layer(
        self,
        results_df: pd.DataFrame,
        layer_name: str,
        output_path: Optional[str] = None,
        add_to_project: bool = True,
    ) -> Optional[QgsVectorLayer]:
        """
        Create attribute-only vector layer (no geometry)

        Args:
            results_df: Results DataFrame
            layer_name: Layer name
            output_path: Optional output file path

        Returns:
            Created QgsVectorLayer
        """
        # Create point layer (even though no real geometry)
        vl = QgsVectorLayer("Point", layer_name, "memory")
        provider = vl.dataProvider()

        # Add fields
        fields = self._create_fields_from_dataframe(results_df)
        provider.addAttributes(fields)
        vl.updateFields()

        # Create features from rows
        features = []
        for idx, row in results_df.iterrows():
            feature = QgsFeature(vl.fields())

            # Set dummy geometry (0,0)
            feature.setGeometry(QgsGeometry.fromPointXY(QgsPoint(0, 0)))

            # Set attributes
            for col_idx, col_name in enumerate(results_df.columns):
                feature.setAttribute(col_idx, self._coerce_attribute_value(row[col_name]))

            features.append(feature)

        provider.addFeatures(features)

        # Save if specified
        if output_path:
            self._save_layer_to_file(vl, output_path)

        if add_to_project:
            self.project.addMapLayer(vl)

        return vl

    def _create_fields_from_dataframe(self, df: pd.DataFrame) -> QgsFields:
        """
        Create QgsFields from DataFrame columns

        Args:
            df: Input DataFrame

        Returns:
            QgsFields object
        """
        fields = QgsFields()

        for col_name in df.columns:
            col_data = df[col_name]

            # Determine field type
            if col_data.dtype in ['float64', 'float32']:
                field = QgsField(col_name, QVariant.Double)
            elif col_data.dtype in ['int64', 'int32']:
                field = QgsField(col_name, QVariant.Int)
            elif col_data.dtype == 'bool':
                field = QgsField(col_name, QVariant.Bool)
            else:
                field = QgsField(col_name, QVariant.String)

            fields.append(field)

        return fields

    @staticmethod
    def _coerce_attribute_value(value):
        """Convert pandas values into QGIS-friendly Python scalars."""
        if pd.isna(value):
            return None
        if hasattr(value, "item"):
            try:
                value = value.item()
            except Exception:
                pass
        if isinstance(value, (int, float, str, bool)):
            return value
        return str(value)

    def _save_layer_to_file(self, layer: QgsVectorLayer, output_path: str) -> bool:
        """
        Save vector layer to file

        Args:
            layer: Vector layer to save
            output_path: Path to save to (shapefile or geopackage)

        Returns:
            True if successful
        """
        try:
            output_path = Path(output_path)

            if output_path.suffix.lower() == '.gpkg':
                # Save as GeoPackage
                layer.dataProvider().dataSourceUri = f"sqlite:///{output_path}"
            else:
                # Save as Shapefile
                from qgis.core import QgsVectorFileWriter

                writer = QgsVectorFileWriter.writeAsVectorFormatV3(
                    layer,
                    str(output_path),
                    layer.dataProvider().transformContext(),
                    QgsVectorFileWriter.SaveVectorOptions()
                )

                return writer[0] == QgsVectorFileWriter.WriterError.NoError

            return True

        except Exception as e:
            print(f"Error saving layer: {e}")
            return False

    def apply_style_to_layer(
        self,
        layer: QgsVectorLayer,
        color_column: Optional[str] = None,
        color_scheme: str = "RdYlGn"
    ) -> None:
        """
        Apply styling to results layer

        Args:
            layer: Vector layer to style
            color_column: Column name to color by (optional)
            color_scheme: Color scheme name
        """
        try:
            if not color_column or color_column not in layer.fields().names():
                # Apply default style
                renderer = layer.renderer()
                if renderer:
                    for symbol in renderer.symbols(None) if hasattr(renderer, 'symbols') else []:
                        symbol.setColor(QColor(100, 150, 255))
                return

            # TODO: Implement graduated/categorized rendering based on column values
            # This would require matplotlib color mapping or QGIS color ramps

        except Exception as e:
            print(f"Error applying style: {e}")

    def apply_netweaver_goal_style(self, layer: QgsVectorLayer, goal_field: str) -> None:
        """Apply the fixed 5-class NetWeaver symbology to a goal field."""
        if goal_field not in layer.fields().names():
            raise ValueError(f"Goal field '{goal_field}' is not present on layer '{layer.name()}'")

        renderer_ranges: List[QgsRendererRange] = []
        for lower, upper, label, color_hex in NETWEAVER_CLASS_BREAKS:
            symbol = QgsSymbol.defaultSymbol(layer.geometryType())
            symbol.setColor(QColor(color_hex))
            renderer_ranges.append(QgsRendererRange(lower, upper, symbol, label))

        # D5: NETWEAVER_CLASS_BREAKS spans -999999.0 .. 1.0, so the engine's
        # unevaluated marker (+1.797e308) in a legacy table matched NO range and
        # rendered as nothing at all — an invisible feature reads as a missing
        # polygon, not as "no data". Append a terminal grey range above 1.0.
        # New results persist NULL instead (see netweaver_scheme.numeric_or_none),
        # so this only has to cover tables written before that fix.
        # ponytail: the legend now shows "No data" twice. Upgrading means adding
        # an explicit terminal pair to NETWEAVER_CLASS_BREAKS, which would desync
        # the 6-entry ramp from the ArcGIS TruthValueScheme it is byte-matched to.
        terminal_symbol = QgsSymbol.defaultSymbol(layer.geometryType())
        terminal_symbol.setColor(QColor(color_for_label(NO_DATA_LABEL)))
        renderer_ranges.append(
            QgsRendererRange(1.0, float("inf"), terminal_symbol, NO_DATA_LABEL)
        )

        renderer = QgsGraduatedSymbolRenderer(goal_field, renderer_ranges)
        renderer.setMode(QgsGraduatedSymbolRenderer.Custom)
        layer.setRenderer(renderer)
        layer.triggerRepaint()

    def apply_cdp_priority_style(self, layer: QgsVectorLayer, score_field: str) -> None:
        """Apply the fixed 5-class CDP prioritization symbology to PriorityScores."""
        if score_field not in layer.fields().names():
            raise ValueError(f"CDP score field '{score_field}' is not present on layer '{layer.name()}'")

        ranges = [
            (0.0, 0.2, "Very Low", QColor("#d73027")),
            (0.2, 0.4, "Low", QColor("#fc8d59")),
            (0.4, 0.6, "Medium", QColor("#fee08b")),
            (0.6, 0.8, "High", QColor("#91cf60")),
            (0.8, 1.000001, "Very High", QColor("#1a9850")),
        ]

        renderer_ranges: List[QgsRendererRange] = []
        for lower, upper, label, color in ranges:
            symbol = QgsSymbol.defaultSymbol(layer.geometryType())
            symbol.setColor(color)
            renderer_ranges.append(QgsRendererRange(lower, upper, symbol, label))

        renderer = QgsGraduatedSymbolRenderer(score_field, renderer_ranges)
        renderer.setMode(QgsGraduatedSymbolRenderer.Custom)
        layer.setRenderer(renderer)
        layer.triggerRepaint()

    def apply_portfolio_inportfolio_style(
        self,
        layer: QgsVectorLayer,
        inportfolio_field: str = "InPortfolio",
    ) -> None:
        """
        Apply a categorized 0/1 symbology to a Portfolio result layer.

        The Portfolio engine writes 0 (not selected) or 1 (selected) into the
        ``InPortfolio`` column. This renders selected projects in bright green
        and not-selected projects in muted red so the map immediately
        communicates the allocation outcome.

        Args:
            layer: Vector layer carrying the portfolio-augmented data.
            inportfolio_field: Name of the binary 0/1 selection column.
        """
        if inportfolio_field not in layer.fields().names():
            raise ValueError(
                f"Portfolio selection field '{inportfolio_field}' is not present "
                f"on layer '{layer.name()}'"
            )

        categories: List[QgsRendererCategory] = []

        # Category 0: Not Selected -- translucent muted red
        not_selected_symbol = QgsSymbol.defaultSymbol(layer.geometryType())
        not_selected_symbol.setColor(QColor(200, 80, 80, 150))
        categories.append(
            QgsRendererCategory(0, not_selected_symbol, "Not Selected")
        )

        # Category 1: Selected -- solid green
        selected_symbol = QgsSymbol.defaultSymbol(layer.geometryType())
        selected_symbol.setColor(QColor(60, 160, 70, 220))
        categories.append(
            QgsRendererCategory(1, selected_symbol, "Selected")
        )

        # Fallback category for rows where the value is missing/unexpected.
        fallback_symbol = QgsSymbol.defaultSymbol(layer.geometryType())
        fallback_symbol.setColor(QColor(180, 180, 180, 120))
        categories.append(QgsRendererCategory("", fallback_symbol, "Unknown"))

        renderer = QgsCategorizedSymbolRenderer(inportfolio_field, categories)
        layer.setRenderer(renderer)
        layer.triggerRepaint()

    def export_results_to_geopackage(
        self,
        results_df: pd.DataFrame,
        output_path: str,
        layer_name: str,
        input_layer: Optional['QgsVectorLayer'] = None
    ) -> bool:
        """
        Export results to GeoPackage file

        Args:
            results_df: Results DataFrame
            output_path: Path to GeoPackage file
            layer_name: Layer name in GeoPackage
            input_layer: Optional input layer for geometry

        Returns:
            True if successful
        """
        try:
            output_path = Path(output_path)
            output_path.parent.mkdir(parents=True, exist_ok=True)

            # Create layer
            vl = self.create_vector_layer_from_results(
                results_df, layer_name, input_layer
            )

            if not vl:
                return False

            # Remove from project temporarily (we'll re-add after saving)
            self.project.removeMapLayer(vl.id())

            # Save to GeoPackage
            from qgis.core import QgsVectorFileWriter

            writer = QgsVectorFileWriter.writeAsVectorFormatV3(
                vl,
                str(output_path),
                vl.dataProvider().transformContext(),
                QgsVectorFileWriter.SaveVectorOptions()
            )

            if writer[0] != QgsVectorFileWriter.WriterError.NoError:
                return False

            # Re-add to project
            vl = QgsVectorLayer(f"{output_path}|layername={layer_name}", layer_name, "ogr")
            self.project.addMapLayer(vl)

            return True

        except Exception as e:
            print(f"Error exporting to GeoPackage: {e}")
            return False
