"""
Delete Points Outside Polygon Layer Extent Action

Allows the user to choose a polygon layer; any points in the active
point layer that are not within or on the boundary of any polygon in
the chosen polygon layer will be deleted. Records full backups for undo.
"""

from .base_action import BaseAction
from qgis.PyQt.QtWidgets import QInputDialog
from qgis.core import (
    QgsProject,
    QgsVectorLayer,
    QgsWkbTypes,
    QgsFeatureRequest,
    QgsCoordinateTransform,
    QgsGeometry,
    QgsSpatialIndex,
    QgsMessageLog,
    Qgis,
)
from ..i18n import tr


class DeletePointsOutsidePolygonLayerExtentAction(BaseAction):
    """Delete points from a point layer that are not within a polygon layer."""

    def __init__(self):
        super().__init__()
        self.action_id = 'delete_points_outside_polygon_layer_extent'
        self.name = 'Delete Points Outside Polygon Layer Extent'
        self.category = 'Editing'
        self.description = (
            'Choose a polygon layer; delete points from the active point layer '
            'that are not within or on the boundary of any polygon in the chosen layer.'
        )

        self.set_action_scope('layer')
        # Support both layer and feature scope to ensure menu visibility
        self.set_supported_scopes(['layer', 'feature'])
        # Available when right-clicking a point layer (or canvas in point context)
        # Support point clicks and canvas as fallback
        self.set_supported_click_types(['point', 'multipoint', 'canvas'])
        self.set_supported_geometry_types(['point', 'multipoint'])

        # Internal state for undo/history
        self._backups = []
        self._affected_layers = []
        self._deleted_fids_by_layer = {}
        self._chosen_polygon_layer_id = None
        self._chosen_polygon_layer_name = None

    def supports_undo(self) -> bool:
        return True

    def get_undo_category(self) -> str:
        return 'payload'

    def get_undo_payload(self, context: dict, execute_result=None) -> dict:
        if not self._backups:
            return None

        layers = [self.create_layer_descriptor(layer) for layer in self._affected_layers]

        return {
            'undo_type': 'delete_feature',
            'layers': layers,
            'features': self._backups,
            'description': f"Deleted {sum(len(v) for v in self._deleted_fids_by_layer.values())} points outside polygon layer {self._chosen_polygon_layer_name}",
            'meta': {'polygon_layer_id': self._chosen_polygon_layer_id, 'polygon_layer_name': self._chosen_polygon_layer_name}
        }

    def get_settings_schema(self):
        return {
            'confirm_deletion': {
                'type': 'bool',
                'default': True,
                'label': 'Confirm before deleting',
                'description': 'Show a confirmation dialog before performing the deletion.'
            },
            'process_selected_only': {
                'type': 'bool',
                'default': False,
                'label': 'Process selected only',
                'description': 'If True and features are selected in the point layer, only process selected features.'
            }
        }

    def execute(self, context):
        LOG_TAG = 'RightClickActions'

        # Resolve point layer from context. Support both layer scope and detected_features fallback.
        point_layer = context.get('layer')
        if point_layer is None:
            detected = context.get('detected_features', []) or []
            if detected:
                point_layer = detected[0].layer

        if not point_layer or not isinstance(point_layer, QgsVectorLayer):
            self.show_error(tr('Error'), tr('No point layer found in context. Please right-click a point layer.'))
            return

        if int(point_layer.geometryType()) != int(QgsWkbTypes.PointGeometry):
            self.show_error(tr('Error'), tr('This action must be run on a point layer.'))
            return

        # Gather polygon layers in the project
        project = QgsProject.instance()
        polygon_layers = [l for l in project.mapLayers().values()
                          if isinstance(l, QgsVectorLayer) and int(l.geometryType()) == int(QgsWkbTypes.PolygonGeometry) and l.isValid()]

        if not polygon_layers:
            self.show_warning(tr('No polygon layers'), tr('No polygon layers found in the project.'))
            return

        # Build selection list with stable identifiers to avoid name collisions
        display_items = [f"{l.name()} [{l.id()}]" for l in polygon_layers]
        item, ok = QInputDialog.getItem(None, tr('Choose polygon layer'), tr('Polygon layer:'), display_items, 0, False)
        if not ok or not item:
            return  # cancelled

        # Map back to layer
        chosen_id = item.split('[')[-1].rstrip(']')
        polygon_layer = next((l for l in polygon_layers if l.id() == chosen_id), None)
        if not polygon_layer:
            self.show_error(tr('Error'), tr('Selected polygon layer could not be found.'))
            return

        # Remember chosen polygon layer for undo description
        self._chosen_polygon_layer_id = polygon_layer.id()
        self._chosen_polygon_layer_name = polygon_layer.name()

        # Determine whether to process only selected points
        try:
            schema = self.get_settings_schema()
            process_selected_only = bool(self.get_setting('process_selected_only', schema['process_selected_only']['default']))
            confirm_setting = bool(self.get_setting('confirm_deletion', schema['confirm_deletion']['default']))
        except Exception:
            process_selected_only = False
            confirm_setting = True

        # Prepare processing mode
        point_crs = point_layer.crs()
        poly_crs = polygon_layer.crs()
        needs_transform = point_crs != poly_crs

        # If user requested only selected features and some are selected, process those
        to_delete_fids = set()
        try:
            if process_selected_only and point_layer.selectedFeatureCount() > 0:
                selected_feats = list(point_layer.selectedFeatures())

                # Create transform (point -> polygon CRS) if needed
                transform_point_to_poly = None
                if needs_transform:
                    try:
                        transform_point_to_poly = QgsCoordinateTransform(point_crs, poly_crs, project)
                    except Exception as e:
                        QgsMessageLog.logMessage(f"CRS transform creation failed: {e}", LOG_TAG, Qgis.Warning)

                # Build spatial index for polygon layer to speed-up candidate lookup
                    try:
                        poly_index = QgsSpatialIndex(polygon_layer.getFeatures())
                    except Exception as e:
                        QgsMessageLog.logMessage(f"Failed to build spatial index for polygon layer: {e}", LOG_TAG, Qgis.Warning)
                        poly_index = None

                    # Progress dialog for selected features
                    from qgis.PyQt.QtWidgets import QProgressDialog
                    progress = QProgressDialog(tr("Checking {count} selected points...").format(count=len(selected_feats)), tr("Cancel"), 0, len(selected_feats))
                    progress.setWindowTitle(tr("Checking Points"))
                    progress.setModal(True)
                    progress.setMinimumDuration(0)
                    progress.setValue(0)

                    for idx, pf in enumerate(selected_feats):
                        if progress.wasCanceled():
                            progress.close()
                            return
                        try:
                            geom = pf.geometry()
                            if not geom or geom.isEmpty():
                                continue

                            if needs_transform and transform_point_to_poly:
                                try:
                                    pt_geom = QgsGeometry.fromWkt(geom.asWkt())
                                    pt_geom.transform(transform_point_to_poly)
                                except Exception:
                                    QgsMessageLog.logMessage(f"Skipping feature {pf.id()} due to transform error", LOG_TAG, Qgis.Warning)
                                    continue
                            else:
                                pt_geom = QgsGeometry.fromWkt(geom.asWkt())

                            matched = False
                            candidate_fids = []
                            if poly_index:
                                try:
                                    candidate_fids = poly_index.intersects(pt_geom.boundingBox())
                                except Exception:
                                    candidate_fids = []

                            if candidate_fids:
                                req = QgsFeatureRequest().setFilterFids(candidate_fids)
                                for poly_feat in polygon_layer.getFeatures(req):
                                    poly_geom = poly_feat.geometry()
                                    if not poly_geom or poly_geom.isEmpty():
                                        continue
                                    try:
                                        if poly_geom.intersects(pt_geom) or poly_geom.contains(pt_geom):
                                            matched = True
                                            break
                                    except Exception:
                                        continue
                            else:
                                # fallback full scan
                                for poly_feat in polygon_layer.getFeatures():
                                    poly_geom = poly_feat.geometry()
                                    if not poly_geom or poly_geom.isEmpty():
                                        continue
                                    try:
                                        if poly_geom.intersects(pt_geom) or poly_geom.contains(pt_geom):
                                            matched = True
                                            break
                                    except Exception:
                                        continue

                            if not matched:
                                to_delete_fids.add(int(pf.id()))

                        except Exception as e:
                            QgsMessageLog.logMessage(f"Error while checking feature {getattr(pf, 'id', lambda: '?')()}: {e}", LOG_TAG, Qgis.Warning)
                            continue

                        progress.setValue(idx + 1)
                        QCoreApplication.processEvents()

                    try:
                        progress.close()
                    except Exception:
                        pass
                else:
                    # Full-layer mode: build spatial index on the point layer (and collect all fids)
                    total_points = point_layer.featureCount()
                    progress_idx = QProgressDialog(tr("Indexing points in layer '{layer}'...").format(layer=point_layer.name()), tr("Cancel"), 0, total_points)
                    progress_idx.setWindowTitle(tr("Indexing Points"))
                    progress_idx.setModal(True)
                    progress_idx.setMinimumDuration(0)
                    progress_idx.setValue(0)

                    point_index = QgsSpatialIndex()
                    all_point_fids = []
                    for i, feat in enumerate(point_layer.getFeatures()):
                        point_index.insertFeature(feat)
                        all_point_fids.append(int(feat.id()))
                        if progress_idx.wasCanceled():
                            try:
                                progress_idx.close()
                            except Exception:
                                pass
                            return
                        if (i + 1) % 1000 == 0:
                            progress_idx.setValue(i + 1)
                            QCoreApplication.processEvents()

                    try:
                        progress_idx.close()
                    except Exception:
                        pass

                    # Prepare transform poly -> point CRS if needed
                    transform_poly_to_point = None
                    if needs_transform:
                        try:
                            transform_poly_to_point = QgsCoordinateTransform(poly_crs, point_crs, project)
                        except Exception as e:
                            QgsMessageLog.logMessage(f"CRS transform creation failed: {e}", LOG_TAG, Qgis.Warning)

                    # Scan polygons and mark covered point fids
                    polygon_count = polygon_layer.featureCount()
                    progress_scan = QProgressDialog(tr("Scanning polygons in '{layer}'...").format(layer=polygon_layer.name()), tr("Cancel"), 0, polygon_count)
                    progress_scan.setWindowTitle(tr("Scanning Polygons"))
                    progress_scan.setModal(True)
                    progress_scan.setMinimumDuration(0)
                    progress_scan.setValue(0)

                    processed_fids = set()
                    covered_fids = set()

                    for idx, poly_feat in enumerate(polygon_layer.getFeatures()):
                        if progress_scan.wasCanceled():
                            try:
                                progress_scan.close()
                            except Exception:
                                pass
                            return

                        poly_geom = poly_feat.geometry()
                        if not poly_geom or poly_geom.isEmpty():
                            progress_scan.setValue(idx + 1)
                            QCoreApplication.processEvents()
                            continue

                        # Clone and transform polygon into point CRS if needed
                        try:
                            poly_for_points = QgsGeometry.fromWkt(poly_geom.asWkt())
                            if transform_poly_to_point:
                                poly_for_points.transform(transform_poly_to_point)
                        except Exception:
                            progress_scan.setValue(idx + 1)
                            QCoreApplication.processEvents()
                            continue

                        # Query candidate points via spatial index
                        try:
                            candidate_fids = point_index.intersects(poly_for_points.boundingBox())
                        except Exception:
                            candidate_fids = []

                        if not candidate_fids:
                            processed_fids.update(candidate_fids)
                            progress_scan.setValue(idx + 1)
                            QCoreApplication.processEvents()
                            continue

                        # Only check those not already processed
                        to_check = [fid for fid in candidate_fids if fid not in processed_fids]
                        if not to_check:
                            processed_fids.update(candidate_fids)
                            progress_scan.setValue(idx + 1)
                            QCoreApplication.processEvents()
                            continue

                        req = QgsFeatureRequest().setFilterFids(to_check)
                        for pt_feat in point_layer.getFeatures(req):
                            fid = int(pt_feat.id())
                            if fid in covered_fids:
                                continue
                            try:
                                pt_geom = pt_feat.geometry()
                                if not pt_geom or pt_geom.isEmpty():
                                    continue
                                # Check intersection/containment in point CRS
                                if poly_for_points.intersects(pt_geom) or poly_for_points.contains(pt_geom):
                                    covered_fids.add(fid)
                            except Exception:
                                continue

                        processed_fids.update(to_check)
                        progress_scan.setValue(idx + 1)
                        QCoreApplication.processEvents()

                    try:
                        progress_scan.close()
                    except Exception:
                        pass

                    # Points to delete are those indexed but not covered
                    to_delete_fids = set(all_point_fids) - covered_fids

        except Exception as e:
            QgsMessageLog.logMessage(f"Error during detection: {e}", LOG_TAG, Qgis.Warning)
            return

            if not to_delete_fids:
                self.show_info(tr('Nothing to delete'), tr('No points found outside the chosen polygon layer.'))
                return

            count = len(to_delete_fids)

            if confirm_setting:
                if not self.confirm_action(tr('Confirm Deletion'), tr('Delete {count} point(s) from layer "{layer}"?').format(count=count, layer=point_layer.name())):
                    return

            # Prepare backups for undo
            try:
                req = QgsFeatureRequest().setFilterFids(list(to_delete_fids))
                backups = [self.create_feature_backup(f, point_layer) for f in point_layer.getFeatures(req)]
            except Exception:
                backups = []

            # Try fast provider-level deletion first
            try:
                provider = point_layer.dataProvider()
                try:
                    deleted_ok = provider.deleteFeatures(list(to_delete_fids))
                except Exception:
                    deleted_ok = False

                if deleted_ok:
                    try:
                        point_layer.triggerRepaint()
                    except Exception:
                        pass
                else:
                    # Fallback to edit-mode deletion (slower)
                    was_in_edit, entered = self.handle_edit_mode(point_layer, 'delete points outside polygon layer')
                    if was_in_edit is None:
                        return
                    try:
                        for fid in list(to_delete_fids):
                            point_layer.deleteFeature(fid)
                        if not self.commit_changes(point_layer, 'delete points outside polygon layer'):
                            return
                    except Exception as e:
                        self.rollback_changes(point_layer)
                        self.show_error(tr('Error'), tr('Failed to delete features from layer {layer}: {error}').format(layer=point_layer.name(), error=str(e)))
                        return

            except Exception as e:
                self.show_error(tr('Error'), tr('Failed to delete features: {error}').format(error=str(e)))
                return

            # Save internal state for undo/history
            self._backups = backups
            self._affected_layers = [point_layer]
            self._deleted_fids_by_layer = {point_layer.id(): [b['fid'] for b in backups]}

            # Record history entry for undo
            try:
                self.record_to_history(
                    description=f"Deleted {count} points outside polygon layer {polygon_layer.name()}",
                    undo_type='delete_feature',
                    can_undo=True,
                    undo_payload=self.get_undo_payload(context),
                    layers=[self.create_layer_descriptor(point_layer)],
                    features=backups,
                )
            except Exception:
                pass

            self.show_info(tr('Deletion complete'), tr('Deleted {count} point(s) from layer "{layer}".').format(count=count, layer=point_layer.name()))


# Create global instance for automatic discovery
try:
    QgsMessageLog.logMessage('Loading action module: delete_points_outside_polygon_layer_extent', 'RightClickActions', Qgis.Info)
except Exception:
    pass

delete_points_outside_polygon_layer_extent_action = DeletePointsOutsidePolygonLayerExtentAction()
