"""
EPIC-1 Phase E6: Task completion handler for FilterTask.

Extracted from filter_task.py finished() method to reduce complexity.
This module handles post-task operations like subset application and canvas refresh.
"""

import logging
from typing import List, Tuple, Optional, Any, Callable

from qgis.core import QgsMessageLog, Qgis, QgsProject
from qgis.utils import iface
from qgis.PyQt.QtCore import QTimer

from ...core.ports.qgis_port import get_qgis_factory
from ...infrastructure.signal_utils import SignalBlocker
from ...infrastructure.constants import (
    QGIS_PROVIDER_POSTGRES, QGIS_PROVIDER_SPATIALITE, QGIS_PROVIDER_OGR,
    QGIS_PROVIDER_MEMORY,
)

logger = logging.getLogger(__name__)

# Import centralized validation (v4.0.4 - eliminate duplication)
try:
    from ...infrastructure.utils import is_layer_valid as is_valid_layer
except ImportError:
    # Fallback for testing or import issues
    def is_valid_layer(layer: Any) -> bool:
        """Check if layer is valid and accessible (fallback)."""
        if layer is None:
            return False
        try:
            return hasattr(layer, 'isValid') and layer.isValid() and hasattr(layer, 'id')
        except RuntimeError:
            return False


def display_warning_messages(warning_messages: List[str]) -> None:
    """
    Display warning messages stored during worker thread execution.

    These warnings could not be displayed from worker thread due to
    Qt thread safety requirements.

    Args:
        warning_messages: List of warning message strings to display
    """
    if not warning_messages:
        return

    for warning_msg in warning_messages:
        try:
            iface.messageBar().pushWarning("FilterMate", warning_msg)
        except (RuntimeError, AttributeError) as e:
            logger.warning(f"Could not display warning: {e}")


def should_skip_subset_application(
    is_canceled: bool,
    has_pending_requests: bool,
    pending_requests: List,
    result: Optional[bool]
) -> bool:
    """
    Determine if pending subset requests should be skipped.

    Args:
        is_canceled: Whether task.isCanceled() is True
        has_pending_requests: Whether there are pending requests
        pending_requests: The actual pending requests list
        result: The task result (True/False/None)

    Returns:
        bool: True if subset application should be skipped
    """
    # v3.0.8: Only skip if TRULY canceled (not just marked as canceled)
    # Check if we have pending requests AND if the task actually returned False (failed)
    # If the task succeeded (result=True), we should still apply the subsets even if
    # isCanceled() returns True (which can happen due to race conditions in QGIS)
    truly_canceled = (
        is_canceled and
        not (has_pending_requests and pending_requests and result is not False)
    )

    return truly_canceled and has_pending_requests and not pending_requests


def _layer_database_path(layer: Any) -> Optional[str]:
    """Return the on-disk SQLite/GeoPackage file backing this layer (or None).

    Mirrors :meth:`ParallelFilterExecutor._get_layer_database_path` so the
    same single-writer locking heuristics can be applied here. Recognised
    extensions: .gpkg, .sqlite, .db, .spatialite. Returned path is
    lowercased for case-insensitive equality on Windows/WSL.
    """
    try:
        if not hasattr(layer, 'source'):
            return None
        source = layer.source()
        if not source:
            return None

        file_path: Optional[str] = None
        if '|' in source:
            file_path = source.split('|')[0]
        elif "dbname='" in source:
            import re
            match = re.search(r"dbname='([^']+)'", source)
            if match:
                file_path = match.group(1)
        elif 'dbname="' in source:
            import re
            match = re.search(r'dbname="([^"]+)"', source)
            if match:
                file_path = match.group(1)
        else:
            file_path = source

        if file_path:
            lower = file_path.lower()
            if lower.endswith(('.gpkg', '.sqlite', '.db', '.spatialite')):
                return lower
        return None
    except (RuntimeError, AttributeError):
        return None


def apply_pending_subset_requests(
    pending_requests: List[Tuple[Any, str]],  # List[Tuple[QgsVectorLayer, str]]
    safe_set_subset_fn: Callable[[Any, str], bool]  # Callable[[QgsVectorLayer, str], bool]
) -> int:
    """
    Apply pending subset string requests on main thread.

    This is called from the main Qt thread (unlike run() which is on a worker thread).
    Fully extracted from FilterTask.finished() in Phase E6.

    Args:
        pending_requests: List of (layer, expression) tuples
        safe_set_subset_fn: Function to safely set subset string

    Returns:
        int: Number of successfully applied filters
    """
    if not pending_requests:
        return 0

    # DEDUP FIX 2026-04-21: defensive dedup on (layer_id, expression) at apply time.
    # Upstream queueing already dedups, but a second guard avoids noisy double-apply
    # when multiple code paths feed the same request.
    seen_keys = set()
    deduped_requests: List[Tuple[Any, str]] = []
    for layer, expression in pending_requests:
        try:
            key = (layer.id() if layer is not None else None, expression)
        except (RuntimeError, AttributeError):
            key = (None, expression)
        if key in seen_keys:
            continue
        seen_keys.add(key)
        deduped_requests.append((layer, expression))

    if len(deduped_requests) != len(pending_requests):
        logger.debug(
            f"Deduped subset requests: {len(pending_requests)} -> {len(deduped_requests)}"
        )
    pending_requests = deduped_requests

    QgsMessageLog.logMessage(
        f"📥 Applying {len(pending_requests)} pending subset requests on main thread",
        "FilterMate", Qgis.MessageLevel.Info
    )
    logger.info(f"finished(): Applying {len(pending_requests)} pending subset requests on main thread")

    # Log all pending requests details — promoted to QGIS log panel 2026-04-29
    # so the user can see exactly what subset is being applied per cascade
    # target. Without this it was impossible to distinguish "no subset queued"
    # from "empty subset queued" from "valid subset queued but rejected by
    # provider"; the previous DEBUG-level message only landed in the Python
    # console which most users don't have open.
    for idx, (lyr, expr) in enumerate(pending_requests):
        lyr_name = lyr.name() if lyr and is_valid_layer(lyr) else "INVALID"
        expr_preview = (expr[:120] + '...') if expr and len(expr) > 120 else (expr or 'EMPTY')
        logger.debug(f"  [{idx + 1}] {lyr_name}: {expr_preview}")
        QgsMessageLog.logMessage(
            f"  [{idx + 1}/{len(pending_requests)}] {lyr_name}: {expr_preview}",
            "FilterMate", Qgis.MessageLevel.Info
        )

    # Performance thresholds
    MAX_FEATURES_FOR_UPDATE_EXTENTS = 50000
    MAX_EXPRESSION_FOR_DIRECT_APPLY = 100000  # 100KB

    # 2026-04-29: count layers per shared SQLite file so we can throttle
    # consecutive setSubsetString+reload+featureCount calls. Without the
    # throttle, an 8-layers-from-the-same-server.sqlite cascade triggers
    # ``OGR error: sqlite3_step(): unable to open database file`` during
    # the redraw that follows each apply, and featureCount() returns the
    # stale pre-filter count — the cascade looks like a no-op even though
    # the subset string is correctly set.
    db_layer_counts: dict = {}
    for layer, _ in pending_requests:
        if layer is None:
            continue
        db_path = _layer_database_path(layer)
        if db_path:
            db_layer_counts[db_path] = db_layer_counts.get(db_path, 0) + 1

    # Collect large expressions for deferred application
    large_expressions = []
    applied_count = 0
    last_db_path: Optional[str] = None

    for layer, expression in pending_requests:
        try:
            layer.name() if layer else "NONE"

            if not layer or not is_valid_layer(layer):
                QgsMessageLog.logMessage(
                    f"finished() ✗ Layer invalid: {layer.name() if layer else 'None'}",
                    "FilterMate", Qgis.MessageLevel.Warning
                )
                continue

            # 2026-04-29: throttle consecutive applies on the same SQLite
            # file. Adaptive delay matches the existing parallel-executor
            # heuristic — more layers per DB → longer pause to let the
            # previous setSubsetString+reload release its file lock.
            current_db_path = _layer_database_path(layer)
            if current_db_path and current_db_path == last_db_path:
                shared_count = db_layer_counts.get(current_db_path, 1)
                if shared_count > 10:
                    delay = 0.5
                elif shared_count > 5:
                    delay = 0.3
                else:
                    delay = 0.2
                import time as _time
                _time.sleep(delay)

            current_subset = layer.subsetString() or ''
            expression_str = expression or ''

            # Check if expression is too large for direct application
            if expression_str and len(expression_str) > MAX_EXPRESSION_FOR_DIRECT_APPLY:
                logger.warning(f"  ⚠️ Large expression ({len(expression_str)} chars) for {layer.name()} - deferring")
                large_expressions.append((layer, expression_str))
                continue

            # Check if filter already applied
            if current_subset.strip() == expression_str.strip():
                # Filter already applied - force reload for PostgreSQL/OGR layers
                # FIX 2026-01-24: Skip reload for Spatialite with empty subset (unfilter operation)
                # layer.reload() on Spatialite with empty subset causes freeze because it tries
                # to reload from temporary tables that don't exist for unfilter operations.
                should_reload = (layer.providerType() in (QGIS_PROVIDER_POSTGRES, QGIS_PROVIDER_OGR) or
                                (layer.providerType() == QGIS_PROVIDER_SPATIALITE and expression_str.strip() != ''))

                if should_reload:
                    with SignalBlocker(layer):
                        layer.reload()

                # Update extents for smaller layers
                feature_count = layer.featureCount()
                if feature_count is not None and feature_count >= 0 and feature_count < MAX_FEATURES_FOR_UPDATE_EXTENTS:
                    try:
                        layer.updateExtents()
                    except (RuntimeError, AttributeError):
                        pass

                layer.triggerRepaint()

                # FIX v2.9.24: Clear selection for Spatialite layers after reload
                if layer.providerType() == QGIS_PROVIDER_SPATIALITE:
                    try:
                        layer.removeSelection()
                        logger.debug("Cleared selection after Spatialite filter (already applied)")
                    except (RuntimeError, AttributeError) as sel_err:
                        logger.debug(f"Could not clear selection: {sel_err}")

                count_str = f"{feature_count} features" if feature_count >= 0 else "(count pending)"
                logger.debug(f"  ✓ Filter already applied to {layer.name()}, triggered reload+repaint")
                QgsMessageLog.logMessage(
                    f"finished() ✓ Repaint: {layer.name()} → {count_str} (filter already applied)",
                    "FilterMate", Qgis.MessageLevel.Info
                )
                applied_count += 1
            else:
                # Apply new filter
                success = safe_set_subset_fn(layer, expression_str)

                if success:
                    # Force reload for PostgreSQL/OGR layers
                    # FIX 2026-01-24: Skip reload for Spatialite with empty subset (unfilter operation)
                    # layer.reload() on Spatialite with empty subset causes freeze because it tries
                    # to reload from temporary tables that don't exist for unfilter operations.
                    should_reload = (layer.providerType() in (QGIS_PROVIDER_POSTGRES, QGIS_PROVIDER_OGR) or
                                    (layer.providerType() == QGIS_PROVIDER_SPATIALITE and expression_str.strip() != ''))

                    if should_reload:
                        with SignalBlocker(layer):
                            layer.reload()

                    # Update extents for smaller layers
                    feature_count = layer.featureCount()
                    if feature_count is not None and feature_count >= 0 and feature_count < MAX_FEATURES_FOR_UPDATE_EXTENTS:
                        try:
                            layer.updateExtents()
                        except (RuntimeError, AttributeError):
                            pass

                    layer.triggerRepaint()

                    # FIX v2.9.24: Clear selection for Spatialite layers
                    if layer.providerType() == QGIS_PROVIDER_SPATIALITE:
                        try:
                            layer.removeSelection()
                            logger.debug("Cleared selection after Spatialite filter (new filter)")
                        except (RuntimeError, AttributeError) as sel_err:
                            logger.debug(f"Could not clear selection: {sel_err}")

                    logger.debug(f"  ✓ Applied filter to {layer.name()}: {len(expression_str)} chars")

                    # Handle feature count and logging
                    feature_count = layer.featureCount()
                    if feature_count >= 0:
                        QgsMessageLog.logMessage(
                            f"✓ Filter APPLIED: {layer.name()} → {feature_count} features",
                            "FilterMate", Qgis.MessageLevel.Info
                        )
                        # Diagnostic for layers with 0 features
                        if feature_count == 0:
                            logger.warning(f"  ⚠️ Layer {layer.name()} has 0 features after filtering!")
                            logger.warning(f"    → Expression length: {len(expression_str)} chars")
                            QgsMessageLog.logMessage(
                                f"⚠️ {layer.name()} → 0 features (filter may be too restrictive)",
                                "FilterMate", Qgis.MessageLevel.Warning
                            )
                    else:
                        QgsMessageLog.logMessage(
                            f"✓ Filter APPLIED: {layer.name()} → (count pending)",
                            "FilterMate", Qgis.MessageLevel.Info
                        )

                    applied_count += 1
                else:
                    # Enhanced diagnostic for failed filters
                    error_msg = 'Unknown error'
                    if layer.error():
                        error_msg = layer.error().message()
                    logger.warning(f"  ✗ Failed to apply filter to {layer.name()}")
                    logger.warning(f"    → Error: {error_msg}")
                    logger.warning(f"    → Expression ({len(expression_str)} chars): {expression_str[:200]}...")
                    logger.warning(f"    → Provider: {layer.providerType()}")
                    QgsMessageLog.logMessage(
                        f"finished() ✗ FAILED: {layer.name()} - {error_msg}",
                        "FilterMate", Qgis.MessageLevel.Critical
                    )

        except Exception as e:  # catch-all safety net: subset application must not crash finished()
            import traceback
            logger.error(f"  ✗ Error applying subset string: {e}")
            logger.error(f"    → Traceback: {traceback.format_exc()}")
            QgsMessageLog.logMessage(
                f"finished() ✗ Exception: {layer.name() if layer else 'Unknown'} - {str(e)}",
                "FilterMate", Qgis.MessageLevel.Critical
            )

        # Track last-touched DB path so the next iteration knows whether
        # to throttle on the same shared SQLite file.
        if layer is not None:
            db_path_after = _layer_database_path(layer)
            if db_path_after:
                last_db_path = db_path_after

    # Handle large expressions with deferred application
    if large_expressions:
        logger.info(f"  📦 Applying {len(large_expressions)} large expressions with deferred processing")
        _schedule_deferred_filter_application(large_expressions, safe_set_subset_fn)

    return applied_count


def _schedule_deferred_filter_application(
    large_expressions: List[Tuple[Any, str]],  # List[Tuple[QgsVectorLayer, str]]
    safe_set_subset_fn: Callable[[Any, str], bool]  # Callable[[QgsVectorLayer, str], bool]
) -> None:
    """
    Schedule deferred application of large filter expressions.

    This allows the UI to remain responsive during large filter application.

    Args:
        large_expressions: List of (layer, expression) tuples with large expressions
        safe_set_subset_fn: Function to safely set subset string
    """
    def apply_deferred_filters():
        """Apply large filter expressions with UI breathing room."""
        for lyr, expr in large_expressions:
            try:
                if lyr and is_valid_layer(lyr):
                    success = safe_set_subset_fn(lyr, expr)
                    if success:
                        lyr.triggerRepaint()
                        QgsMessageLog.logMessage(
                            f"finished() ✓ Deferred: {lyr.name()} → {lyr.featureCount()} features",
                            "FilterMate", Qgis.MessageLevel.Info
                        )
                    else:
                        logger.error(f"Failed to apply deferred filter to {lyr.name()}")
            except (RuntimeError, AttributeError, ValueError) as e:
                logger.error(f"Error applying deferred filter: {e}")

        # Final canvas refresh
        try:
            iface.mapCanvas().refresh()
        except (RuntimeError, AttributeError):
            pass

    # Defer to allow UI to breathe
    QTimer.singleShot(100, apply_deferred_filters)


def schedule_canvas_refresh(
    is_complex_filter_fn: Callable[[str, str], bool],
    single_refresh_fn: Callable[[], None]
) -> None:
    """
    Schedule canvas refresh with adaptive timing.

    Uses longer delay for complex filters to allow provider to process.

    Args:
        is_complex_filter_fn: Function to check if filter is complex
        single_refresh_fn: Function to perform single canvas refresh
    """
    try:
        # Stop any ongoing rendering first
        canvas = iface.mapCanvas()
        canvas.stopRendering()
        canvas.refresh()
        logger.debug("Immediate canvas refresh triggered after filter application (with stopRendering)")
    except (RuntimeError, AttributeError) as refresh_err:
        logger.debug(f"Could not trigger immediate canvas refresh: {refresh_err}")

    try:
        # Check if any filter is complex
        has_complex_filter = False
        # FIX 2026-01-15: Use QgsProject.instance() directly instead of factory.get_project()
        # The QGISFactory doesn't have a get_project() method
        project = QgsProject.instance()
        for layer_id, layer in project.mapLayers().items():
            if layer.type() == 0:  # Vector layer
                subset = layer.subsetString() or ''
                if subset and is_complex_filter_fn(subset, layer.providerType()):
                    has_complex_filter = True
                    break

        # Use longer delay for complex filters (configurable via APP.OPTIONS.UI_RESPONSIVENESS)
        try:
            from ...config.config import ENV_VARS
            _ui_resp = ENV_VARS.get('CONFIG_DATA', {}).get('APP', {}).get('OPTIONS', {}).get('UI_RESPONSIVENESS', {})
            _complex = _ui_resp.get('canvas_refresh_delay_complex_ms', {})
            _simple = _ui_resp.get('canvas_refresh_delay_simple_ms', {})
            delay_complex = _complex.get('value', 1500) if isinstance(_complex, dict) else 1500
            delay_simple = _simple.get('value', 500) if isinstance(_simple, dict) else 500
        except (ImportError, AttributeError, TypeError):
            delay_complex, delay_simple = 1500, 500
        refresh_delay = delay_complex if has_complex_filter else delay_simple

        # Schedule single comprehensive refresh
        QTimer.singleShot(refresh_delay, single_refresh_fn)
        logger.debug(f"Scheduled single canvas refresh in {refresh_delay}ms (complex={has_complex_filter})")

    except (RuntimeError, AttributeError) as canvas_err:
        logger.warning(f"Failed to schedule canvas refresh: {canvas_err}")
        # Fallback: immediate refresh
        try:
            iface.mapCanvas().refresh()
        except (RuntimeError, AttributeError):
            pass


def cleanup_memory_layer(ogr_source_geom: Optional[Any]) -> None:  # Optional[QgsVectorLayer]
    """
    Cleanup memory layer added to project to prevent garbage collection issues.

    Args:
        ogr_source_geom: The OGR source geometry layer to cleanup
    """
    if ogr_source_geom is None:
        return

    try:
        if ogr_source_geom.isValid() and ogr_source_geom.providerType() == QGIS_PROVIDER_MEMORY:
            # Check if layer is in project
            factory = get_qgis_factory()
            project = factory.get_project()
            if project.map_layer(ogr_source_geom.id()):
                logger.debug("finished(): Removing OGR source memory layer from project")
                project.remove_map_layer(ogr_source_geom.id())
    except (RuntimeError, AttributeError) as e:
        logger.debug(f"Could not cleanup memory layer: {e}")
