from __future__ import annotations

from io import BytesIO
from dataclasses import dataclass

from rights_filter.analysis.preprocessing import ImagePayload

FaceBox = tuple[int, int, int, int]


@dataclass(frozen=True)
class FacePersonSignal:
    face_count: int
    person_count: int
    face_boxes: tuple[FaceBox, ...] = ()

    @property
    def present(self) -> bool:
        return self.face_count > 0 or self.person_count > 0


class HeuristicFacePersonDetector:
    """Presence-only detector used by the standalone implementation.

    Target applications should replace this with their approved local detector or
    internal service. It intentionally emits no identity or embedding data.
    """

    def detect(self, image: ImagePayload) -> FacePersonSignal:
        return self._detect_with_opencv(image.content)

    def _detect_with_opencv(self, content: bytes) -> FacePersonSignal:
        try:
            import cv2
            import numpy as np
        except Exception:
            return FacePersonSignal(face_count=0, person_count=0)

        encoded = np.frombuffer(content, dtype=np.uint8)
        decoded = cv2.imdecode(encoded, cv2.IMREAD_COLOR)
        if decoded is None:
            decoded = _decode_with_pillow(content, cv2, np)
        if decoded is None:
            return FacePersonSignal(face_count=0, person_count=0)

        try:
            gray = cv2.cvtColor(decoded, cv2.COLOR_BGR2GRAY)
        except Exception:
            return FacePersonSignal(face_count=0, person_count=0)

        face_boxes: list[FaceBox] = []
        for cascade_name in (
            "haarcascade_frontalface_default.xml",
            "haarcascade_profileface.xml",
        ):
            cascade = cv2.CascadeClassifier(f"{cv2.data.haarcascades}{cascade_name}")
            if hasattr(cascade, "empty") and cascade.empty():
                continue
            rects = cascade.detectMultiScale(
                gray,
                scaleFactor=1.1,
                minNeighbors=4,
                minSize=(30, 30),
            )
            for rect in rects:
                box = tuple(int(value) for value in rect)
                if len(box) == 4 and not any(
                    _boxes_overlap(box, existing) for existing in face_boxes
                ):
                    face_boxes.append(box)

        face_count = len(face_boxes)
        return FacePersonSignal(
            face_count=face_count,
            person_count=face_count,
            face_boxes=tuple(face_boxes),
        )

def _boxes_overlap(a: FaceBox, b: FaceBox, threshold: float = 0.5) -> bool:
    # Boxes are (x, y, w, h). Treat them as the same physical face when their
    # intersection-over-union meets the threshold, so a face detected by BOTH
    # the frontal and profile cascades is counted once rather than twice.
    ax, ay, aw, ah = a
    bx, by, bw, bh = b
    left = max(ax, bx)
    top = max(ay, by)
    right = min(ax + aw, bx + bw)
    bottom = min(ay + ah, by + bh)
    intersection = max(0, right - left) * max(0, bottom - top)
    if intersection == 0:
        return False
    union = (aw * ah) + (bw * bh) - intersection
    if union <= 0:
        return False
    return intersection / union >= threshold


def _decode_with_pillow(content: bytes, cv2: object, np: object) -> object | None:
    try:
        from PIL import Image

        with Image.open(BytesIO(content)) as image:
            rgb = image.convert("RGB")
            return cv2.cvtColor(np.array(rgb), cv2.COLOR_RGB2BGR)
    except Exception:
        return None
