"""
Clip Polygon to Layer Extent Action

Clips the clicked polygon feature to the bounding extent of a user-selected
polygon layer. Any parts of the feature outside the chosen layer's extent
are removed. Handles CRS transformation, edit mode, commit/rollback and
records an undo payload so the change can be reverted.
"""

from .base_action import BaseAction
from qgis.PyQt.QtWidgets import QInputDialog, QApplication
from qgis.PyQt.QtCore import Qt
from qgis.core import (
    QgsProject,
    QgsVectorLayer,
    QgsGeometry,
    QgsWkbTypes,
    QgsCoordinateTransform,
    QgsFeatureRequest,
)

import base64
import re
from ..i18n import tr


class ClipPolygonToLayerExtentAction(BaseAction):
    """Action to clip a polygon feature to the extent of another polygon layer."""

    def __init__(self):
        super().__init__()

        # Required properties
        self.action_id = "clip_polygon_to_layer_extent"
        self.name = "Clip Polygon to Layer Extent"
        self.category = "Editing"
        self.description = (
            "Clip the selected polygon feature to the bounding extent of a "
            "chosen polygon layer. Parts outside the chosen layer's extent "
            "will be removed."
        )
        self.enabled = True

        # Scope and supported types
        self.set_action_scope('feature')
        self.set_supported_scopes(['feature'])
        self.set_supported_click_types(['polygon', 'multipolygon'])
        self.set_supported_geometry_types(['polygon', 'multipolygon'])

    def get_settings_schema(self):
        return {
            'confirm_clip': {
                'type': 'bool',
                'default': True,
                'label': 'Confirm before clipping',
                'description': 'Ask user to confirm before applying the clip',
            },
            'auto_commit_changes': {
                'type': 'bool',
                'default': True,
                'label': 'Auto-commit changes',
                'description': 'Automatically commit changes after clipping',
            },
            'show_success_message': {
                'type': 'bool',
                'default': True,
                'label': 'Show success message',
                'description': 'Show a brief message when clipping succeeds',
            },
        }

    # Undo support
    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:
        # We rely on the history payload passed at record time; return execute_result
        return execute_result or {}

    def execute(self, context):
        # Read settings with proper conversion
        try:
            confirm_clip = bool(self.get_setting('confirm_clip', True))
            auto_commit = bool(self.get_setting('auto_commit_changes', True))
            show_success = bool(self.get_setting('show_success_message', True))
        except (ValueError, TypeError) as e:
            self.show_error('Error', f'Invalid setting values: {e}')
            return

        # Extract context
        detected_features = context.get('detected_features', [])
        if not detected_features:
            self.show_error('Error', 'No polygon features found at this location')
            return

        detected_feature = detected_features[0]
        feature = detected_feature.feature
        layer = detected_feature.layer

        if feature is None or layer is None:
            self.show_error('Error', 'Feature or layer not available in context')
            return

        geometry = feature.geometry()
        if geometry is None or geometry.isEmpty():
            self.show_error('Error', 'Feature has no geometry')
            return

        try:
            if geometry.type() != QgsWkbTypes.PolygonGeometry:
                self.show_error('Error', 'This action only works with polygon geometries')
                return
        except Exception:
            pass

        # Collect polygon layers in the project (exclude the source layer)
        project = QgsProject.instance()
        polygon_layers = []
        for lid, lyr in project.mapLayers().items():
            if not isinstance(lyr, QgsVectorLayer):
                continue
            # Exclude the layer we clicked on to avoid a no-op
            try:
                if lyr.id() == layer.id():
                    continue
            except Exception:
                pass
            try:
                if lyr.geometryType() != QgsWkbTypes.PolygonGeometry:
                    continue
            except Exception:
                continue
            if not lyr.isValid():
                continue
            polygon_layers.append(lyr)

        if not polygon_layers:
            self.show_error('Error', 'No polygon layers available in the project')
            return

        # Build display choices (name + short id) to reduce confusion with duplicate names
        choices = [f"{ly.name()} ({ly.id()[:8]})" for ly in polygon_layers]
        chosen, ok = QInputDialog.getItem(None, tr('Select Polygon Layer'), tr('Choose polygon layer to clip to extent:'), choices, 0, False)
        if not ok:
            return

        # Resolve chosen layer (prefer id match)
        selected_layer = None
        m = re.search(r'\(([^)]+)\)$', chosen)
        if m:
            short_id = m.group(1)
            for ly in polygon_layers:
                if ly.id().startswith(short_id):
                    selected_layer = ly
                    break

        if selected_layer is None:
            # fallback: match by name portion
            name_part = chosen.split(' (')[0]
            for ly in polygon_layers:
                if ly.name() == name_part:
                    selected_layer = ly
                    break

        if selected_layer is None:
            self.show_error(tr('Error'), tr('Selected layer not found'))
            return

        # Confirm with user if requested
        if confirm_clip:
            if not self.confirm_action(tr('Clip Polygon'), tr("Clip feature {fid} from layer '{source}' to extent of layer '{target}'?").format(fid=feature.id(), source=layer.name(), target=selected_layer.name())):
                return

        # Backup the original feature for undo
        try:
            original_backup = self.create_feature_backup(feature, layer)
        except Exception:
            original_backup = None

        # Work on a copy of the geometry so we don't mutate the original prematurely
        try:
            geom_copy = QgsGeometry(feature.geometry())
        except Exception:
            try:
                geom_copy = feature.geometry().clone()
            except Exception:
                geom_copy = feature.geometry()

        src_crs = layer.crs()
        tgt_crs = selected_layer.crs()

        # Transform geometry to target layer CRS if needed
        try:
            if src_crs != tgt_crs and src_crs.isValid() and tgt_crs.isValid():
                transform = QgsCoordinateTransform(src_crs, tgt_crs, QgsProject.instance())
                geom_copy.transform(transform)
        except Exception as e:
            self.show_error('Error', f'CRS transform failed: {e}')
            return

        # Build union of polygon geometries from the selected layer, but only
        # use selected features (if any) or those that intersect the clicked
        # feature's bounding box to avoid processing the entire layer.
        app = None
        try:
            app = QApplication.instance()
            QApplication.setOverrideCursor(Qt.WaitCursor)
        except Exception:
            app = None

        try:
            layer_geoms = []

            # Use selected features if available (faster and explicit)
            if selected_layer.selectedFeatureCount() > 0:
                features_iter = selected_layer.selectedFeatures()
            else:
                # Use a spatial filter to retrieve only features whose bbox
                # intersects the clicked geometry bbox. This relies on the
                # provider's spatial filtering for performance.
                try:
                    req = QgsFeatureRequest().setFilterRect(geom_copy.boundingBox())
                    req.setSubsetOfAttributes([])
                    features_iter = selected_layer.getFeatures(req)
                except Exception:
                    features_iter = selected_layer.getFeatures()

            for feat in features_iter:
                g = feat.geometry()
                if not g or g.isEmpty():
                    continue
                try:
                    if not g.isGeosValid():
                        g = g.makeValid()
                        if not g or g.isEmpty():
                            continue
                except Exception:
                    pass
                # quick spatial check: must intersect the source geometry
                try:
                    if not g.intersects(geom_copy):
                        continue
                except Exception:
                    pass
                layer_geoms.append(g)

            if not layer_geoms:
                self.show_error('Error', 'Selected layer contains no polygon geometries intersecting the feature (or no features selected).')
                return

            # For very large numbers of geometries, perform a batched union to
            # reduce memory spikes and improve performance.
            total_union = None
            try:
                if len(layer_geoms) > 200:
                    def batch_union(geoms, batch_size=50):
                        chunks = []
                        for i in range(0, len(geoms), batch_size):
                            chunk = geoms[i:i+batch_size]
                            try:
                                u = QgsGeometry.unaryUnion(chunk)
                            except Exception:
                                u = chunk[0]
                                for gg in chunk[1:]:
                                    try:
                                        u = u.combine(gg)
                                    except Exception:
                                        pass
                            if u and not u.isEmpty():
                                chunks.append(u)
                        # iteratively union the chunks
                        while len(chunks) > 1:
                            new_chunks = []
                            for i in range(0, len(chunks), batch_size):
                                chunk = chunks[i:i+batch_size]
                                try:
                                    u = QgsGeometry.unaryUnion(chunk)
                                except Exception:
                                    u = chunk[0]
                                    for gg in chunk[1:]:
                                        try:
                                            u = u.combine(gg)
                                        except Exception:
                                            pass
                                if u and not u.isEmpty():
                                    new_chunks.append(u)
                            chunks = new_chunks
                        return chunks[0] if chunks else None

                    total_union = batch_union(layer_geoms, batch_size=50)
                else:
                    total_union = QgsGeometry.unaryUnion(layer_geoms)
            except Exception:
                # Fallback to iterative combine
                try:
                    total_union = layer_geoms[0]
                    for g in layer_geoms[1:]:
                        try:
                            total_union = total_union.combine(g)
                        except Exception:
                            pass
                except Exception as e:
                    self.show_error('Error', f'Failed to compute union of layer geometries: {e}')
                    return

            if not total_union or total_union.isEmpty():
                self.show_error('Error', 'Failed to compute the union of the selected layer geometries.')
                return

            # Intersect the clicked feature with the unioned coverage geometry
            clipped = geom_copy.intersection(total_union)
            if clipped is None or clipped.isEmpty():
                self.show_warning('Result Empty', 'Clipping produced an empty geometry. No changes made.')
                return

            # If the intersection equals the original geometry (in target CRS), no change is needed
            try:
                if clipped.equals(geom_copy):
                    self.show_info('No action', 'Feature is already fully covered by the chosen layer geometries. No changes made.')
                    return
            except Exception:
                try:
                    orig_area = geom_copy.area()
                    clipped_area = clipped.area()
                    if orig_area > 0 and abs(orig_area - clipped_area) / orig_area < 1e-9:
                        self.show_info('No action', 'Feature is already fully covered by the chosen layer geometries. No changes made.')
                        return
                except Exception:
                    pass

        except Exception as e:
            self.show_error('Error', f'Geometry intersection failed: {e}')
            return
        finally:
            try:
                QApplication.restoreOverrideCursor()
            except Exception:
                pass

        # Transform clipped geometry back to source layer CRS if needed
        try:
            if src_crs != tgt_crs and src_crs.isValid() and tgt_crs.isValid():
                inv_transform = QgsCoordinateTransform(tgt_crs, src_crs, QgsProject.instance())
                clipped.transform(inv_transform)
        except Exception as e:
            self.show_error('Error', f'Failed to transform clipped geometry back to layer CRS: {e}')
            return

        # Start edit mode
        edit_result = self.handle_edit_mode(layer, 'Clip Polygon to Layer Extent')
        if edit_result[0] is None:
            return
        was_in_edit_mode, edit_entered = edit_result

        try:
            fid = int(feature.id())

            # Apply geometry change
            if not layer.changeGeometry(fid, clipped):
                raise Exception('changeGeometry failed')

            # Prepare history payload: include old and new geometries (WKB base64)
            try:
                new_wkb = clipped.asWkb()
                new_geom_b64 = base64.b64encode(new_wkb).decode('utf-8')
            except Exception:
                new_geom_b64 = None

            features_payload = [
                {
                    'fid': fid,
                    'old_geometry': original_backup.get('geometry') if original_backup else None,
                    'new_geometry': {'wkb_base64': new_geom_b64} if new_geom_b64 else None,
                }
            ]

            # Commit changes if requested
            if auto_commit:
                if not self.commit_changes(layer, 'Clip Polygon to Layer Extent'):
                    # Commit failed - rollback already called by commit_changes
                    return

            # Ensure layer refresh so map updates immediately
            try:
                layer.triggerRepaint()
            except Exception:
                pass

            # Record to history for undo/redo
            try:
                layer_desc = self.create_layer_descriptor(layer)
                undo_payload = {
                    'undo_type': 'update_geometry',
                    'layers': [layer_desc],
                    'features': features_payload,
                    'description': f"Clipped feature {fid} to extent of layer '{selected_layer.name()}'",
                }

                self.record_to_history(
                    description=undo_payload['description'],
                    undo_type='update_geometry',
                    can_undo=True,
                    undo_payload=undo_payload,
                    layers=[layer_desc],
                    features=features_payload,
                    meta={'clipped_to_layer': selected_layer.id(), 'feature_id': fid},
                )
            except Exception:
                # History recording must not block success
                pass

            if show_success:
                self.show_info('Success', f"Clipped feature {fid} to extent of layer '{selected_layer.name()}'")

        except Exception as e:
            # Rollback on error
            try:
                self.rollback_changes(layer)
            except Exception:
                pass
            self.show_error('Error', f'Failed to clip polygon: {e}')
        finally:
            # Exit edit mode if we entered it
            self.exit_edit_mode(layer, edit_entered)


# Create global instance for automatic discovery
clip_polygon_to_layer_extent_action = ClipPolygonToLayerExtentAction()
