"""
Clip All Polygons In Layer To Chosen Layer Coverage

This action runs at layer scope. When invoked on a polygon layer it asks the
user to pick another polygon layer and clips every polygon feature in the
source layer so that only parts that fall under the chosen layer's coverage
remain. The action handles CRS transforms, edit-mode, batching of geometry
operations for performance, and records undo payloads for restore.
"""

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

from ..i18n import tr

import base64
import math


class ClipPolygonsLayerToLayerCoverageAction(BaseAction):
    def __init__(self):
        super().__init__()

        self.action_id = 'clip_polygons_layer_to_layer_coverage'
        self.name = 'Clip Layer Polygons to Layer Coverage'
        self.category = 'Editing'
        self.description = (
            'Clip every polygon in the clicked layer to the coverage of a chosen '
            'polygon layer. Parts outside the chosen layer are removed. Undoable.'
        )
        self.enabled = True

        # Layer-scoped action
        self.set_action_scope('layer')
        self.set_supported_scopes(['layer'])
        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': tr('Confirm'), 'description': tr('Confirm before clipping the whole layer')},
            'auto_commit_changes': {'type': 'bool', 'default': True, 'label': tr('Auto-commit'), 'description': tr('Automatically commit changes after clipping')},
            'batch_size': {'type': 'int', 'default': 50, 'label': tr('Batch union size'), 'description': tr('Number of geometries to union per batch (performance tuning)'), 'min': 1, 'max': 1000},
        }

    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:
        return execute_result or {}

    def _compute_union_of_layer(self, layer, geom_filter=None, batch_size=50):
        geoms = []
        # Use selected features if present
        if layer.selectedFeatureCount() > 0:
            feats = layer.selectedFeatures()
        else:
            if geom_filter is not None:
                try:
                    req = QgsFeatureRequest().setFilterRect(geom_filter.boundingBox())
                    req.setSubsetOfAttributes([])
                    feats = layer.getFeatures(req)
                except Exception:
                    feats = layer.getFeatures()
            else:
                feats = layer.getFeatures()

        for f in feats:
            g = f.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
            # If filter provided, skip those not intersecting
            if geom_filter is not None:
                try:
                    if not g.intersects(geom_filter):
                        continue
                except Exception:
                    pass
            geoms.append(g)

        if not geoms:
            return None

        # Batching union
        try:
            if len(geoms) > batch_size * 4:
                # chunk and iteratively union to avoid memory blowup
                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)

                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
            else:
                return QgsGeometry.unaryUnion(geoms)
        except Exception:
            try:
                u = geoms[0]
                for gg in geoms[1:]:
                    try:
                        u = u.combine(gg)
                    except Exception:
                        pass
                return u
            except Exception:
                return None

    def execute(self, context):
        try:
            confirm_clip = bool(self.get_setting('confirm_clip', True))
            auto_commit = bool(self.get_setting('auto_commit_changes', True))
            batch_size = int(self.get_setting('batch_size', 50))
            batch_size = max(1, min(1000, batch_size))
        except Exception as e:
            self.show_error(tr('Error'), tr('Invalid settings: {error}').format(error=str(e)))
            return

        detected = context.get('detected_features', [])
        if not detected:
            self.show_error(tr('Error'), tr('No layer detected in context'))
            return

        source_layer = detected[0].layer
        if source_layer is None or not isinstance(source_layer, QgsVectorLayer):
            self.show_error(tr('Error'), tr('Source layer not available'))
            return

        try:
            if source_layer.geometryType() != QgsWkbTypes.PolygonGeometry:
                self.show_error(tr('Error'), tr('This action works only on polygon layers'))
                return
        except Exception:
            pass

        # Choose target layer
        project = QgsProject.instance()
        polygon_layers = []
        for lid, lyr in project.mapLayers().items():
            if not isinstance(lyr, QgsVectorLayer):
                continue
            try:
                if lyr.geometryType() != QgsWkbTypes.PolygonGeometry:
                    continue
            except Exception:
                continue
            if lyr.id() == source_layer.id():
                continue
            polygon_layers.append(lyr)

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

        choices = [f"{ly.name()} ({ly.id()[:8]})" for ly in polygon_layers]
        chosen, ok = QInputDialog.getItem(None, tr('Select Target Layer'), tr('Clip to coverage of layer:'), choices, 0, False)
        if not ok:
            return

        # resolve chosen
        target_layer = None
        for ly in polygon_layers:
            if ly.id().startswith(chosen.split('(')[-1].rstrip(')')):
                target_layer = ly
                break
        if target_layer is None:
            # fallback by name
            name_part = chosen.split(' (')[0]
            for ly in polygon_layers:
                if ly.name() == name_part:
                    target_layer = ly
                    break

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

        if confirm_clip:
            if not self.confirm_action(tr('Clip Layer'), tr("Clip all polygons in layer '{src}' to coverage of '{tgt}'?").format(src=source_layer.name(), tgt=target_layer.name())):
                return

        # Compute union of target layer (only nearby features by bbox of source layer)
        try:
            # build a bbox representing source layer extent in target CRS
            source_extent = source_layer.extent()
            target_crs = target_layer.crs()
            source_crs = source_layer.crs()

            # transform bbox to target CRS if needed
            bbox_geom = None
            try:
                bbox_geom = QgsGeometry.fromRect(source_extent)
                if source_crs.isValid() and target_crs.isValid() and source_crs != target_crs:
                    t = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
                    bbox_geom.transform(t)
            except Exception:
                bbox_geom = None

            total_union = self._compute_union_of_layer(target_layer, geom_filter=bbox_geom, batch_size=batch_size)
            if total_union is None or total_union.isEmpty():
                # As a fallback, compute full union
                total_union = self._compute_union_of_layer(target_layer, geom_filter=None, batch_size=batch_size)
            if total_union is None or total_union.isEmpty():
                self.show_error(tr('Error'), tr('Failed to compute target layer coverage geometry'))
                return
        except Exception as e:
            self.show_error(tr('Error'), tr('Failed to compute target coverage: {error}').format(error=str(e)))
            return

        # Prepare to iterate source features and clip
        # Show busy cursor
        try:
            app = QApplication.instance()
            QApplication.setOverrideCursor(Qt.WaitCursor)
        except Exception:
            app = None

        # Start edit session on source layer
        was_in_edit, entered = self.handle_edit_mode(source_layer, tr('Clip layer polygons'))
        if was_in_edit is None:
            try:
                QApplication.restoreOverrideCursor()
            except Exception:
                pass
            return

        features_payload = []
        changed = 0

        try:
            # Iterate features (use selected subset if present)
            if source_layer.selectedFeatureCount() > 0:
                feats = list(source_layer.selectedFeatures())
            else:
                feats = list(source_layer.getFeatures())

            for feat in feats:
                fid = int(feat.id())
                g = feat.geometry()
                if not g or g.isEmpty():
                    continue

                # Transform feature geometry to target CRS for intersection
                feature_geom_work = QgsGeometry(g)
                try:
                    if source_crs.isValid() and target_crs.isValid() and source_crs != target_crs:
                        tf = QgsCoordinateTransform(source_crs, target_crs, QgsProject.instance())
                        feature_geom_work.transform(tf)
                except Exception:
                    pass

                try:
                    new_geom_in_target = feature_geom_work.intersection(total_union)
                    if new_geom_in_target is None or new_geom_in_target.isEmpty():
                        # result empty -> delete geometry (or set to empty)
                        # We'll set empty geometry
                        new_geom = QgsGeometry()
                    else:
                        # transform back to source CRS if needed
                        try:
                            if source_crs.isValid() and target_crs.isValid() and source_crs != target_crs:
                                inv = QgsCoordinateTransform(target_crs, source_crs, QgsProject.instance())
                                new_geom_in_target.transform(inv)
                        except Exception:
                            pass
                        new_geom = new_geom_in_target
                except Exception:
                    continue

                # If geometries are equal, skip
                try:
                    if new_geom and not new_geom.isEmpty() and new_geom.equals(g):
                        continue
                except Exception:
                    try:
                        if g.area() > 0 and new_geom.area() > 0:
                            if abs(g.area() - new_geom.area()) / g.area() < 1e-9:
                                continue
                    except Exception:
                        pass

                # Backup old geometry
                try:
                    backup = self.create_feature_backup(feat, source_layer)
                except Exception:
                    backup = None

                # Apply change
                try:
                    if not source_layer.changeGeometry(fid, new_geom):
                        raise Exception('changeGeometry failed')
                    changed += 1
                    try:
                        new_wkb = new_geom.asWkb() if new_geom and not new_geom.isEmpty() else b''
                        new_b64 = base64.b64encode(new_wkb).decode('utf-8') if new_wkb else None
                    except Exception:
                        new_b64 = None
                    features_payload.append({'fid': fid, 'old_geometry': backup.get('geometry') if backup else None, 'new_geometry': {'wkb_base64': new_b64} if new_b64 else None})
                except Exception:
                    # continue on errors for individual features
                    continue

            # Commit if requested
            if auto_commit:
                if not self.commit_changes(source_layer, 'Clip layer polygons'):
                    return

            # Record history
            try:
                layer_desc = self.create_layer_descriptor(source_layer)
                undo_payload = {'undo_type': 'update_geometry', 'layers': [layer_desc], 'features': features_payload, 'description': f'Clipped {changed} features in layer {source_layer.name()} to coverage of {target_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_count': changed, 'target_layer': target_layer.id()})
            except Exception:
                pass

            self.show_info(tr('Result'), tr('Clipped {count} features in layer "{name}"').format(count=changed, name=source_layer.name()))

        except Exception as e:
            self.rollback_changes(source_layer)
            self.show_error(tr('Error'), tr('Failed while clipping layer: {error}').format(error=str(e)))
        finally:
            # restore cursor and exit edit mode
            try:
                QApplication.restoreOverrideCursor()
            except Exception:
                pass
            self.exit_edit_mode(source_layer, entered)


# global instance
clip_polygons_layer_to_layer_coverage = ClipPolygonsLayerToLayerCoverageAction()
