"""
Erase Polygon by Layer Coverage Action

Erases the parts of the right-clicked polygon feature that overlap with a
chosen polygon layer. Only the area NOT covered by the chosen layer remains.
This is the geometric difference / erase operation — the opposite of clip.

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 ErasePolygonByLayerCoverageAction(BaseAction):
    """Action to erase the parts of a polygon that overlap with another polygon layer."""

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

        self.action_id = "erase_polygon_by_layer_coverage"
        self.name = "Erase Polygon by Layer Coverage"
        self.category = "Editing"
        self.description = (
            "Erase the parts of the selected polygon that overlap with a chosen "
            "polygon layer. Only the area NOT covered by the chosen layer remains. "
            "This is the geometric difference — the opposite of clip."
        )
        self.enabled = True

        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_erase': {
                'type': 'bool',
                'default': True,
                'label': 'Confirm before erasing',
                'description': 'Ask user to confirm before applying the erase',
            },
            'auto_commit_changes': {
                'type': 'bool',
                'default': True,
                'label': 'Auto-commit changes',
                'description': 'Automatically commit changes after erasing',
            },
            'show_success_message': {
                'type': 'bool',
                'default': True,
                'label': 'Show success message',
                'description': 'Show a brief message when erasing 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:
        return execute_result or {}

    # ------------------------------------------------------------------
    # Helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _build_union(geoms, batch_size=50):
        """Build union of a list of QgsGeometry objects using batching for performance."""
        if not geoms:
            return None
        try:
            if len(geoms) <= batch_size * 4:
                return QgsGeometry.unaryUnion(geoms)

            # For large sets: chunk → union chunks → iteratively reduce
            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

        except Exception:
            # Fallback: iterative combine
            try:
                u = geoms[0]
                for gg in geoms[1:]:
                    try:
                        u = u.combine(gg)
                    except Exception:
                        pass
                return u
            except Exception:
                return None

    # ------------------------------------------------------------------
    # Main execute
    # ------------------------------------------------------------------

    def execute(self, context):
        try:
            confirm_erase = bool(self.get_setting('confirm_erase', 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 candidate eraser layers (all polygon layers except source) ---
        project = QgsProject.instance()
        polygon_layers = []
        for lid, lyr in project.mapLayers().items():
            if not isinstance(lyr, QgsVectorLayer):
                continue
            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 other polygon layers available in the project')
            return

        # --- Let user pick the eraser layer ---
        choices = [f"{ly.name()} ({ly.id()[:8]})" for ly in polygon_layers]
        chosen, ok = QInputDialog.getItem(
            None, tr('Select Eraser Layer'),
            tr('Choose polygon layer whose coverage to erase from the feature:'),
            choices, 0, False
        )
        if not ok:
            return

        # Resolve chosen layer by short id
        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:
            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('Error', 'Selected layer not found')
            return

        # --- Optional confirmation ---
        if confirm_erase:
            if not self.confirm_action(
                'Erase Polygon',
                f"Erase parts of feature {feature.id()} (layer '{layer.name()}') "
                f"that overlap with layer '{selected_layer.name()}'?"
            ):
                return

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

        # Work on a geometry copy to avoid mutating 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()
        eraser_crs = selected_layer.crs()

        # Transform source geometry to eraser layer CRS for the difference operation
        try:
            if src_crs != eraser_crs and src_crs.isValid() and eraser_crs.isValid():
                t_fwd = QgsCoordinateTransform(src_crs, eraser_crs, QgsProject.instance())
                geom_copy.transform(t_fwd)
        except Exception as e:
            self.show_error('Error', f'CRS transform failed: {e}')
            return

        # --- Compute the eraser union (busy cursor while processing) ---
        try:
            QApplication.setOverrideCursor(Qt.WaitCursor)
        except Exception:
            pass

        result = None
        try:
            # Fetch only eraser features whose bbox intersects the source feature
            # bbox — avoids loading the entire eraser layer into memory.
            eraser_geoms = []
            if selected_layer.selectedFeatureCount() > 0:
                feat_iter = selected_layer.selectedFeatures()
            else:
                try:
                    req = QgsFeatureRequest().setFilterRect(geom_copy.boundingBox())
                    req.setSubsetOfAttributes([])
                    feat_iter = selected_layer.getFeatures(req)
                except Exception:
                    feat_iter = selected_layer.getFeatures()

            for ef in feat_iter:
                eg = ef.geometry()
                if not eg or eg.isEmpty():
                    continue
                try:
                    if not eg.isGeosValid():
                        eg = eg.makeValid()
                        if not eg or eg.isEmpty():
                            continue
                except Exception:
                    pass
                # Fine-grained spatial filter: skip if no actual intersection
                try:
                    if not eg.intersects(geom_copy):
                        continue
                except Exception:
                    pass
                eraser_geoms.append(eg)

            if not eraser_geoms:
                self.show_info(
                    tr('No Overlap'),
                    tr('No geometries from the chosen layer intersect the feature. No changes made.')
                )
                return

            eraser_union = self._build_union(eraser_geoms, batch_size=50)
            if eraser_union is None or eraser_union.isEmpty():
                self.show_error(tr('Error'), tr('Failed to compute union of eraser layer geometries.'))
                return

            # === Core operation: difference instead of intersection ===
            result = geom_copy.difference(eraser_union)

            if result is None or result.isEmpty():
                self.show_warning(
                    tr('Polygon Fully Erased'),
                    tr('The chosen layer completely covers this polygon. Erasing would remove the entire polygon. No changes were made.')
                )
                return

            # No-op check: if difference equals original, there was no overlap
            try:
                if result.equals(geom_copy):
                    self.show_info(
                        tr('No Overlap'),
                        tr('The feature does not overlap the chosen layer. No changes made.')
                    )
                    return
            except Exception:
                try:
                    orig_area = geom_copy.area()
                    result_area = result.area()
                    if orig_area > 0 and abs(orig_area - result_area) / orig_area < 1e-9:
                        self.show_info(
                            'No Overlap',
                            'The feature does not overlap the chosen layer. No changes made.'
                        )
                        return
                except Exception:
                    pass

        except Exception as e:
            self.show_error(tr('Error'), tr('Geometry difference operation failed: {error}').format(error=e))
            return
        finally:
            try:
                QApplication.restoreOverrideCursor()
            except Exception:
                pass

        # Transform result back to source layer CRS
        try:
            if src_crs != eraser_crs and src_crs.isValid() and eraser_crs.isValid():
                t_inv = QgsCoordinateTransform(eraser_crs, src_crs, QgsProject.instance())
                result.transform(t_inv)
        except Exception as e:
            self.show_error(tr('Error'), tr('Failed to transform result geometry back to source CRS: {error}').format(error=e))
            return

        # --- Enter edit mode and apply ---
        edit_result = self.handle_edit_mode(layer, 'Erase Polygon by Layer Coverage')
        if edit_result[0] is None:
            return
        was_in_edit_mode, edit_entered = edit_result

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

            if not layer.changeGeometry(fid, result):
                raise Exception('changeGeometry failed')

            # Build undo payload
            try:
                new_wkb = result.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,
                }
            ]

            if auto_commit:
                if not self.commit_changes(layer, 'Erase Polygon by Layer Coverage'):
                    return

            try:
                layer.triggerRepaint()
            except Exception:
                pass

            # Record history
            try:
                layer_desc = self.create_layer_descriptor(layer)
                undo_payload = {
                    'undo_type': 'update_geometry',
                    'layers': [layer_desc],
                    'features': features_payload,
                    'description': (
                        f"Erased overlapping parts of feature {fid} "
                        f"(layer '{layer.name()}') using 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={'erased_by_layer': selected_layer.id(), 'feature_id': fid},
                )
            except Exception:
                pass

            if show_success:
                self.show_info(
                    'Success',
                    f"Erased overlapping parts of feature {fid} "
                    f"using layer '{selected_layer.name()}'"
                )

        except Exception as e:
            try:
                self.rollback_changes(layer)
            except Exception:
                pass
            self.show_error('Error', f'Failed to erase polygon: {e}')
        finally:
            self.exit_edit_mode(layer, edit_entered)


# Create global instance for automatic discovery
erase_polygon_by_layer_coverage_action = ErasePolygonByLayerCoverageAction()
