#!/opt/homebrew/bin/python3.11
"""
Pure OpenCV Stamp Search - No neural networks, very fast
Uses: skin detection (hands) + bright regions (paper/envelopes) + small rectangle detection (stamps)
"""

import os
import cv2
import json
import time
import numpy as np

UUID = "384b0ff44aaaa1f1"
VIDEO_PATH = f"output/{UUID}/{UUID}.mp4"
OUTPUT_DIR = f"output/{UUID}/opencv_stamp_search"
os.makedirs(OUTPUT_DIR, exist_ok=True)
CROPS_DIR = os.path.join(OUTPUT_DIR, "crops")
os.makedirs(CROPS_DIR, exist_ok=True)

FRAME_INTERVAL = 5
print("=" * 60)
print("⚡ Pure OpenCV Stamp Search")
print("=" * 60)

cap = cv2.VideoCapture(VIDEO_PATH)
fps = cap.get(cv2.CAP_PROP_FPS)
total_sec = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) / fps)
print(f"📹 Video: {total_sec}s ({total_sec // 60} min), {fps:.1f} fps")


def find_stamps_pure_opencv(frame):
    """
    Find stamps using only OpenCV:
    1. Find hands via skin color
    2. Find paper/envelopes via bright rectangular regions
    3. In those areas, look for small rectangles with complex patterns
    """
    h, w = frame.shape[:2]
    results = []

    # Collect container regions
    containers = []

    # 1. Skin detection (hands)
    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    skin_mask = cv2.inRange(hsv, np.array([0, 20, 60]), np.array([25, 180, 255]))
    skin_mask += cv2.inRange(hsv, np.array([160, 20, 60]), np.array([179, 180, 255]))

    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9))
    skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel)
    skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel)

    contours, _ = cv2.findContours(
        skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
    )
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if 1500 < area < h * w * 0.35:
            x, y, cw, ch = cv2.boundingRect(cnt)
            containers.append(
                {
                    "type": "hand",
                    "bbox": [
                        max(0, x - 50),
                        max(0, y - 50),
                        min(w, x + cw + 50),
                        min(h, y + ch + 50),
                    ],
                }
            )

    # 2. Bright rectangular regions (paper/envelope)
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    _, bright = cv2.threshold(gray, 175, 255, cv2.THRESH_BINARY)
    kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
    bright = cv2.morphologyEx(bright, cv2.MORPH_CLOSE, kernel_rect)

    contours, _ = cv2.findContours(bright, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    for cnt in contours:
        area = cv2.contourArea(cnt)
        if 3000 < area < h * w * 0.5:
            x, y, cw, ch = cv2.boundingRect(cnt)
            aspect = cw / ch if ch > 0 else 0
            if 0.2 < aspect < 4.0:
                containers.append(
                    {
                        "type": "paper",
                        "bbox": [
                            max(0, x - 40),
                            max(0, y - 40),
                            min(w, x + cw + 40),
                            min(h, y + ch + 40),
                        ],
                    }
                )

    if not containers:
        return results

    # 3. In each container, search for small stamps
    for container in containers:
        cx1, cy1, cx2, cy2 = container["bbox"]
        region = frame[cy1:cy2, cx1:cx2]

        if region.size == 0:
            continue

        rh, rw = region.shape[:2]
        region_gray = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY)

        # Find small rectangular shapes (15-120px) that could be stamps
        # Use Canny edge detection
        edges = cv2.Canny(region_gray, 50, 150)
        contours_s, _ = cv2.findContours(
            edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )

        for cnt in contours_s:
            area = cv2.contourArea(cnt)
            if 200 < area < 15000:  # Small objects
                x, y, sw, sh = cv2.boundingRect(cnt)
                aspect = sw / sh if sh > 0 else 0

                # Stamp-like aspect ratios
                if 0.4 < aspect < 2.5 and 15 < sw < 120 and 15 < sh < 120:
                    # Check complexity: stamps have patterns, not solid colors
                    roi = region_gray[y : y + sh, x : x + sw]
                    if roi.size == 0:
                        continue

                    # Variance indicates pattern/texture
                    variance = np.var(roi)
                    if variance < 50:
                        continue  # Too uniform, probably not a stamp

                    # Check for color diversity (stamps usually have multiple colors)
                    roi_color = region[y : y + sh, x : x + sw]
                    roi_hsv = cv2.cvtColor(roi_color, cv2.COLOR_BGR2HSV)

                    # Count distinct hue values
                    hue_vals = roi_hsv[:, :, 0]
                    unique_hues = len(np.unique(hue_vals))

                    # Calculate saturation (stamps usually have color)
                    sat_mean = np.mean(roi_hsv[:, :, 1])

                    # Score: higher variance + more colors = more likely a stamp
                    score = min(
                        1.0, (variance / 500 + unique_hues / 50 + sat_mean / 200) / 3
                    )

                    if score > 0.15:  # Threshold
                        # Map back to original frame
                        ox1 = cx1 + x
                        oy1 = cy1 + y
                        ox2 = cx1 + x + sw
                        oy2 = cy1 + y + sh

                        crop = frame[oy1:oy2, ox1:ox2]
                        if crop.size == 0:
                            continue

                        results.append(
                            {
                                "timestamp": 0,  # Will be set by caller
                                "container": container["type"],
                                "stamp_term": "opencv_rect",
                                "score": score,
                                "bbox": [ox1, oy1, ox2, oy2],
                                "size": [sw, sh],
                                "variance": float(variance),
                                "unique_hues": int(unique_hues),
                                "saturation": float(sat_mean),
                            }
                        )

    return results


all_results = []
start_time = time.time()

for sec in range(0, total_sec, FRAME_INTERVAL):
    cap.set(cv2.CAP_PROP_POS_MSEC, sec * 1000)
    ret, frame = cap.read()
    if not ret:
        continue

    elapsed = time.time() - start_time
    progress = sec / total_sec * 100
    eta = (
        (elapsed / (sec / FRAME_INTERVAL + 1))
        * (total_sec / FRAME_INTERVAL - sec / FRAME_INTERVAL - 1)
        if sec > 0
        else 0
    )

    results = find_stamps_pure_opencv(frame)

    # Set timestamp
    for r in results:
        r["timestamp"] = sec

    if results:
        print(
            f"  [{sec}s | {progress:.0f}% | ETA:{eta:.0f}s] Found {len(results)} candidates"
        )

        for r in results:
            ox1, oy1, ox2, oy2 = r["bbox"]
            crop = frame[oy1:oy2, ox1:ox2]
            if crop.size > 0:
                crop_name = f"stamp_{sec}s_{r['container']}_{r['score']:.2f}.jpg"
                cv2.imwrite(os.path.join(CROPS_DIR, crop_name), crop)

                cv2.rectangle(frame, (ox1, oy1), (ox2, oy2), (0, 255, 0), 2)
                cv2.putText(
                    frame,
                    f"{r['score']:.2f}",
                    (ox1, oy1 - 5),
                    cv2.FONT_HERSHEY_SIMPLEX,
                    0.5,
                    (0, 255, 0),
                    1,
                )

        ann_path = os.path.join(OUTPUT_DIR, f"annotated_{sec}s.jpg")
        cv2.imwrite(ann_path, frame)
        all_results.extend(results)
    else:
        if sec % 120 == 0:
            print(
                f"  [{sec // 60}min/{total_sec // 60}min | {progress:.0f}% | ETA:{eta:.0f}s] Scanning..."
            )

cap.release()

# Sort and deduplicate
all_results.sort(key=lambda x: x["score"], reverse=True)
seen = set()
unique = []
for r in all_results:
    ts = r["timestamp"]
    if ts not in seen:
        seen.add(ts)
        unique.append(r)

print(f"\n{'=' * 60}")
print(f"📊 Found {len(unique)} stamp candidates")
for r in unique[:20]:
    print(
        f"  🎯 {r['timestamp']}s | score:{r['score']:.2f} | via:{r['container']} | size:{r['size'][0]}x{r['size'][1]} | var:{r['variance']:.0f} hues:{r['unique_hues']}"
    )

with open(os.path.join(OUTPUT_DIR, "results.json"), "w") as f:
    json.dump(unique, f, indent=2)

print(f"\n🏁 Done. Crops: {CROPS_DIR}")
