from __future__ import annotations

"""Layer factory for SecInterp preview.

Handles creation of temporary memory layers and configuration of native QGIS symbology.
"""

import math
from typing import Any, ClassVar

from qgis.core import (
    QgsCategorizedSymbolRenderer,
    QgsClassificationFixedInterval,
    QgsFeature,
    QgsFillSymbol,
    QgsGeometry,
    QgsGraduatedSymbolRenderer,
    QgsLineSymbol,
    QgsPalLayerSettings,
    QgsPointXY,
    QgsProject,
    QgsRendererCategory,
    QgsSingleSymbolRenderer,
    QgsStyle,
    QgsTextFormat,
    QgsVectorLayer,
    QgsVectorLayerSimpleLabeling,
)
from qgis.PyQt.QtGui import QColor

from sec_interp.core.domain import GeologyData, ProfileData, StructureData
from sec_interp.core.utils.geometry_utils.optimization import PreviewOptimizer
from sec_interp.logger_config import get_logger

logger = get_logger(__name__)


class PreviewLayerFactory:
    """Factory for creating and styling QGIS memory layers for the preview."""

    # Color palette for geological units
    GEOLOGY_COLORS: ClassVar[list[QColor]] = [
        QColor(231, 76, 60),  # Red
        QColor(52, 152, 219),  # Blue
        QColor(46, 204, 113),  # Green
        QColor(155, 89, 182),  # Purple
        QColor(241, 196, 15),  # Yellow
        QColor(230, 126, 34),  # Orange
        QColor(26, 188, 156),  # Turquoise
        QColor(52, 73, 94),  # Dark Blue/Grey
        QColor(149, 165, 166),  # Grey
        QColor(211, 84, 0),  # Pumpkin
        QColor(192, 57, 43),  # Dark Red
        QColor(127, 140, 141),  # Dark Grey
        QColor(142, 68, 173),  # Wisteria
        QColor(41, 128, 185),  # Belize Hole
        QColor(39, 174, 96),  # Nephritis
        QColor(22, 160, 133),  # Green Sea
    ]

    def __init__(self):
        """Initialize the layer factory."""
        self.active_units: dict[str, QColor] = {}

    def get_color_for_unit(self, name: str) -> QColor:
        """Get a consistent color for a geological unit based on its name."""
        if not name:
            return QColor(100, 100, 100)  # Default grey

        if name in self.active_units:
            return self.active_units[name]

        # Simple hash to map name to index
        hash_val = sum(ord(c) for c in str(name))
        index = hash_val % len(self.GEOLOGY_COLORS)
        color = self.GEOLOGY_COLORS[index]
        self.active_units[name] = color
        return color

    def create_memory_layer(
        self,
        geometry_type: str,
        name: str,
        fields: str | None = None,
    ) -> tuple[QgsVectorLayer | None, Any]:
        """Create a memory layer with an unknown CRS.

        Args:
            geometry_type: "Point", "LineString", "Polygon"
            name: Layer display name
            fields: Optional field definition string (e.g., "field=id:integer")

        Returns:
            Tuple of (QgsVectorLayer, QgsDataProvider) or (None, None) if failed

        """
        uri = geometry_type
        if fields:
            uri += f"?{fields}"

        layer = QgsVectorLayer(uri, name, "memory")

        if not layer.isValid():
            logger.error(f"Failed to create memory layer: {name}")
            return None, None

        # Ensure layer has a valid CRS (Project CRS) to allow rendering
        # independent of On-The-Fly transformation settings
        project_crs = QgsProject.instance().crs()
        if project_crs.isValid():
            layer.setCrs(project_crs)

        return layer, layer.dataProvider()

    def create_topo_layer(
        self,
        topo_data: ProfileData,
        vert_exag: float = 1.0,
        max_points: int = 1000,
        use_adaptive_sampling: bool = False,
    ) -> QgsVectorLayer | None:
        """Create temporary layer for topographic profile with polychromatic elevation styling."""
        if not topo_data or len(topo_data) < 2:
            return None

        # Apply LOD decimation
        if use_adaptive_sampling:
            render_data = PreviewOptimizer.adaptive_sample(topo_data, max_points=max_points)
        else:
            render_data = PreviewOptimizer.decimate(topo_data, max_points=max_points)

        # Create layer with elevation field for polychromy
        layer, provider = self.create_memory_layer("LineString", "Topography", "field=elev:double")
        if not layer:
            return None

        # Create segments for each pair of points to allow per-segment coloring
        features = []

        for i in range(len(render_data) - 1):
            p1 = render_data[i]
            p2 = render_data[i + 1]

            line_points = [
                QgsPointXY(p1[0], p1[1] * vert_exag),
                QgsPointXY(p2[0], p2[1] * vert_exag),
            ]
            line_geom = QgsGeometry.fromPolylineXY(line_points)

            feat = QgsFeature(layer.fields())
            feat.setGeometry(line_geom)
            # Use average elevation for the segment color
            avg_elev = (p1[1] + p2[1]) / 2.0
            feat.setAttribute("elev", avg_elev)
            features.append(feat)

        if not features:
            return None

        provider.addFeatures(features)
        self._style_topo_layer(layer)
        layer.updateExtents()
        return layer

    def _style_topo_layer(self, layer: QgsVectorLayer):
        """Apply styles to the topography layer."""
        renderer = QgsGraduatedSymbolRenderer("elev")
        renderer.setSourceSymbol(QgsLineSymbol.createSimple({"width": "0.8", "capstyle": "round"}))

        style = QgsStyle.defaultStyle()
        # Try a terrain-like default ramp
        color_ramp = style.colorRamp("Spectral") or style.colorRamp("RdYlGn")
        if color_ramp:
            renderer.updateColorRamp(color_ramp)

        renderer.setClassificationMethod(QgsClassificationFixedInterval())
        renderer.updateClasses(layer, 8)
        layer.setRenderer(renderer)

    def create_topo_fill_layer(
        self,
        topo_data: ProfileData,
        vert_exag: float = 1.0,
        max_points: int = 1000,
        base_elevation: float | None = None,
    ) -> QgsVectorLayer | None:
        """Create a solid 'curtain' fill layer under the topography for depth."""
        if not topo_data or len(topo_data) < 2:
            return None

        render_data = PreviewOptimizer.decimate(topo_data, max_points=max_points)

        layer, provider = self.create_memory_layer("Polygon", "Topography Fill")
        if not layer:
            return None

        # Calculate base line (bottom of the section)
        elevs = [p[1] for p in topo_data]
        if base_elevation is None:
            base_elevation = min(elevs) - (max(elevs) - min(elevs)) * 0.2

        base_y = base_elevation * vert_exag

        # Construct polygon points
        poly_points = []
        # Top edge (the profile)
        for d, e in render_data:
            poly_points.append(QgsPointXY(d, e * vert_exag))

        # Bottom edge (closing the curtain)
        poly_points.append(QgsPointXY(render_data[-1][0], base_y))
        poly_points.append(QgsPointXY(render_data[0][0], base_y))
        poly_points.append(QgsPointXY(render_data[0][0], render_data[0][1] * vert_exag))

        geom = QgsGeometry.fromPolygonXY([poly_points])
        feat = QgsFeature()
        feat.setGeometry(geom)
        provider.addFeatures([feat])

        self._style_topo_fill_layer(layer)
        layer.updateExtents()
        return layer

    def _style_topo_fill_layer(self, layer: QgsVectorLayer):
        """Apply styles to the topography fill layer."""
        symbol = QgsFillSymbol.createSimple(
            {
                "color": "101,67,33,30",  # Earthy brown, very transparent
                "outline_color": "0,0,0,0",
                "outline_width": "0",
            }
        )
        layer.setRenderer(QgsSingleSymbolRenderer(symbol))

    def create_geol_layer(
        self, geol_data: GeologyData, vert_exag: float = 1.0, max_points: int = 1000
    ) -> QgsVectorLayer | None:
        """Create temporary layer for geological profile."""
        if not geol_data:
            return None

        layer, provider = self.create_memory_layer("LineString", "Geology", "field=unit:string")
        if not layer:
            return None

        unique_units = {s.unit_name for s in geol_data}
        features = []
        for segment in geol_data:
            if not segment.points or len(segment.points) < 2:
                continue

            render_points = PreviewOptimizer.decimate(segment.points, max_points=max_points)
            line_points = [QgsPointXY(dist, elev * vert_exag) for dist, elev in render_points]
            line_geom = QgsGeometry.fromPolylineXY(line_points)

            feat = QgsFeature(layer.fields())
            feat.setGeometry(line_geom)
            feat.setAttribute("unit", segment.unit_name)
            features.append(feat)

        provider.addFeatures(features)
        self._style_geol_layer(layer, unique_units)
        layer.updateExtents()
        return layer

    def _style_geol_layer(self, layer: QgsVectorLayer, unique_units: set[str]):
        """Apply styles to the geology layer."""
        categories = []
        for unit_name in unique_units:
            color = self.get_color_for_unit(unit_name)
            symbol = QgsLineSymbol.createSimple(
                {
                    "color": f"{color.red()},{color.green()},{color.blue()}",
                    "width": "0.7",
                    "capstyle": "round",
                    "joinstyle": "round",
                }
            )
            categories.append(QgsRendererCategory(unit_name, symbol, unit_name))

        layer.setRenderer(QgsCategorizedSymbolRenderer("unit", categories))

    def create_struct_layer(
        self,
        struct_data: StructureData,
        reference_data: ProfileData,
        vert_exag: float = 1.0,
        dip_line_length: float | None = None,
    ) -> QgsVectorLayer | None:
        """Create temporary layer for structural dips."""
        if not struct_data:
            return None

        layer, provider = self.create_memory_layer("LineString", "Structures")
        if not layer:
            return None

        if dip_line_length is not None and dip_line_length > 0:
            line_length = dip_line_length
        else:
            if reference_data:
                elevs = [e for _, e in reference_data]
                e_range = max(elevs) - min(elevs)
            else:
                e_range = 100
            line_length = e_range * 0.1

        features = []
        for m in struct_data:
            elev = m.elevation
            dist = m.distance
            app_dip = m.apparent_dip

            rad_dip = math.radians(abs(app_dip))
            dx = line_length * math.cos(rad_dip)
            dy = line_length * math.sin(rad_dip)

            if app_dip < 0:
                dx = -dx

            p1 = QgsPointXY(dist, elev * vert_exag)
            p2 = QgsPointXY(dist + dx, (elev - dy) * vert_exag)

            line_geom = QgsGeometry.fromPolylineXY([p1, p2])
            feat = QgsFeature()
            feat.setGeometry(line_geom)
            features.append(feat)

        provider.addFeatures(features)

        self._style_struct_layer(layer)
        layer.updateExtents()
        return layer

    def _style_struct_layer(self, layer: QgsVectorLayer):
        """Apply styles to the structure layer."""
        symbol = QgsLineSymbol.createSimple(
            {"color": "204,0,0", "width": "0.5", "capstyle": "round"}
        )
        layer.setRenderer(QgsSingleSymbolRenderer(symbol))

    def create_drillhole_trace_layer(
        self, drillhole_data: list, vert_exag: float = 1.0
    ) -> QgsVectorLayer | None:
        """Create temporary layer for drillhole traces."""
        logger.debug(
            f"create_drillhole_trace_layer called with {len(drillhole_data) if drillhole_data else 0} holes"
        )
        if not drillhole_data:
            logger.warning("No drillhole data provided for trace layer")
            return None

        layer, provider = self.create_memory_layer(
            "LineString", "Drillhole Traces", "field=hole_id:string"
        )
        if not layer:
            return None

        features = []
        for hole_data in drillhole_data:
            # hole_data is (hole_id, trace_2d, trace_3d, trace_3d_proj, segments)
            if len(hole_data) >= 5:
                hole_id, trace_points = hole_data[0], hole_data[1]
            else:
                hole_id, trace_points = hole_data[0], hole_data[1]

            if not trace_points or len(trace_points) < 2:
                logger.debug(
                    f"Skipping hole {hole_id}: insufficient trace points ({len(trace_points) if trace_points else 0})"
                )
                continue

            render_points = [QgsPointXY(x, y * vert_exag) for x, y in trace_points]
            line_geom = QgsGeometry.fromPolylineXY(render_points)

            feat = QgsFeature(layer.fields())
            feat.setGeometry(line_geom)
            feat.setAttribute("hole_id", hole_id)
            features.append(feat)

        logger.info(f"Adding {len(features)} drillhole trace features to layer")

        provider.addFeatures(features)
        self._style_drillhole_trace_layer(layer)
        layer.updateExtents()
        return layer

    def _style_drillhole_trace_layer(self, layer: QgsVectorLayer):
        """Apply styles to the drillhole trace layer."""
        symbol = QgsLineSymbol.createSimple(
            {"color": "50,50,50", "width": "0.3", "capstyle": "round"}
        )
        layer.setRenderer(QgsSingleSymbolRenderer(symbol))

        settings = QgsPalLayerSettings()
        settings.fieldName = "hole_id"
        settings.placement = QgsPalLayerSettings.Placement.Line

        txt_format = QgsTextFormat()
        txt_format.setColor(QColor(0, 0, 0))
        txt_format.setSize(8)
        settings.setFormat(txt_format)

        layer.setLabeling(QgsVectorLayerSimpleLabeling(settings))
        layer.setLabelsEnabled(True)

    def create_drillhole_interval_layer(
        self, drillhole_data: list, vert_exag: float = 1.0
    ) -> QgsVectorLayer | None:
        """Create temporary layer for drillhole intervals."""
        if not drillhole_data:
            return None

        all_segments = []
        for hole_data in drillhole_data:
            # segments are at index 2 (legacy) or 4 (v2.7.0)
            segments = hole_data[4] if len(hole_data) >= 5 else hole_data[2]
            if segments:
                all_segments.extend(segments)

        if not all_segments:
            return None

        layer, provider = self.create_memory_layer(
            "LineString", "Drillhole Intervals", "field=unit:string"
        )
        if not layer:
            return None

        features = []
        unique_units = set()
        for segment in all_segments:
            if not segment.points or len(segment.points) < 2:
                continue

            unique_units.add(segment.unit_name)
            render_points = [QgsPointXY(x, y * vert_exag) for x, y in segment.points]
            line_geom = QgsGeometry.fromPolylineXY(render_points)

            feat = QgsFeature(layer.fields())
            feat.setGeometry(line_geom)
            feat.setAttribute("unit", segment.unit_name)
            features.append(feat)

        provider.addFeatures(features)
        self._style_drillhole_interval_layer(layer, unique_units)
        layer.updateExtents()
        return layer

    def _style_drillhole_interval_layer(self, layer: QgsVectorLayer, unique_units: set[str]):
        """Apply styles to the drillhole interval layer."""
        categories = []
        for unit_name in unique_units:
            color = self.get_color_for_unit(unit_name)
            symbol = QgsLineSymbol.createSimple(
                {
                    "color": f"{color.red()},{color.green()},{color.blue()}",
                    "width": "2.0",
                    "capstyle": "flat",
                    "joinstyle": "bevel",
                }
            )
            categories.append(QgsRendererCategory(unit_name, symbol, unit_name))

        layer.setRenderer(QgsCategorizedSymbolRenderer("unit", categories))

    def interpolate_elevation(self, reference_data: ProfileData, target_dist: float) -> float:
        """Interpolate elevation at a given distance."""
        if not reference_data:
            return 0
        for i in range(len(reference_data) - 1):
            d1, e1 = reference_data[i]
            d2, e2 = reference_data[i + 1]
            if d1 <= target_dist <= d2:
                if d2 == d1:
                    return e1
                t = (target_dist - d1) / (d2 - d1)
                return e1 + t * (e2 - e1)
        if target_dist < reference_data[0][0]:
            return reference_data[0][1]
        return reference_data[-1][1]
