"""Interpretation tool for Profile View.

This module provides the ProfileInterpretationTool for drawing
interpretation polygons in the profile preview window.
"""

from __future__ import annotations

import contextlib
import datetime
import random
import uuid
from typing import Any

from qgis.core import (
    QgsMapLayer,
    QgsPointLocator,
    QgsPointXY,
    QgsProject,
    QgsVectorLayer,
    QgsWkbTypes,
)
from qgis.gui import (
    QgsMapCanvas,
    QgsMapToolEmitPoint,
    QgsRubberBand,
    QgsVertexMarker,
)
from qgis.PyQt.QtCore import QCoreApplication, QPoint, Qt, pyqtSignal
from qgis.PyQt.QtGui import QColor

from sec_interp.core.domain import InterpretationPolygon
from sec_interp.logger_config import get_logger, log_critical_operation

logger = get_logger(__name__)


class ProfileSnapper:
    """Helper class to handle point snapping functionality.

    Duplicates logic from ProfileMeasureTool to avoid tight coupling.
    """

    def __init__(self, canvas: QgsMapCanvas) -> None:
        """Initialize the profile snapper.

        Args:
            canvas: The map canvas to snap on.

        """
        self.canvas = canvas
        self._locators: dict[str, QgsPointLocator] = {}

    def snap(self, mouse_pos: QPoint) -> QgsPointXY:
        """Find the nearest vertex or edge to the mouse position."""
        point = self.canvas.getCoordinateTransform().toMapCoordinates(mouse_pos)

        # Search tolerance in map units (approx 12 pixels)
        tolerance = (self.canvas.mapUnitsPerPixel() or 1.0) * 12

        best_match = None
        best_dist = float("inf")

        layers = self.canvas.layers()
        self._cleanup_locators({layer.id() for layer in layers if layer is not None})

        crs = self.canvas.mapSettings().destinationCrs()
        context = QgsProject.instance().transformContext()

        for layer in layers:
            if not self._is_snappable(layer):
                continue

            try:
                locator = self._get_locator(layer, crs, context)
                if locator:
                    best_match, best_dist = self._find_best_match_in_locator(
                        locator, point, tolerance, best_match, best_dist
                    )
            except Exception:  # nosec B112
                # If layer was deleted or something went wrong with locator
                continue

        if best_match:
            return best_match.point()

        return point

    def _find_best_match_in_locator(
        self,
        locator: QgsPointLocator,
        point: QgsPointXY,
        tolerance: float,
        current_best: Any,
        current_dist: float,
    ) -> tuple[Any, float]:
        """Find the best match (vertex or edge) in a given locator."""
        best_match = current_best
        best_dist = current_dist

        v_match = locator.nearestVertex(point, tolerance)
        if v_match.isValid() and v_match.distance() < best_dist:
            best_match = v_match
            best_dist = v_match.distance()

        e_match = locator.nearestEdge(point, tolerance)
        if e_match.isValid() and e_match.distance() < best_dist:
            best_match = e_match
            best_dist = e_match.distance()

        return best_match, best_dist

    def _cleanup_locators(self, current_ids: set[str]) -> None:
        """Remove locators for layers that are no longer active."""
        hits_to_remove = [lid for lid in self._locators if lid not in current_ids]
        for lid in hits_to_remove:
            del self._locators[lid]

    def _is_snappable(self, layer: QgsMapLayer) -> bool:
        """Check if a layer is valid for snapping."""
        return bool(layer and layer.type() == QgsMapLayer.VectorLayer)

    def _get_locator(
        self, layer: QgsVectorLayer, crs, context
    ) -> QgsPointLocator | None:
        """Retrieve or create a locator for a layer."""
        if layer.id() not in self._locators:
            try:
                self._locators[layer.id()] = QgsPointLocator(layer, crs, context)
            except Exception as e:
                logger.warning(
                    f"Failed to create locator for layer {layer.name()}: {e}"
                )
                return None
        return self._locators[layer.id()]


class ProfileInterpretationTool(QgsMapToolEmitPoint):
    """Tool for drawing interpretation polygons on the profile canvas.

    - Left Click: Add vertex
    - Move: Update preview rubber band
    - Right Click: Remove last vertex
    - Enter/Double Click: Finalize polygon
    - Escape: Cancel
    """

    polygonFinished = pyqtSignal(InterpretationPolygon)

    def __init__(self, canvas: QgsMapCanvas) -> None:
        """Initialize the profile interpretation tool.

        Args:
            canvas: The map canvas where interpretation is drawn.

        """
        super().__init__(canvas)
        self.canvas = canvas
        self.points: list[QgsPointXY] = []
        self.rubber_band: QgsRubberBand | None = None
        self.vertex_markers: list[QgsVertexMarker] = []
        self.snapper = ProfileSnapper(canvas)
        self.cursor = Qt.CrossCursor

    def activate(self) -> None:
        """Activate the interpretation tool."""
        log_critical_operation(logger, "activate_interpretation_tool")
        logger.debug("ProfileInterpretationTool.activate() called")
        super().activate()
        self.canvas.setCursor(self.cursor)
        logger.debug("ProfileInterpretationTool activated successfully")

    def deactivate(self) -> None:
        """Deactivate the interpretation tool."""
        log_critical_operation(logger, "deactivate_interpretation_tool")
        logger.debug("ProfileInterpretationTool.deactivate() called")
        self.reset()
        super().deactivate()
        logger.debug("ProfileInterpretationTool deactivated successfully")

    def disconnect_signals(self) -> None:
        """Disconnect signals to prevent memory leaks."""
        with contextlib.suppress(TypeError):
            self.polygonFinished.disconnect()

    def reset(self) -> None:
        """Reset the tool state safely."""
        log_critical_operation(
            logger,
            "reset_interpretation_tool",
            points=len(self.points) if self.points else 0,
        )
        logger.debug(
            f"ProfileInterpretationTool.reset() called - {len(self.points)} points, {len(self.vertex_markers)} markers"
        )
        self.points = []

        # Rubber band cleanup
        if self.rubber_band:
            with contextlib.suppress(Exception):
                self.rubber_band.reset(QgsWkbTypes.PolygonGeometry)
                self.canvas.scene().removeItem(self.rubber_band)
            self.rubber_band = None

        # Markers cleanup
        for marker in self.vertex_markers:
            with contextlib.suppress(Exception):
                self.canvas.scene().removeItem(marker)
        self.vertex_markers = []

        self.is_drawing = False
        if self.canvas:
            logger.debug("ProfileInterpretationTool.reset() - refreshing canvas")
            self.canvas.refresh()
        logger.debug("ProfileInterpretationTool.reset() completed")

    def canvasReleaseEvent(self, event: Any) -> None:
        """Handle mouse click release.

        Args:
            event: Map tool event from QGIS

        """
        if event.button() == Qt.RightButton:
            if self.points:
                self._remove_last_point()
            return

        snapped_point = self.snapper.snap(event.pos())
        self._add_point(snapped_point)

    def canvasMoveEvent(self, event: Any) -> None:
        """Handle mouse move for rubber band update.

        Args:
            event: Map tool event from QGIS

        """
        if not self.points:
            return
        current_point = self.snapper.snap(event.pos())
        self._update_rubber_band(current_point)

    def canvasDoubleClickEvent(self, event: Any) -> None:
        """Finalize polygon on double click.

        Args:
            event: Map tool event from QGIS

        """
        MIN_POLYGON_POINTS = 3
        if len(self.points) >= MIN_POLYGON_POINTS:
            self.finalize_polygon()

    def keyPressEvent(self, event: Any) -> None:
        """Handle keyboard events.

        Args:
            event: Key event from QGIS

        """
        if event.key() in (Qt.Key_Return, Qt.Key_Enter):
            MIN_POLYGON_POINTS = 3
            if len(self.points) >= MIN_POLYGON_POINTS:
                self.finalize_polygon()
                event.accept()
            return
        if event.key() == Qt.Key_Escape:
            self.reset()
            event.accept()
            return
        super().keyPressEvent(event)

    def _add_point(self, point: QgsPointXY) -> None:
        """Add a vertex to the current polygon."""
        # Prevent adding the exact same point twice in a row (e.g. slow click)
        if self.points and self.points[-1].compare(point, 1e-6):
            return

        self.points.append(point)
        self._ensure_rubber_band()
        self.rubber_band.addPoint(point, True)
        self._add_vertex_marker(point)

    def _remove_last_point(self) -> None:
        """Remove the last added vertex."""
        if not self.points:
            return
        self.points.pop()
        if self.vertex_markers:
            marker = self.vertex_markers.pop()
            self.canvas.scene().removeItem(marker)

        if not self.points:
            if self.rubber_band:
                self.canvas.scene().removeItem(self.rubber_band)
                self.rubber_band = None
        else:
            self.rubber_band.reset(QgsWkbTypes.PolygonGeometry)
            for p in self.points:
                self.rubber_band.addPoint(p, False)

    def _add_vertex_marker(self, point: QgsPointXY) -> None:
        """Add a visual marker for a vertex."""
        marker = QgsVertexMarker(self.canvas)
        marker.setCenter(point)
        marker.setColor(QColor(255, 165, 0))  # Orange
        marker.setIconSize(10)
        marker.setIconType(QgsVertexMarker.ICON_X)
        marker.setPenWidth(2)
        self.vertex_markers.append(marker)

    def _ensure_rubber_band(self) -> None:
        """Ensure the rubber band exists."""
        if self.rubber_band:
            return
        self.rubber_band = QgsRubberBand(self.canvas, QgsWkbTypes.PolygonGeometry)
        color = QColor(255, 0, 0, 100)  # Semi-transparent red
        self.rubber_band.setColor(color)
        self.rubber_band.setFillColor(color)
        self.rubber_band.setWidth(2)

    def _update_rubber_band(self, current_point: QgsPointXY) -> None:
        """Update rubber band geometry."""
        if not self.rubber_band or not self.points:
            return
        self.rubber_band.reset(QgsWkbTypes.PolygonGeometry)
        for p in self.points:
            self.rubber_band.addPoint(p, False)
        self.rubber_band.addPoint(current_point, True)

    def finalize_polygon(self) -> None:
        """Finalize the polygon and emit signal."""
        log_critical_operation(logger, "finalize_polygon", points=len(self.points))
        logger.debug(
            f"ProfileInterpretationTool.finalize_polygon() called with {len(self.points)} points"
        )
        MIN_POLYGON_POINTS = 3
        if len(self.points) < MIN_POLYGON_POINTS:
            logger.warning("finalize_polygon() aborted - less than 3 points")
            return

        # Capture the points as (dist, elev) which are (x, y) in profile units
        vertices_2d = [(p.x(), p.y()) for p in self.points]

        # Generate a random vivid color for the new interpretation
        # Random hue (0-359), high saturation (200-255), medium-lightness (100-200)
        hue = random.randint(0, 359)  # nosec B311
        sat = random.randint(200, 255)  # nosec B311
        val = random.randint(150, 255)  # nosec B311
        # Use simple hex format if QColor is not easily serializable, but QColor.name() works
        rand_color = QColor.fromHsv(hue, sat, val)
        color_hex = rand_color.name()  # e.g. #RRGGBB

        interp = InterpretationPolygon(
            id=str(uuid.uuid4()),
            name=QCoreApplication.translate(
                "ProfileInterpretationTool", "New Interpretation"
            ),
            type="lithology",
            vertices_2d=vertices_2d,
            attributes={},
            color=color_hex,
            created_at=datetime.datetime.now().isoformat(),
        )

        logger.debug(
            f"finalize_polygon() - emitting polygonFinished signal for {interp.id}"
        )
        self.polygonFinished.emit(interp)
        # Note: Do NOT call reset() here.
        # The dialog handler should deactivate the tool, which calls reset() cleanly.
        logger.info(
            f"Interpretation polygon finalized with {len(vertices_2d)} vertices, ID: {interp.id}"
        )
        logger.debug("finalize_polygon() completed")
