"""
BackendService - Backend Management Service.

Centralizes backend detection, selection, forcing, and validation logic.
Extracted from filter_mate_dockwidget.py as part of the God Class migration.

Story: MIG-075
Phase: 6 - God Class DockWidget Migration
Pattern: Strangler Fig - Gradual extraction

v4.1.5: BackendType now aliases canonical ProviderType from core.domain.filter_expression
"""

import logging
from typing import Dict, List, Optional, Tuple, Any, TYPE_CHECKING
from dataclasses import dataclass

try:
    from qgis.PyQt.QtCore import pyqtSignal, QObject
except ImportError:
    from PyQt5.QtCore import pyqtSignal, QObject

if TYPE_CHECKING:
    from qgis.core import QgsVectorLayer

from ..domain.filter_expression import ProviderType
from enum import Enum

logger = logging.getLogger(__name__)


# v4.1.5: BackendType uses canonical ProviderType values plus AUTO
class BackendType(Enum):
    """Available backend types (extends ProviderType with AUTO)."""
    POSTGRESQL = "postgresql"
    SPATIALITE = "spatialite"
    OGR = "ogr"
    MEMORY = "memory"
    AUTO = "auto"

    def to_provider_type(self) -> ProviderType:
        """Convert to ProviderType for domain operations."""
        if self == BackendType.AUTO:
            return ProviderType.UNKNOWN
        return ProviderType(self.value)


@dataclass
class BackendInfo:
    """Information about a backend."""
    type: BackendType
    name: str
    icon: str
    is_available: bool = True
    is_compatible: bool = True
    reason: str = ""


@dataclass
class BackendRecommendation:
    """Recommendation for optimal backend."""
    recommended: BackendType
    available_backends: List[BackendInfo]
    reason: str
    feature_count: int = 0
    provider_type: str = ""


class BackendService(QObject):
    """
    Service for backend management.

    Provides:
    - Backend detection for layers
    - Backend forcing (per-layer or global)
    - Backend compatibility testing
    - Optimal backend recommendation
    - Backend availability checking

    Emits:
    - backend_changed: When a backend is forced/changed
    - backend_detected: When auto-detection occurs
    - backends_available: When available backends change
    """

    # Signals
    backend_changed = pyqtSignal(str, str)  # layer_id, backend_type
    backend_detected = pyqtSignal(str, str)  # layer_id, detected_type
    backends_available = pyqtSignal(list)  # list of BackendInfo
    force_all_complete = pyqtSignal(int, int, int)  # forced, warned, skipped

    # Backend icons and names
    BACKEND_INFO = {
        BackendType.POSTGRESQL: ("PostgreSQL", "🐘"),
        BackendType.SPATIALITE: ("Spatialite", "💾"),
        BackendType.OGR: ("OGR", "📁"),
        BackendType.AUTO: ("Auto", "🔄"),
    }

    # Thresholds for backend recommendations
    LARGE_DATASET_THRESHOLD = 50000
    VERY_LARGE_DATASET_THRESHOLD = 200000

    def __init__(self, parent: Optional[QObject] = None):
        """
        Initialize BackendService.

        Args:
            parent: Optional parent QObject
        """
        super().__init__(parent)

        # Forced backends: layer_id -> BackendType
        self._forced_backends: Dict[str, BackendType] = {}

        # Cache for backend availability
        self._postgresql_available: Optional[bool] = None

        # Backend instances cache
        self._backend_instances: Dict[BackendType, Any] = {}

    # ─────────────────────────────────────────────────────────────────
    # Availability Checks
    # ─────────────────────────────────────────────────────────────────

    @property
    def is_postgresql_available(self) -> bool:
        """Check if PostgreSQL backend is available (psycopg2 installed)."""
        if self._postgresql_available is None:
            try:
                from ..ports import get_backend_services
                self._postgresql_available = get_backend_services().get_postgresql_availability().postgresql_available
            except ImportError:
                self._postgresql_available = False
        return self._postgresql_available

    def get_available_backends(self) -> List[BackendInfo]:
        """
        Get list of all potentially available backends.

        Returns:
            List of BackendInfo objects
        """
        backends = []

        # PostgreSQL (conditional)
        backends.append(BackendInfo(
            type=BackendType.POSTGRESQL,
            name="PostgreSQL",
            icon="🐘",
            is_available=self.is_postgresql_available,
            reason="" if self.is_postgresql_available else "psycopg2 not installed"
        ))

        # Spatialite (always available)
        backends.append(BackendInfo(
            type=BackendType.SPATIALITE,
            name="Spatialite",
            icon="💾",
            is_available=True
        ))

        # OGR (always available)
        backends.append(BackendInfo(
            type=BackendType.OGR,
            name="OGR",
            icon="📁",
            is_available=True
        ))

        return backends

    # ─────────────────────────────────────────────────────────────────
    # Backend Detection
    # ─────────────────────────────────────────────────────────────────

    def detect_backend(self, layer: "QgsVectorLayer") -> BackendType:
        """
        Detect which backend should be used for a layer.

        Priority:
        1. Forced backend (if set)
        2. Auto-detection based on provider

        Args:
            layer: QgsVectorLayer instance

        Returns:
            BackendType enum value
        """
        if layer is None or not layer.isValid():
            return BackendType.OGR

        layer_id = layer.id()

        # Check for forced backend
        if layer_id in self._forced_backends:
            forced = self._forced_backends[layer_id]
            logger.debug(f"Using forced backend {forced.value} for layer {layer.name()}")
            return forced

        # Auto-detect based on provider
        detected = self._auto_detect_backend(layer)
        self.backend_detected.emit(layer_id, detected.value)
        return detected

    def _auto_detect_backend(self, layer: "QgsVectorLayer") -> BackendType:
        """
        Auto-detect optimal backend based on layer provider.

        FIX v4.1.4 (2026-01-21): PostgreSQL layers ALWAYS use PostgreSQL backend.
        QGIS native API (setSubsetString) works without psycopg2.

        Args:
            layer: QgsVectorLayer instance

        Returns:
            BackendType enum value
        """
        provider_type = layer.providerType()

        # PostgreSQL layers ALWAYS use PostgreSQL backend (QGIS native API)
        if provider_type == 'postgres':
            return BackendType.POSTGRESQL
        elif provider_type == 'spatialite':
            return BackendType.SPATIALITE
        else:
            return BackendType.OGR

    def get_current_backend_string(self, layer: "QgsVectorLayer") -> str:
        """
        Get current backend as string (for backward compatibility).

        Args:
            layer: QgsVectorLayer instance

        Returns:
            Backend type string ('postgresql', 'spatialite', 'ogr')
        """
        return self.detect_backend(layer).value

    # ─────────────────────────────────────────────────────────────────
    # Backend Forcing
    # ─────────────────────────────────────────────────────────────────

    def force_backend(
        self,
        layer_id: str,
        backend_type: Optional[BackendType]
    ) -> None:
        """
        Force a specific backend for a layer.

        Args:
            layer_id: Layer ID
            backend_type: Backend to force, or None for auto
        """
        if backend_type is None:
            # Remove forced backend (use auto)
            if layer_id in self._forced_backends:
                del self._forced_backends[layer_id]
                logger.debug(f"Removed forced backend for layer {layer_id}")
        else:
            self._forced_backends[layer_id] = backend_type
            logger.debug(f"Forced backend {backend_type.value} for layer {layer_id}")

        self.backend_changed.emit(layer_id, backend_type.value if backend_type else "auto")

    def force_backend_string(
        self,
        layer_id: str,
        backend_type_str: Optional[str]
    ) -> None:
        """
        Force a backend using string type (backward compatibility).

        Args:
            layer_id: Layer ID
            backend_type_str: Backend type string or None
        """
        if backend_type_str is None:
            self.force_backend(layer_id, None)
        else:
            backend_type = BackendType(backend_type_str)
            self.force_backend(layer_id, backend_type)

    def get_forced_backend(self, layer_id: str) -> Optional[BackendType]:
        """
        Get forced backend for a layer, if any.

        Args:
            layer_id: Layer ID

        Returns:
            BackendType or None if auto
        """
        return self._forced_backends.get(layer_id)

    def get_forced_backend_string(self, layer_id: str) -> Optional[str]:
        """
        Get forced backend as string (backward compatibility).

        Args:
            layer_id: Layer ID

        Returns:
            Backend type string or None
        """
        forced = self.get_forced_backend(layer_id)
        return forced.value if forced else None

    def clear_forced_backend(self, layer_id: str) -> None:
        """
        Clear forced backend for a layer (use auto-detection).

        Args:
            layer_id: Layer ID
        """
        self.force_backend(layer_id, None)

    def clear_all_forced_backends(self) -> None:
        """Clear all forced backends."""
        layer_ids = list(self._forced_backends.keys())
        self._forced_backends.clear()

        for layer_id in layer_ids:
            self.backend_changed.emit(layer_id, "auto")

        logger.debug(f"Cleared forced backends for {len(layer_ids)} layers")

    # ─────────────────────────────────────────────────────────────────
    # Force All Layers
    # ─────────────────────────────────────────────────────────────────

    def force_backend_for_all_layers(
        self,
        backend_type: BackendType,
        layers: Optional[List["QgsVectorLayer"]] = None
    ) -> Tuple[int, int, int]:
        """
        Force a specific backend for all layers.

        Args:
            backend_type: Backend to force
            layers: List of layers (if None, uses QgsProject)

        Returns:
            Tuple of (forced_count, warned_count, skipped_count)
        """
        if layers is None:
            from qgis.core import QgsProject, QgsVectorLayer
            project = QgsProject.instance()
            all_layers = project.mapLayers().values()
            layers = [l for l in all_layers if isinstance(l, QgsVectorLayer)]

        logger.info("=" * 60)
        logger.debug(f"FORCING {backend_type.value.upper()} BACKEND FOR ALL LAYERS")
        logger.info("=" * 60)

        forced_count = 0
        warned_count = 0
        skipped_count = 0

        for layer in layers:
            if not layer.isValid():
                skipped_count += 1
                continue

            layer_name = layer.name()
            logger.info(f"\nProcessing layer: {layer_name}")

            # Check compatibility
            is_compatible, warn = self._check_backend_compatibility(layer, backend_type)

            if is_compatible and not warn:
                self.force_backend(layer.id(), backend_type)
                forced_count += 1
                logger.debug(f"  ✓ Forced backend to: {backend_type.value.upper()}")
            elif is_compatible and warn:
                # Compatible with warning (e.g., Spatialite on GeoPackage)
                self.force_backend(layer.id(), backend_type)
                warned_count += 1
                logger.warning(f"  ⚠️ Forced {backend_type.value.upper()} with warning")
            else:
                skipped_count += 1
                logger.info("  ⚠ Skipped - not compatible")

        total = forced_count + warned_count
        logger.info("\n" + "=" * 60)
        logger.info(f"FORCE BACKEND COMPLETE: {total} forced, {skipped_count} skipped")
        logger.info("=" * 60)

        self.force_all_complete.emit(forced_count, warned_count, skipped_count)
        return forced_count, warned_count, skipped_count

    def _check_backend_compatibility(
        self,
        layer: "QgsVectorLayer",
        backend_type: BackendType
    ) -> Tuple[bool, bool]:
        """
        Check if a backend is compatible with a layer.

        Returns:
            Tuple of (is_compatible, has_warning)
        """
        if backend_type == BackendType.POSTGRESQL:
            if not self.is_postgresql_available:
                return False, False
            # PostgreSQL only works with postgres provider
            return layer.providerType() == 'postgres', False

        elif backend_type == BackendType.SPATIALITE:
            provider = layer.providerType()
            source = layer.source().lower()

            if provider == 'spatialite':
                return True, False

            # GeoPackage/SQLite work with Spatialite (with warning)
            if 'gpkg' in source or 'sqlite' in source:
                return True, True

            # OGR may work (with warning)
            if provider == 'ogr':
                return True, True

            return False, False

        elif backend_type == BackendType.OGR:
            # OGR is universal fallback
            return True, False

        return False, False

    # ─────────────────────────────────────────────────────────────────
    # Backend Compatibility Testing
    # ─────────────────────────────────────────────────────────────────

    def verify_backend_supports_layer(
        self,
        layer: "QgsVectorLayer",
        backend_type: BackendType
    ) -> bool:
        """
        Verify that a backend can actually support a layer.

        Uses the backend's supports_layer() method.

        Args:
            layer: QgsVectorLayer instance
            backend_type: Backend type to test

        Returns:
            bool: True if backend supports this layer
        """
        if layer is None or not layer.isValid():
            return False

        try:
            backend = self._get_backend_instance(backend_type)
            if backend is None:
                return False

            return backend.supports_layer(layer)

        except Exception as e:
            logger.warning(
                f"Error testing backend {backend_type.value} for layer {layer.name()}: {e}"
            )
            return False

    def _get_backend_instance(self, backend_type: BackendType) -> Any:
        """
        Get or create a backend instance for testing.

        Args:
            backend_type: Backend type

        Returns:
            Backend instance or None
        """
        if backend_type in self._backend_instances:
            return self._backend_instances[backend_type]

        try:
            task_params = {}  # Minimal params for testing

            from ..ports import get_backend_services
            _services = get_backend_services()

            if backend_type == BackendType.POSTGRESQL:
                if not self.is_postgresql_available:
                    return None
                PostgreSQLGeometricFilter = _services.get_postgresql_geometric_filter()
                if not PostgreSQLGeometricFilter:
                    return None
                backend = PostgreSQLGeometricFilter(task_params)

            elif backend_type == BackendType.SPATIALITE:
                SpatialiteGeometricFilter = _services.get_spatialite_geometric_filter()
                if not SpatialiteGeometricFilter:
                    return None
                backend = SpatialiteGeometricFilter(task_params)

            elif backend_type == BackendType.OGR:
                OGRGeometricFilter = _services.get_ogr_geometric_filter()
                if not OGRGeometricFilter:
                    return None
                backend = OGRGeometricFilter(task_params)

            else:
                return None

            self._backend_instances[backend_type] = backend
            return backend

        except Exception as e:
            logger.warning(f"Could not create backend instance for {backend_type.value}: {e}")
            return None

    # ─────────────────────────────────────────────────────────────────
    # Available Backends for Layer
    # ─────────────────────────────────────────────────────────────────

    def get_available_backends_for_layer(
        self,
        layer: "QgsVectorLayer"
    ) -> List[BackendInfo]:
        """
        Get list of available backends for a specific layer.

        Args:
            layer: QgsVectorLayer instance

        Returns:
            List of BackendInfo objects (compatible backends)
        """
        if layer is None or not layer.isValid():
            return []

        available = []
        provider_type = layer.providerType()
        source = layer.source().lower()

        # PostgreSQL - FIX v4.1.4: ALWAYS available for postgres layers (QGIS native API)
        if provider_type == 'postgres':
            available.append(BackendInfo(
                type=BackendType.POSTGRESQL,
                name="PostgreSQL",
                icon="🐘",
                is_available=True,
                is_compatible=True
            ))

        # Spatialite
        if provider_type == 'spatialite':
            available.append(BackendInfo(
                type=BackendType.SPATIALITE,
                name="Spatialite",
                icon="💾",
                is_available=True,
                is_compatible=True
            ))
        elif 'gpkg' in source or 'sqlite' in source:
            available.append(BackendInfo(
                type=BackendType.SPATIALITE,
                name="Spatialite",
                icon="💾",
                is_available=True,
                is_compatible=True,
                reason="GeoPackage/SQLite support"
            ))

        # OGR (always available)
        available.append(BackendInfo(
            type=BackendType.OGR,
            name="OGR",
            icon="📁",
            is_available=True,
            is_compatible=True,
            reason="Universal fallback"
        ))

        self.backends_available.emit(available)
        return available

    # ─────────────────────────────────────────────────────────────────
    # Optimal Backend Recommendation
    # ─────────────────────────────────────────────────────────────────

    def recommend_optimal_backend(
        self,
        layer: "QgsVectorLayer"
    ) -> BackendRecommendation:
        """
        Recommend optimal backend for a layer.

        Considers:
        - Provider type
        - Feature count
        - PostgreSQL availability
        - Data source type

        Args:
            layer: QgsVectorLayer instance

        Returns:
            BackendRecommendation object
        """
        if layer is None or not layer.isValid():
            return BackendRecommendation(
                recommended=BackendType.OGR,
                available_backends=[],
                reason="Invalid or null layer"
            )

        provider_type = layer.providerType()
        feature_count = layer.featureCount()
        available = self.get_available_backends_for_layer(layer)

        # PostgreSQL is optimal for postgres layers - FIX v4.1.4: ALWAYS use PostgreSQL
        if provider_type == 'postgres':
            return BackendRecommendation(
                recommended=BackendType.POSTGRESQL,
                available_backends=available,
                reason="Native PostgreSQL layer (QGIS native API)",
                feature_count=feature_count,
                provider_type=provider_type
            )

        # Spatialite for spatialite layers
        if provider_type == 'spatialite':
            return BackendRecommendation(
                recommended=BackendType.SPATIALITE,
                available_backends=available,
                reason="Native Spatialite layer",
                feature_count=feature_count,
                provider_type=provider_type
            )

        # For large datasets, recommend Spatialite if available
        source = layer.source().lower()
        if feature_count > self.LARGE_DATASET_THRESHOLD:
            if 'gpkg' in source or 'sqlite' in source:
                return BackendRecommendation(
                    recommended=BackendType.SPATIALITE,
                    available_backends=available,
                    reason=f"Large dataset ({feature_count:,} features) - Spatialite recommended",
                    feature_count=feature_count,
                    provider_type=provider_type
                )

        # Default to OGR
        return BackendRecommendation(
            recommended=BackendType.OGR,
            available_backends=available,
            reason="Default OGR backend",
            feature_count=feature_count,
            provider_type=provider_type
        )

    # ─────────────────────────────────────────────────────────────────
    # Backend Info Helpers
    # ─────────────────────────────────────────────────────────────────

    def get_backend_display_info(
        self,
        backend_type: BackendType
    ) -> Tuple[str, str]:
        """
        Get display name and icon for a backend.

        Args:
            backend_type: Backend type

        Returns:
            Tuple of (name, icon)
        """
        return self.BACKEND_INFO.get(backend_type, ("Unknown", "❓"))

    def get_backend_from_string(self, backend_str: str) -> BackendType:
        """
        Convert string to BackendType enum.

        Args:
            backend_str: Backend string

        Returns:
            BackendType enum value
        """
        try:
            return BackendType(backend_str.lower())
        except ValueError:
            return BackendType.OGR

    # ─────────────────────────────────────────────────────────────────
    # State Management
    # ─────────────────────────────────────────────────────────────────

    def get_forced_backends_summary(self) -> Dict[str, str]:
        """
        Get summary of all forced backends.

        Returns:
            Dict mapping layer_id to backend_type string
        """
        return {
            layer_id: backend.value
            for layer_id, backend in self._forced_backends.items()
        }

    def set_forced_backends_from_dict(self, forced_dict: Dict[str, str]) -> None:
        """
        Restore forced backends from a dictionary.

        Args:
            forced_dict: Dict mapping layer_id to backend_type string
        """
        self._forced_backends.clear()

        for layer_id, backend_str in forced_dict.items():
            try:
                backend_type = BackendType(backend_str)
                self._forced_backends[layer_id] = backend_type
            except ValueError:
                logger.warning(f"Invalid backend type '{backend_str}' for layer {layer_id}")

    def cleanup_removed_layers(self, existing_layer_ids: List[str]) -> int:
        """
        Remove forced backends for layers that no longer exist.

        Args:
            existing_layer_ids: List of existing layer IDs

        Returns:
            Number of entries removed
        """
        existing_set = set(existing_layer_ids)
        to_remove = [
            layer_id for layer_id in self._forced_backends
            if layer_id not in existing_set
        ]

        for layer_id in to_remove:
            del self._forced_backends[layer_id]

        if to_remove:
            logger.debug(f"Cleaned up {len(to_remove)} forced backend entries")

        return len(to_remove)
