from __future__ import annotations

"""Structure Data Processing Service.

This module handles the calculation of apparent dips and projection
of structural measurements (planes, lines) onto the section plane.
"""

# /***************************************************************************
#  SecInterp - StructureService
#                                  A QGIS plugin
#  Service for projecting structural measurements.
#                               -------------------
#         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 collections.abc import Iterator

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

from sec_interp.core import utils as scu
from sec_interp.core.domain import StructureData, StructureMeasurement
from sec_interp.core.exceptions import ProcessingError
from sec_interp.core.interfaces.structure_interface import IStructureService
from sec_interp.logger_config import get_logger

logger = get_logger(__name__)


class StructureService(IStructureService):
    """Service for projecting structural measurements onto cross-sections.

    This service handles the filtering and projection of structural measurements
    (dip/strike) onto a cross-section plane to calculate apparent dip.
    """

    def detach_structures(
        self,
        struct_lyr: QgsVectorLayer,
        line_geom: QgsGeometry,
        buffer_m: float,
    ) -> list[dict[str, Any]]:
        """Extract structural features within buffer into detached dictionaries."""
        buffer_geom = self._create_buffer_zone(line_geom, struct_lyr.crs(), buffer_m)
        filtered_features = self._filter_structures(struct_lyr, buffer_geom, struct_lyr.crs())

        detached_data = []
        for feat in filtered_features:
            detached_data.append(
                {
                    "wkt": feat.geometry().asWkt() if feat.hasGeometry() else None,
                    "attributes": scu.extract_feature_attributes(feat),
                }
            )
        return detached_data

    def project_structures(
        self,
        line_geom: QgsGeometry,
        line_start: QgsPointXY,
        da: QgsDistanceArea,
        raster_lyr: QgsRasterLayer,
        struct_data: list[dict[str, Any]],
        buffer_m: float,
        line_az: float,
        dip_field: str,
        strike_field: str,
        band_number: int = 1,
    ) -> StructureData:
        """Project detached structural measurements onto the section plane."""
        projected_structs = []

        for item in struct_data:
            measurement = self._process_single_structure(
                item,
                line_geom,
                line_start,
                da,
                raster_lyr,
                band_number,
                line_az,
                dip_field,
                strike_field,
            )
            if measurement:
                projected_structs.append(measurement)

        # Sort by distance
        projected_structs.sort(key=lambda x: x.distance)

        logger.info(f"Processed {len(projected_structs)} structural measurements")
        return projected_structs

    def _create_buffer_zone(
        self,
        line_geom: QgsGeometry,
        crs: QgsCoordinateReferenceSystem,
        buffer_m: float,
    ) -> QgsGeometry:
        """Create a buffer geometry around the section line.

        Args:
            line_geom: The section line geometry.
            crs: CRS of the line layer.
            buffer_m: Buffer distance in meters.

        Returns:
            The buffered area as a QgsGeometry.

        Raises:
            ProcessingError: If buffer creation fails.

        """
        try:
            return scu.create_buffer_geometry(line_geom, crs, buffer_m, segments=25)
        except (ValueError, RuntimeError) as e:
            logger.exception("Buffer creation failed")
            raise ProcessingError(
                "Cannot create buffer zone", {"buffer_m": buffer_m, "crs": crs.authid()}
            ) from e

    def _filter_structures(
        self,
        struct_lyr: QgsVectorLayer,
        buffer_geom: QgsGeometry,
        target_crs: QgsCoordinateReferenceSystem,
    ) -> Iterator[QgsFeature]:
        """Select structure features within the buffer using spatial indexing.

        Args:
            struct_lyr: The layer containing structural measurements.
            buffer_geom: The buffer area geometry.
            target_crs: CRS of the project/section line.

        Returns:
            An iterator over the filtered QgsFeature objects.

        Raises:
            ProcessingError: If spatial filtering fails.

        """
        try:
            return scu.filter_features_by_buffer(struct_lyr, buffer_geom, target_crs)
        except (ValueError, RuntimeError) as e:
            logger.exception("Spatial filtering failed")
            raise ProcessingError(
                "Cannot filter structures by buffer", {"layer": struct_lyr.name()}
            ) from e

    def _process_single_structure(
        self,
        data: dict[str, Any],
        line_geom: QgsGeometry,
        line_start: QgsPointXY,
        da: QgsDistanceArea,
        raster_lyr: QgsRasterLayer,
        band_number: int,
        line_az: float,
        dip_field: str,
        strike_field: str,
    ) -> StructureMeasurement | None:
        """Process a single structure from detached data."""
        wkt = data.get("wkt")
        if not wkt:
            return None

        struct_geom = QgsGeometry.fromWkt(wkt)
        if struct_geom.isNull():
            return None

        # Project point onto line to get true station distance
        proj_dist = line_geom.lineLocatePoint(struct_geom)
        if proj_dist < 0:
            return None

        # Interpolate point on line at that distance
        proj_pt = line_geom.interpolate(proj_dist).asPoint()

        # Measure geodesic distance from start
        dist = da.measureLine(line_start, proj_pt)

        elev = scu.sample_point_elevation(raster_lyr, proj_pt, band_number)

        parsed_data = self._parse_structural_data(
            data["attributes"], strike_field, dip_field, line_az
        )
        if not parsed_data:
            return None

        strike, dip_angle, app_dip = parsed_data

        # Create object
        return StructureMeasurement(
            distance=round(dist, 1),
            elevation=round(elev, 1),
            apparent_dip=round(app_dip, 1),
            original_dip=dip_angle,
            original_strike=strike,
            attributes=data["attributes"],
        )

    def _parse_structural_data(
        self,
        attributes: dict[str, Any],
        strike_field: str,
        dip_field: str,
        line_az: float,
    ) -> tuple[float, float, float] | None:
        """Parse strike and dip attributes and calculate apparent dip."""
        try:
            strike_raw = attributes.get(strike_field)
            dip_raw = attributes.get(dip_field)
        except (AttributeError, KeyError):
            return None

        strike = scu.parse_strike(strike_raw)
        dip_angle, _ = scu.parse_dip(dip_raw)

        if strike is None or dip_angle is None:
            return None

        # Validate ranges
        if not (0 <= strike <= 360) or not (0 <= dip_angle <= 90):
            return None

        app_dip = scu.calculate_apparent_dip(strike, dip_angle, line_az)
        return strike, dip_angle, app_dip
