"""Shared crop primitive for the region/crop pipeline (#114).

One model- and transport-agnostic function sits at the trunk of both crop jobs:
- CCIP characters: a face/figure detector proposes regions → crop → CCIP-embed.
- SigLIP concepts: head-guided / saliency proposes regions → crop → SigLIP-embed.
Only the PROPOSER (where to crop) and the EMBEDDER (what to run) differ; the crop
itself — including the lower-bound size floor below which a region is too small to
embed reliably — is identical, so it lives here and both jobs call it.

The actual detector + embedders run in the GPU agent; this is pure Pillow so it's
importable + testable anywhere (and the agent imports it for the crop step).
"""

from __future__ import annotations

from PIL import Image

# Size floor: a region must be at least this big on its SHORTER edge to be worth
# embedding — a smaller crop is a blurry upscale carrying little real signal, and
# unbounded tiny crops would explode the bag. Expressed as BOTH a fraction of the
# image's short side and an absolute pixel floor; the larger of the two wins.
MIN_CROP_FRACTION = 0.10
MIN_CROP_PX = 64


def _to_pixels(bbox: tuple[float, float, float, float], w: int, h: int):
    """Normalized (x, y, w, h) in [0,1] → pixel (x, y, w, h)."""
    x, y, bw, bh = bbox
    return x * w, y * h, bw * w, bh * h


def crop_region(
    img: Image.Image,
    bbox: tuple[float, float, float, float],
    *,
    pad: float = 0.0,
    min_fraction: float = MIN_CROP_FRACTION,
    min_px: int = MIN_CROP_PX,
    out_size: int | None = None,
) -> Image.Image | None:
    """Crop a NORMALIZED bbox (x, y, w, h in [0,1]) from img.

    - pad: grow the box by this fraction on each side (e.g. 0.15 = +15% context),
      clamped to the image bounds.
    - Returns None when the resulting region is below the size floor (too small to
      embed reliably) — the caller skips embedding it.
    - out_size: if given, resize the crop to out_size×out_size; otherwise return
      the raw crop and let the embedder do its own preprocessing.
    """
    iw, ih = img.size
    px, py, pw, ph = _to_pixels(bbox, iw, ih)

    if pad:
        px -= pw * pad / 2.0
        py -= ph * pad / 2.0
        pw *= (1.0 + pad)
        ph *= (1.0 + pad)

    left = max(0, int(round(px)))
    top = max(0, int(round(py)))
    right = min(iw, int(round(px + pw)))
    bottom = min(ih, int(round(py + ph)))
    if right <= left or bottom <= top:
        return None

    floor = max(min_px, int(min_fraction * min(iw, ih)))
    if min(right - left, bottom - top) < floor:
        return None

    crop = img.crop((left, top, right, bottom)).convert("RGB")
    if out_size:
        crop = crop.resize((out_size, out_size))
    return crop
