"""
Single source of truth for resolving a QGIS map layer's kind
(VECTOR vs RASTER) and reporting kind-mismatch errors.

Raster-support Phase 1: every place in the codebase that currently
does an ad-hoc `isinstance(layer, QgsVectorLayer)` /
`isinstance(layer, QgsRasterLayer)` check should be migrated to
LayerKindResolver so the homogeneity rule (a Study is all-vector or
all-raster) has one consistent enforcement point.

LayerKindMismatchError is raised by guards that detect a vector layer
where a raster was expected (or vice-versa) — most notably the engine
input loader (executor.py) and the workflow `for-each-feature` step.
The exception carries the relevant names so the calling UI can render
a clear message instead of a stack trace.
"""

from __future__ import annotations

from typing import Optional

from .models import LayerKind


class LayerKindMismatchError(Exception):
    """Raised when a layer's kind does not match what the caller expected.

    Attributes:
        layer_name: Name of the offending layer (best-effort; may be "").
        expected:   The LayerKind the caller required.
        actual:     The LayerKind that was resolved, or None if the
                    layer could not be resolved to either kind.
    """

    def __init__(
        self,
        layer_name: str,
        expected: LayerKind,
        actual: Optional[LayerKind],
        message: Optional[str] = None,
    ) -> None:
        self.layer_name = layer_name or ""
        self.expected = expected
        self.actual = actual
        if message is None:
            actual_label = actual.value if actual is not None else "unknown"
            message = (
                f"Layer kind mismatch for '{self.layer_name}': "
                f"expected {expected.value}, got {actual_label}."
            )
        super().__init__(message)


class LayerKindResolver:
    """Resolve a QgsMapLayer to a LayerKind via isinstance checks.

    Kept as a tiny class (not a free function) so call sites read as
    `LayerKindResolver.resolve(layer)` — making it obvious in code
    review that this is the single source of truth, and easy to grep.

    Returns None if the layer is neither a QgsVectorLayer nor a
    QgsRasterLayer (mesh, point cloud, plugin layer, etc.). Callers
    that want to fail closed should compare to a specific LayerKind.
    """

    @staticmethod
    def resolve(layer) -> Optional[LayerKind]:
        if layer is None:
            return None
        # Import lazily so tests / non-QGIS contexts that pass duck-typed
        # stand-ins still work via the duck-typing fallback below.
        try:
            from qgis.core import QgsRasterLayer, QgsVectorLayer  # type: ignore

            if isinstance(layer, QgsRasterLayer):
                return LayerKind.RASTER
            if isinstance(layer, QgsVectorLayer):
                return LayerKind.VECTOR
        except Exception:
            pass

        # Duck-typing fallback for tests: any object exposing
        # `getFeatures` is treated as vector; one exposing `dataProvider`
        # + `width` (typical raster surface) is treated as raster. This
        # is intentionally narrow so we don't accidentally classify real
        # mesh/pointcloud layers.
        if hasattr(layer, "getFeatures") and hasattr(layer, "fields"):
            return LayerKind.VECTOR
        if hasattr(layer, "width") and hasattr(layer, "height") and hasattr(layer, "bandCount"):
            return LayerKind.RASTER
        return None

    @staticmethod
    def feature_count(layer) -> Optional[int]:
        """Cheap feature count for a vector layer, or None when unknown.

        QgsVectorLayer.featureCount() returns a negative sentinel for providers
        that have not resolved a count yet (QgsVectorDataProvider.UnknownCount
        = -1, Uncounted = -2); a UI must never render those as a number.
        Broken/closed providers can also raise, so the call is guarded.
        """
        counter = getattr(layer, "featureCount", None)
        if counter is None:
            return None
        try:
            count = counter()
        except Exception:
            return None
        if not isinstance(count, int) or count < 0:
            return None
        return count

    @staticmethod
    def is_vector(layer) -> bool:
        return LayerKindResolver.resolve(layer) == LayerKind.VECTOR

    @staticmethod
    def is_raster(layer) -> bool:
        return LayerKindResolver.resolve(layer) == LayerKind.RASTER

    @staticmethod
    def require(layer, expected: LayerKind, layer_name: str = "") -> None:
        """Raise LayerKindMismatchError if the layer's kind != expected.

        `layer_name` is used in the error message; pass the layer's
        display name if you have it handy (callers usually do, since
        they just looked the layer up by name).
        """
        actual = LayerKindResolver.resolve(layer)
        if actual != expected:
            raise LayerKindMismatchError(
                layer_name=layer_name or (getattr(layer, "name", lambda: "")() if layer else ""),
                expected=expected,
                actual=actual,
            )
