"""
Raster study input loader.

Parallel to executor._load_input_layer_data (which loads a vector
layer's attributes into a pandas DataFrame for the engines). For
raster studies, the prepared input is the overlay raster produced by
RasterOverlay plus its enriched RAT; engines that work on the RAT
(the common EMDS case) consume `rat_dataframe` directly, which is the
same DataFrame shape today's vector path delivers.

This module deliberately keeps the loader narrow — it loads the
raster + its RAT, captures the spatial metadata, and stops. The
band-to-variable mapping is BandMapper's job; the engine invocation
is the bridge's job. Separating these mirrors the vector pipeline.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional

import pandas as pd

from .rat_io import RatNotAvailableError, RatReader


@dataclass
class RasterStudyInput:
    """Engine-ready bundle produced by RasterStudyInputLoader.

    Fields:
      raster_layer:   live QgsRasterLayer (or None when tests stub it)
      raster_path:    filesystem path to the raster
      rat_dataframe:  the enriched RAT as a pandas DataFrame
      transform:      Affine-style 6-tuple (a, b, c, d, e, f) or None
      crs_authid:     EPSG-style auth id like "EPSG:4326" or ""
      nodata:         nodata value for the primary band, or None
      width / height: raster dimensions (cells)
      band_count:     number of bands
    """

    raster_path: str
    rat_dataframe: pd.DataFrame
    raster_layer: Any = None
    transform: Optional[tuple] = None
    crs_authid: str = ""
    nodata: Optional[float] = None
    width: int = 0
    height: int = 0
    band_count: int = 1
    metadata: dict = field(default_factory=dict)


class RasterStudyInputLoaderError(RuntimeError):
    """Raised when the loader cannot assemble a complete RasterStudyInput."""


class RasterStudyInputLoader:
    """Open a prepared overlay raster + RAT for engine consumption.

    Usage:
        loader = RasterStudyInputLoader()
        bundle = loader.load(raster_path)  # → RasterStudyInput

    Test seam: pass `layer_opener` to inject a fake that returns a
    duck-typed raster layer instead of a QgsRasterLayer.
    """

    def __init__(
        self,
        *,
        rat_reader: Optional[RatReader] = None,
        layer_opener: Optional[Any] = None,
    ):
        self._rat_reader = rat_reader or RatReader()
        self._layer_opener = layer_opener or _default_layer_opener

    def load(self, raster_path: str) -> RasterStudyInput:
        if not raster_path:
            raise RasterStudyInputLoaderError(
                "raster_path is empty — pass the path produced by RasterOverlay."
            )
        if not Path(raster_path).exists():
            raise RasterStudyInputLoaderError(
                f"Overlay raster not found on disk: {raster_path}"
            )

        layer = self._layer_opener(raster_path)
        if layer is None:
            raise RasterStudyInputLoaderError(
                f"Could not open raster '{raster_path}' as a QgsRasterLayer. "
                f"Check that the GDAL provider is available in QGIS."
            )

        rat_df = self._rat_reader.read(layer, band=1)
        if rat_df is None:
            raise RasterStudyInputLoaderError(
                f"Overlay raster '{raster_path}' has no readable RAT. "
                f"Re-run the raster Create-Study-Area overlay to rebuild it."
            )

        return RasterStudyInput(
            raster_path=raster_path,
            rat_dataframe=rat_df,
            raster_layer=layer,
            transform=_safe_transform(layer),
            crs_authid=_safe_crs_authid(layer),
            nodata=_safe_nodata(layer, band=1),
            width=_safe_int(getattr(layer, "width", lambda: 0)),
            height=_safe_int(getattr(layer, "height", lambda: 0)),
            band_count=_safe_int(getattr(layer, "bandCount", lambda: 1)),
        )


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

def _default_layer_opener(path: str) -> Any:
    """Open a path as a QgsRasterLayer in production; None on import failure."""
    try:
        from qgis.core import QgsRasterLayer  # type: ignore
    except ImportError:
        return None
    layer = QgsRasterLayer(path, Path(path).stem)
    return layer if layer.isValid() else None


def _safe_int(getter) -> int:
    try:
        value = getter() if callable(getter) else getter
        return int(value)
    except Exception:
        return 0


def _safe_transform(layer: Any) -> Optional[tuple]:
    """Pull an affine transform out of a QgsRasterLayer if possible."""
    try:
        extent = layer.extent()
        x_size = layer.rasterUnitsPerPixelX()
        y_size = layer.rasterUnitsPerPixelY()
        return (
            x_size,                      # a: x-scale
            0.0,                         # b: x-skew
            extent.xMinimum(),           # c: x-origin
            0.0,                         # d: y-skew
            -float(y_size),              # e: y-scale (negative — top-down)
            extent.yMaximum(),           # f: y-origin
        )
    except Exception:
        return None


def _safe_crs_authid(layer: Any) -> str:
    try:
        return layer.crs().authid() or ""
    except Exception:
        return ""


def _safe_nodata(layer: Any, band: int) -> Optional[float]:
    try:
        provider = layer.dataProvider()
        if provider.sourceHasNoDataValue(band):
            return float(provider.sourceNoDataValue(band))
    except Exception:
        pass
    return None
