"""
_qr_detector.py — QR code detection helper using cv2.QRCodeDetector (no ROS2 deps).

Exposes two public functions:

  detect_qr(bgr)        → list of RawQR namedtuples
  deduplicate(results, cache, cooldown_s)  → (new_results, updated_cache)

RawQR
-----
  data    : str   — decoded text
  corners : list[float]  — [x0,y0, x1,y1, x2,y2, x3,y3] (pixel coords)
  cx, cy  : float — centroid (pixels)

Deduplication
-------------
The cache maps decoded data → last_seen_monotonic timestamp.  A result is
considered a duplicate if the same data was published within cooldown_s seconds.
"""

from __future__ import annotations

from typing import Dict, List, NamedTuple, Tuple
import time


class RawQR(NamedTuple):
    data:    str
    corners: List[float]  # [x0,y0, x1,y1, x2,y2, x3,y3]
    cx:      float
    cy:      float


def detect_qr(bgr) -> List[RawQR]:
    """
    Run cv2.QRCodeDetector on a BGR image.

    Parameters
    ----------
    bgr : np.ndarray (H, W, 3) uint8

    Returns
    -------
    List of RawQR, one per decoded QR code found.  Empty list if none found.
    """
    import cv2
    import numpy as np

    detector = _get_detector()
    # detectAndDecodeMulti returns (retval, decoded_info, points, straight_qrcode)
    # points shape: (N, 4, 2)  — 4 corners per QR code
    try:
        ok, decoded_list, points, _ = detector.detectAndDecodeMulti(bgr)
    except cv2.error:
        return []

    if not ok or points is None:
        return []

    results: List[RawQR] = []
    for data, quad in zip(decoded_list, points):
        if not data:          # empty string = decode failure
            continue
        flat = quad.reshape(-1).tolist()   # [x0,y0, x1,y1, x2,y2, x3,y3]
        cx   = float(np.mean(quad[:, 0]))
        cy   = float(np.mean(quad[:, 1]))
        results.append(RawQR(data=data, corners=flat, cx=cx, cy=cy))

    return results


def deduplicate(
    results:    List[RawQR],
    cache:      Dict[str, float],
    cooldown_s: float,
    now:        float | None = None,
) -> Tuple[List[RawQR], Dict[str, float]]:
    """
    Filter results to those whose data has not been seen within cooldown_s.

    Parameters
    ----------
    results    : raw detections from detect_qr()
    cache      : {data: last_published_monotonic_time} — mutated in-place
    cooldown_s : minimum seconds between re-publishing the same data
    now        : current time.monotonic() — defaults to time.monotonic()

    Returns
    -------
    (fresh_results, cache)  — fresh_results contains only newly-seen QRs
    """
    if now is None:
        now = time.monotonic()

    fresh: List[RawQR] = []
    for r in results:
        last = cache.get(r.data, -float('inf'))
        if now - last >= cooldown_s:
            fresh.append(r)
            cache[r.data] = now

    # Evict entries older than 10× cooldown to prevent unbounded growth
    evict_before = now - 10 * cooldown_s
    stale_keys   = [k for k, t in cache.items() if t < evict_before]
    for k in stale_keys:
        del cache[k]

    return fresh, cache


# ── Module-level singleton detector (avoids re-creating on every call) ────────

_detector = None


def _get_detector():
    global _detector
    if _detector is None:
        import cv2
        _detector = cv2.QRCodeDetector()
    return _detector
