from __future__ import annotations

"""Geology Data Processing Service.

This module handles the extraction and projection of geological boundaries
and unit segments from map layers to the cross-section plane.
"""

# /***************************************************************************
#  SecInterp - GeologyService
#                                  A QGIS plugin
#  Service for generating geological profiles.
#                               -------------------
#         begin                : 2025-12-07
#         copyright            : (C) 2025 by Juan M Bernales
#         email                : juanbernales@gmail.com
#  ***************************************************************************/
#
# /***************************************************************************
#  *                                                                         *
#  *   This program is free software; you can redistribute it and/or modify  *
#  *   it under the terms of the GNU General Public License as published by  *
#  *   the Free Software Foundation; either version 2 of the License, or     *
#  *   (at your option) any later version.                                   *
#  *                                                                         *
#  ***************************************************************************/

from typing import Any

from qgis.core import (
    QgsCoordinateReferenceSystem,
    QgsDistanceArea,
    QgsFeatureRequest,
    QgsGeometry,
    QgsPointXY,
    QgsRasterLayer,
    QgsVectorLayer,
)

from sec_interp.core import utils as scu
from sec_interp.core.domain import GeologyData, GeologySegment, GeologyTaskInput
from sec_interp.core.exceptions import DataMissingError, GeometryError
from sec_interp.core.interfaces.geology_interface import IGeologyService
from sec_interp.core.performance_metrics import performance_monitor
from sec_interp.core.utils.geometry_utils.extraction import extract_lines_from_geometry
from sec_interp.core.utils.geometry_utils.processing import (
    calculate_segment_range,
    interpolate_segment_points,
)
from sec_interp.logger_config import get_logger

logger = get_logger(__name__)


class GeologyService(IGeologyService):
    """Service for generating geological profiles.

    This service handles the extraction of geological unit intersections
    along a cross-section line.
    """

    @performance_monitor
    def generate_geological_profile(
        self,
        line_lyr: QgsVectorLayer,
        raster_lyr: QgsRasterLayer,
        outcrop_lyr: QgsVectorLayer,
        outcrop_name_field: str,
        band_number: int = 1,
    ) -> GeologyData:
        """Generate geological profile data by intersecting the section line with outcrop polygons.

        Extracts geological unit intersections along the cross-section line,
        calculates elevations from the DEM, and returns a list of segments.

        Args:
            line_lyr: The QGIS vector layer representing the cross-section line.
            raster_lyr: The Digital Elevation Model (DEM) raster layer.
            outcrop_lyr: The QGIS vector layer containing geological outcrop polygons.
            outcrop_name_field: The attribute field name for geological unit names.
            band_number: The raster band to use for elevation sampling (default 1).

        Returns:
            GeologyData: A list of `GeologySegment` objects, sorted by distance along the section.

        Raises:
            DataMissingError: If the line layer has no features.
            GeometryError: If the line geometry is invalid.
            ProcessingError: If the intersection processing fails.

        """
        task_input = self.prepare_task_input(
            line_lyr, raster_lyr, outcrop_lyr, outcrop_name_field, band_number
        )
        return self.process_task_data(task_input)

    def prepare_task_input(
        self,
        line_lyr: QgsVectorLayer,
        raster_lyr: QgsRasterLayer,
        outcrop_lyr: QgsVectorLayer,
        outcrop_name_field: str,
        band_number: int = 1,
    ) -> GeologyTaskInput:
        """Prepare detached domain data for background task."""
        self._validate_inputs(line_lyr, raster_lyr, outcrop_lyr, outcrop_name_field, band_number)

        line_geom, line_start = self._extract_line_info(line_lyr)
        crs = line_lyr.crs()
        da = scu.create_distance_area(crs)

        # 1. Generate Master Profile Data (needs Raster access)
        master_profile_data, master_grid_dists_raw = self._generate_master_profile_data(
            line_geom, raster_lyr, band_number, da, line_start
        )

        # Convert master_grid_dists to domain types (dist, (x, y), elev)
        master_grid_dists = [(d, (pt.x(), pt.y()), e) for d, pt, e in master_grid_dists_raw]

        # 2. Extract Outcrop Data (needs Vector access)
        # This already returns detached dicts with WKT
        outcrop_data = self._extract_outcrop_data(line_geom, outcrop_lyr, outcrop_name_field)

        return GeologyTaskInput(
            line_geometry_wkt=line_geom.asWkt(),
            line_start_x=line_start.x(),
            line_start_y=line_start.y(),
            crs_authid=crs.authid(),
            master_profile_data=master_profile_data,
            master_grid_dists=master_grid_dists,
            outcrop_data=outcrop_data,
            outcrop_name_field=outcrop_name_field,
        )

    def _validate_inputs(
        self,
        line_lyr: QgsVectorLayer,
        raster_lyr: QgsRasterLayer,
        outcrop_lyr: QgsVectorLayer,
        outcrop_name_field: str,
        band_number: int,
    ) -> None:
        """Validate input layers and parameters."""
        from sec_interp.core.exceptions import ValidationError

        # 1. Layer Validity
        for lyr, name in [
            (line_lyr, "Line layer"),
            (raster_lyr, "Raster layer"),
            (outcrop_lyr, "Outcrop layer"),
        ]:
            if not lyr or not lyr.isValid():
                raise DataMissingError(
                    f"Invalid layer: {name}. Please check input layers.",
                    {"layer": name},
                )

        # 2. Band Validation
        if band_number < 1:
            raise ValidationError("Band number must be positive.")

        if band_number > raster_lyr.bandCount():
            raise ValidationError(
                f"Band number {band_number} exceeds raster band count ({raster_lyr.bandCount()})."
            )

        # 3. Field Validation
        idx = outcrop_lyr.fields().indexFromName(outcrop_name_field)
        if idx == -1:
            raise ValidationError(f"Field '{outcrop_name_field}' not found in outcrop layer.")

    def _extract_outcrop_data(
        self,
        line_geom: QgsGeometry,
        outcrop_lyr: QgsVectorLayer,
        outcrop_name_field: str,
    ) -> list[dict[str, Any]]:
        """Extract outcrop features intersecting the line bounding box.

        Returns a list of dictionaries with WKT geometry and attributes,
        completely detached from QGIS objects.
        """
        outcrop_data = []
        line_bbox = line_geom.boundingBox()
        request = QgsFeatureRequest().setFilterRect(line_bbox)

        for feature in outcrop_lyr.getFeatures(request):
            if not feature.hasGeometry():
                continue

            # Copy attributes and geometry to detached structures
            attrs = dict(zip(feature.fields().names(), feature.attributes(), strict=False))
            try:
                # Ensure we handle potential attribute read errors safely
                unit_name = str(feature[outcrop_name_field])
            except KeyError:
                unit_name = "Unknown"

            outcrop_data.append(
                {
                    "wkt": feature.geometry().asWkt(),  # Store as WKT immediately
                    "attrs": attrs,
                    "unit_name": unit_name,
                }
            )
        return outcrop_data

    def process_task_data(
        self, task_input: GeologyTaskInput, feedback: Any | None = None
    ) -> GeologyData:
        """Process geological data in a thread-safe way (Domain-Pure logic)."""
        # Reconstruct necessary tools from IDs/Strings
        crs = QgsCoordinateReferenceSystem(task_input.crs_authid)
        da = scu.create_distance_area(crs)
        line_geom = QgsGeometry.fromWkt(task_input.line_geometry_wkt)
        line_start = QgsPointXY(task_input.line_start_x, task_input.line_start_y)

        segments = []
        total = len(task_input.outcrop_data)

        for i, item in enumerate(task_input.outcrop_data):
            if feedback and feedback.isCanceled():
                return []

            new_segments = self._process_single_outcrop(item, line_geom, line_start, da, task_input)
            segments.extend(new_segments)

            if feedback:
                feedback.setProgress((i / total) * 100)

        # Sort by start distance
        segments.sort(key=lambda x: x.points[0][0] if x.points else 0)
        return segments

    def _process_detached_intersection(
        self,
        geom: QgsGeometry,
        attrs: dict[str, Any],
        unit_name: str,
        line_start: QgsPointXY,
        da: QgsDistanceArea,
        master_grid_dists: list[tuple[float, QgsPointXY, float]],
        master_profile_data: list[tuple[float, float]],
        tolerance: float,
    ) -> list[GeologySegment]:
        """Process a detached intersection geometry to extract geology segments."""
        if not geom or geom.isNull():
            return []

        geometries = extract_lines_from_geometry(geom)
        if not geometries:
            return []

        segments = []
        for seg_geom in geometries:
            segment = self._create_segment_from_geometry(
                seg_geom,
                attrs,
                unit_name,
                line_start,
                da,
                master_grid_dists,
                master_profile_data,
                tolerance,
            )
            if segment:
                segments.append(segment)

        return segments

    def _process_single_outcrop(
        self,
        item: dict[str, Any],
        line_geom: QgsGeometry,
        line_start: QgsPointXY,
        da: QgsDistanceArea,
        task_input: GeologyTaskInput,
    ) -> list[GeologySegment]:
        """Process a single outcrop feature intersection."""
        outcrop_geom = QgsGeometry.fromWkt(item["wkt"])
        intersection = line_geom.intersection(outcrop_geom)

        if intersection.isEmpty():
            return []

        return self._process_detached_intersection(
            intersection,
            item["attrs"],
            item["unit_name"],
            line_start,
            da,
            task_input.master_grid_dists,
            task_input.master_profile_data,
            task_input.tolerance,
        )

    def _create_segment_from_geometry(
        self,
        seg_geom: QgsGeometry,
        attributes: dict[str, Any],
        unit_name: str,
        line_start: QgsPointXY,
        da: QgsDistanceArea,
        master_grid_dists: list[tuple[float, QgsPointXY, float]],
        master_profile_data: list[tuple[float, float]],
        tolerance: float,
    ) -> GeologySegment | None:
        """Create a GeologySegment from a geometry part by sampling elevations."""
        rng = calculate_segment_range(seg_geom, line_start, da)
        if not rng:
            return None

        dist_start, dist_end = rng
        segment_points = interpolate_segment_points(
            dist_start, dist_end, master_grid_dists, master_profile_data, tolerance
        )

        return GeologySegment(
            unit_name=unit_name,
            geometry_wkt=(seg_geom.asWkt() if seg_geom and not seg_geom.isNull() else None),
            attributes=attributes,
            points=[(float(d), float(e)) for d, e in segment_points],
        )

    def _generate_master_profile_data(
        self,
        line_geom: QgsGeometry,
        raster_lyr: QgsRasterLayer,
        band_number: int,
        da: QgsDistanceArea,
        line_start: QgsPointXY,
    ) -> tuple[list[tuple[float, float]], list[tuple[float, QgsPointXY, float]]]:
        """Generate the master profile data (grid points and elevations)."""
        # 1. Generate densified grid points
        grid_points = self._get_densified_grid_points(line_geom, raster_lyr)

        # 2. Sample elevations at grid points
        return self._sample_elevation_at_points(
            grid_points, raster_lyr, band_number, da, line_start
        )

    def _get_densified_grid_points(
        self, line_geom: QgsGeometry, raster_lyr: QgsRasterLayer
    ) -> list[QgsPointXY]:
        """Densify line and extract vertices."""
        try:
            interval = raster_lyr.rasterUnitsPerPixelX()
            master_densified = scu.densify_line_by_interval(line_geom, interval)
            return scu.get_line_vertices(master_densified)
        except (AttributeError, ValueError, TypeError) as e:
            logger.warning(f"Failed to densify line, using original vertices: {e}")
            return scu.get_line_vertices(line_geom)

    def _sample_elevation_at_points(
        self,
        points: list[QgsPointXY],
        raster_lyr: QgsRasterLayer,
        band_number: int,
        da: QgsDistanceArea,
        line_start: QgsPointXY,
    ) -> tuple[list[tuple[float, float]], list[tuple[float, QgsPointXY, float]]]:
        """Sample elevation from raster at provided points."""
        master_profile_data = []
        master_grid_dists = []
        current_dist = 0.0

        for i, pt in enumerate(points):
            if i > 0:
                current_dist += da.measureLine(points[i - 1], pt)

            val, ok = raster_lyr.dataProvider().sample(pt, band_number)
            elev = val if ok else 0.0

            master_profile_data.append((current_dist, elev))
            master_grid_dists.append((current_dist, pt, elev))

        return master_profile_data, master_grid_dists

    def _extract_line_info(self, line_lyr: QgsVectorLayer) -> tuple[QgsGeometry, QgsPointXY]:
        """Extract geometry and start point from the line layer.

        Args:
            line_lyr: The vector layer containing the section line.

        Returns:
            A tuple containing (line_geometry, start_point).

        Raises:
            DataMissingError: If layer has no features.
            GeometryError: If geometry is invalid.

        """
        line_feat = next(line_lyr.getFeatures(), None)
        if not line_feat:
            raise DataMissingError("Line layer has no features", {"layer": line_lyr.name()})

        line_geom = line_feat.geometry()
        if not line_geom or line_geom.isNull():
            raise GeometryError("Line geometry is not valid", {"layer": line_lyr.name()})

        if line_geom.isMultipart():
            line_start = line_geom.asMultiPolyline()[0][0]
        else:
            line_start = line_geom.asPolyline()[0]

        return line_geom, line_start
