"""Convert geographic coordinates to SAM pixel prompts using inverse geotransform."""

import structlog

from domain_types import BBox, LatLng

logger = structlog.get_logger(__name__)


def extent_to_bbox(xmin: float, ymin: float, xmax: float, ymax: float) -> BBox:
    """Convert (xmin, ymin, xmax, ymax) in EPSG:4326 to BBox(ne, sw).

    Note: in EPSG:4326, x=longitude, y=latitude.
    """
    return BBox(ne=LatLng(lat=ymax, lng=xmax), sw=LatLng(lat=ymin, lng=xmin))


def geometries_to_api_prompts(geometries: list[dict]) -> tuple[list[dict], int]:
    """Convert extract_geometries output (in EPSG:4326) to samgis-be API prompt format.

    Phase 1: only point geometries are supported. Polygons are skipped with a warning.

    Args:
        geometries: List from qgis_vector.extract_geometries(vlayer, "EPSG:4326").
            Each is {"type": "point", "coords": (lng, lat)} (x=lng, y=lat in 4326).

    Returns:
        Tuple of (prompts, skipped_count) where prompts is a list of API-format dicts:
            {"type": "point", "data": {"lat": float, "lng": float}, "label": 1}
    """
    prompts = []
    skipped = 0
    for geom in geometries:
        if geom["type"] == "point":
            lng, lat = geom["coords"]
            prompts.append(
                {
                    "type": "point",
                    "data": {"lat": lat, "lng": lng},
                    "label": 1,
                }
            )
        else:
            skipped += 1
            logger.warning(
                "Skipping %s geometry (not supported for remote inference)",
                geom["type"],
            )
    logger.debug(
        "geometries_to_api_prompts: %d prompts, %d skipped",
        len(prompts),
        skipped,
    )
    return prompts, skipped


def geo_to_sam_prompts(
    geometries: list[dict],
    geotransform: tuple[float, ...],
    raster_width: int,
    raster_height: int,
) -> list[dict]:
    """Convert geographic geometries to SAM prompt format in pixel coordinates.

    Args:
        geometries: List from qgis_vector.extract_geometries().
            Each is {"type": "point", "coords": (x, y)} or
                    {"type": "polygon", "bbox": (xmin, ymin, xmax, ymax)}.
        geotransform: 6-coefficient GDAL geotransform
            (origin_x, pixel_w, 0, origin_y, 0, -pixel_h).
        raster_width: Raster width in pixels.
        raster_height: Raster height in pixels.

    Returns:
        List of SAM prompts:
            {"type": "point", "data": [col, row], "label": 1} or
            {"type": "rectangle", "data": [col1, row1, col2, row2], "label": 0}

    SAM label convention:
        - Points: 1 = foreground (include), 0 = background (exclude)
        - Rectangles: label is ignored by SAM (hardcoded to [2, 3] internally)
    """
    origin_x, pixel_w, _, origin_y, _, pixel_h = geotransform
    logger.debug(
        "geo_to_sam_prompts: geotransform=%s, raster=%dx%d, geometries=%d",
        geotransform,
        raster_width,
        raster_height,
        len(geometries),
    )
    prompts = []

    for geom in geometries:
        if geom["type"] == "point":
            x, y = geom["coords"]
            col, row = _geo_to_pixel(x, y, origin_x, origin_y, pixel_w, pixel_h)
            logger.debug("Point geo=(%.6f, %.6f) -> pixel=(%d, %d)", x, y, col, row)

            if not _in_bounds(col, row, raster_width, raster_height):
                logger.warning(
                    "Skipping out-of-bounds point at pixel (%d, %d)", col, row
                )
                continue

            verify_x, verify_y = _pixel_to_geo(
                col, row, origin_x, origin_y, pixel_w, pixel_h
            )
            logger.debug(
                "Point verification: pixel=(%d,%d) -> geo=(%.6f,%.6f)"
                " (input was %.6f,%.6f)",
                col,
                row,
                verify_x,
                verify_y,
                x,
                y,
            )
            label = geom.get("label", 1)
            prompts.append({"type": "point", "data": [col, row], "label": label})

        elif geom["type"] == "polygon":
            xmin, ymin, xmax, ymax = geom["bbox"]
            col1, row1 = _geo_to_pixel(xmin, ymax, origin_x, origin_y, pixel_w, pixel_h)
            col2, row2 = _geo_to_pixel(xmax, ymin, origin_x, origin_y, pixel_w, pixel_h)

            # Ensure correct ordering (top-left, bottom-right)
            c1, c2 = min(col1, col2), max(col1, col2)
            r1, r2 = min(row1, row2), max(row1, row2)
            logger.debug(
                "Polygon bbox=(%.6f,%.6f,%.6f,%.6f) -> pixel=(%d,%d)-(%d,%d)",
                xmin,
                ymin,
                xmax,
                ymax,
                c1,
                r1,
                c2,
                r2,
            )

            # Clamp to raster extent (polygon may exceed raster bounds)
            c1, r1, c2, r2 = _clamp_rectangle(
                c1, r1, c2, r2, raster_width, raster_height
            )
            if c1 is None:
                logger.warning(
                    "Skipping entirely out-of-bounds rectangle for"
                    " bbox=(%.6f,%.6f,%.6f,%.6f)",
                    xmin,
                    ymin,
                    xmax,
                    ymax,
                )
                continue

            logger.debug("Clamped rectangle -> pixel=(%d,%d)-(%d,%d)", c1, r1, c2, r2)
            prompts.append({"type": "rectangle", "data": [c1, r1, c2, r2], "label": 0})

    logger.debug(
        "Generated %d prompts from %d geometries", len(prompts), len(geometries)
    )
    return prompts


def _geo_to_pixel(
    x: float,
    y: float,
    origin_x: float,
    origin_y: float,
    pixel_w: float,
    pixel_h: float,
) -> tuple[int, int]:
    """Convert geographic (x, y) to pixel (col, row) using inverse geotransform."""
    col = int((x - origin_x) / pixel_w)
    row = int((y - origin_y) / pixel_h)
    return col, row


def _pixel_to_geo(
    col: int,
    row: int,
    origin_x: float,
    origin_y: float,
    pixel_w: float,
    pixel_h: float,
) -> tuple[float, float]:
    """Convert pixel (col, row) back to geographic (x, y) — inverse of _geo_to_pixel."""
    x = origin_x + col * pixel_w
    y = origin_y + row * pixel_h
    return x, y


def _in_bounds(col: int, row: int, width: int, height: int) -> bool:
    """Check if a point is within raster bounds (exclusive upper bound)."""
    return 0 <= col < width and 0 <= row < height


def _clamp_rectangle(
    c1: int,
    r1: int,
    c2: int,
    r2: int,
    width: int,
    height: int,
) -> tuple[int | None, int | None, int | None, int | None]:
    """Clamp rectangle to raster bounds. Returns (None,...) if entirely outside."""
    # Entirely outside?
    if c2 < 0 or r2 < 0 or c1 >= width or r1 >= height:
        return None, None, None, None

    c1 = max(
        0, min(c1, width - 1)
    )  # pragma: no mutate (early return guards c1 >= width)
    r1 = max(
        0, min(r1, height - 1)
    )  # pragma: no mutate (early return guards r1 >= height)
    c2 = max(0, min(c2, width - 1))  # pragma: no mutate (early return guards c2 < 0)
    r2 = max(0, min(r2, height - 1))  # pragma: no mutate (early return guards r2 < 0)
    return c1, r1, c2, r2
