"""Frame extraction and face detection/recognition using face_recognition + OpenCV."""

import json
import re
import subprocess
import sys
import time
import warnings

import cv2
from rich.console import Console
from rich.table import Table

warnings.filterwarnings("ignore", message="pkg_resources is deprecated", category=UserWarning)
import face_recognition  # noqa: E402
import numpy as np  # noqa: E402

from .config import (
    FACE_MATCH_TOLERANCE,
    FACE_PADDING,
    FACE_RECOGNITION_MODEL,
    FACES_DIR,
    FRAME_INTERVAL_SECONDS,
    FRAMES_DIR,
    MIN_FACE_SIZE,
    VIDEO_DIR,
)
from .db import get_db, get_person_encodings, get_untagged_encodings, migrate_db


def detect_faces(frame_rgb):
    """Detect faces and compute encodings using face_recognition.

    Args:
        frame_rgb: RGB numpy array (face_recognition expects RGB, not BGR).

    Returns:
        List of (x, y, w, h, encoding) tuples.
    """
    # face_locations returns list of (top, right, bottom, left) tuples
    locations = face_recognition.face_locations(frame_rgb, model=FACE_RECOGNITION_MODEL)

    # Filter out faces smaller than MIN_FACE_SIZE
    filtered = []
    for top, right, bottom, left in locations:
        w = right - left
        h = bottom - top
        if w >= MIN_FACE_SIZE and h >= MIN_FACE_SIZE:
            filtered.append((top, right, bottom, left))

    if not filtered:
        return []

    # Compute 128-d encodings for each detected face
    encodings = face_recognition.face_encodings(frame_rgb, filtered)

    results = []
    for (top, right, bottom, left), encoding in zip(filtered, encodings, strict=True):
        x = left
        y = top
        w = right - left
        h = bottom - top
        results.append((x, y, w, h, encoding))

    return results


def crop_face(frame, x, y, w, h, padding=FACE_PADDING):
    """Crop a face region with padding from the frame."""
    fh, fw = frame.shape[:2]
    pad_w = int(w * padding)
    pad_h = int(h * padding)

    x1 = max(0, x - pad_w)
    y1 = max(0, y - pad_h)
    x2 = min(fw, x + w + pad_w)
    y2 = min(fh, y + h + pad_h)

    return frame[y1:y2, x1:x2]


def extract_gps(video_path):
    """Extract GPS coordinates from video metadata using ffprobe.

    Returns:
        (lat, lon) tuple or None if no GPS data found.
    """
    try:
        result = subprocess.run(
            [
                "ffprobe",
                "-v",
                "quiet",
                "-print_format",
                "json",
                "-show_format",
                str(video_path),
            ],
            capture_output=True,
            text=True,
            timeout=10,
            check=False,
        )
        if result.returncode != 0:
            return None

        data = json.loads(result.stdout)
        tags = data.get("format", {}).get("tags", {})

        # Check common GPS tag names
        location = (
            tags.get("location")
            or tags.get("com.apple.quicktime.location.ISO6709")
            or tags.get("location-eng")
        )

        if not location:
            return None

        # Parse ISO 6709 format: +DD.DDDD-DDD.DDDD or +DD.DDDD+DDD.DDDD
        # Examples: "+37.7749-122.4194/", "+48.8566+002.3522/"
        m = re.match(r"([+-]\d+\.?\d*?)([+-]\d+\.?\d*)", location)
        if m:
            lat = float(m.group(1))
            lon = float(m.group(2))
            return (lat, lon)

        return None
    except (subprocess.TimeoutExpired, json.JSONDecodeError, ValueError):
        return None


def process_video(video_row, conn, force=False, supplement=False, interval=None):
    """Extract frames and detect faces for a single video."""
    video_id = video_row["id"]
    number = video_row["number"]
    filename = video_row["filename"]
    video_path = VIDEO_DIR / filename
    frame_interval = interval or FRAME_INTERVAL_SECONDS

    if not video_path.exists():
        print(f"  WARNING: {filename} not found, skipping")
        return 0, 0

    # Check if already processed
    if not force and not supplement:
        existing = conn.execute(
            "SELECT COUNT(*) FROM frames WHERE video_id = ?", (video_id,)
        ).fetchone()[0]
        if existing > 0:
            return 0, 0

    # Get existing timestamps to skip in supplement mode
    existing_timestamps = set()
    if supplement:
        rows = conn.execute(
            "SELECT timestamp_seconds FROM frames WHERE video_id = ?", (video_id,)
        ).fetchall()
        existing_timestamps = {int(r["timestamp_seconds"]) for r in rows}

    cap = cv2.VideoCapture(str(video_path))
    if not cap.isOpened():
        print(f"  WARNING: Could not open {filename}")
        return 0, 0

    fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
    total_frames_in_video = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    duration = total_frames_in_video / fps if fps > 0 else 0

    # Update duration in database if not set
    if video_row["duration_seconds"] is None or video_row["duration_seconds"] == 0:
        conn.execute(
            "UPDATE videos SET duration_seconds = ? WHERE id = ?",
            (duration, video_id),
        )

    # Extract GPS metadata
    gps = extract_gps(video_path)
    if gps:
        conn.execute(
            "UPDATE videos SET gps_lat = ?, gps_lon = ? WHERE id = ?",
            (gps[0], gps[1], video_id),
        )

    frame_count = 0
    face_count = 0
    timestamp = 0.0

    num_str = f"{number:03d}"

    while timestamp < duration:
        ts_int = int(timestamp)

        # Skip timestamps that already have frames
        if supplement and ts_int in existing_timestamps:
            timestamp += frame_interval
            continue

        frame_number = int(timestamp * fps)
        cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
        ret, frame = cap.read()
        if not ret:
            break

        # Save frame
        ts_str = f"{ts_int:04d}"
        frame_filename = f"{num_str}_{ts_str}.jpg"
        frame_path = FRAMES_DIR / frame_filename
        cv2.imwrite(str(frame_path), frame, [cv2.IMWRITE_JPEG_QUALITY, 85])

        h, w = frame.shape[:2]
        rel_path = f"frames/{frame_filename}"

        cursor = conn.execute(
            """INSERT INTO frames (video_id, path, timestamp_seconds, width, height)
               VALUES (?, ?, ?, ?, ?)""",
            (video_id, rel_path, timestamp, w, h),
        )
        frame_id = cursor.lastrowid
        frame_count += 1

        # Convert BGR to RGB for face_recognition
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

        # Detect faces and compute encodings
        faces = detect_faces(frame_rgb)
        for idx, (fx, fy, fw, fh, encoding) in enumerate(faces):
            face_crop = crop_face(frame, fx, fy, fw, fh)
            face_filename = f"face_{num_str}_{ts_str}_{idx}.jpg"
            face_path = FACES_DIR / face_filename
            cv2.imwrite(str(face_path), face_crop, [cv2.IMWRITE_JPEG_QUALITY, 90])

            rel_face_path = f"faces/{face_filename}"
            encoding_blob = encoding.tobytes()
            conn.execute(
                """INSERT INTO faces (frame_id, video_id, path, x, y, width, height, encoding)
                   VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
                (frame_id, video_id, rel_face_path, fx, fy, fw, fh, encoding_blob),
            )
            face_count += 1

        timestamp += frame_interval

    cap.release()
    conn.commit()
    return frame_count, face_count


def extract_all(video_number=None, force=False, supplement=False, interval=None):
    """Extract frames from all or a single video."""
    FRAMES_DIR.mkdir(parents=True, exist_ok=True)
    FACES_DIR.mkdir(parents=True, exist_ok=True)

    conn = get_db()
    migrate_db(conn)

    if video_number:
        videos = conn.execute(
            "SELECT * FROM videos WHERE number = ?", (video_number,)
        ).fetchall()
        if not videos:
            print(
                f"Video #{video_number} not found in database."
                " Run 'python3 -m videocatalog.db init' first."
            )
            return
    else:
        videos = conn.execute("SELECT * FROM videos ORDER BY number").fetchall()

    frame_interval = interval or FRAME_INTERVAL_SECONDS
    total_frames = 0
    total_faces = 0
    start_time = time.time()

    print(f"Processing {len(videos)} video(s)...")
    print(f"Frame interval: every {frame_interval} seconds")
    if supplement:
        print("Mode: supplement (skipping existing timestamps)")
    print(f"Face model: {FACE_RECOGNITION_MODEL}")
    print(f"Output: {FRAMES_DIR}")
    print()

    for i, video in enumerate(videos, 1):
        title = video["title"] or video["filename"]
        print(
            f"[{i}/{len(videos)}] #{video['number']:03d} {title}...",
            end=" ",
            flush=True,
        )
        fc, fac = process_video(
            video, conn, force=force, supplement=supplement, interval=interval
        )
        if fc > 0:
            print(f"{fc} frames, {fac} faces")
        else:
            print(
                "(skipped)" if supplement else "(already done)" if not force else "(no frames)"
            )
        total_frames += fc
        total_faces += fac

    elapsed = time.time() - start_time
    print()
    print(f"Done in {elapsed:.0f}s")
    print(f"  Frames extracted: {total_frames}")
    print(f"  Faces detected: {total_faces}")
    print(f"  Frames dir: {FRAMES_DIR}")
    print(f"  Faces dir: {FACES_DIR}")

    conn.close()


def auto_match(tolerance=None):
    """Auto-tag untagged faces by comparing encodings to known people."""
    if tolerance is None:
        tolerance = FACE_MATCH_TOLERANCE

    conn = get_db()
    migrate_db(conn)

    # Load all tagged people and their encodings
    people = conn.execute("SELECT id, name FROM people").fetchall()
    person_encodings = {}
    for person in people:
        encs = get_person_encodings(conn, person["id"])
        if encs:
            person_encodings[person["id"]] = {
                "name": person["name"],
                "encodings": [enc for _, enc in encs],
            }

    if not person_encodings:
        print("No tagged faces with encodings found. Tag some faces in the web UI first.")
        conn.close()
        return

    print(f"Loaded encodings for {len(person_encodings)} people:")
    for _pid, info in person_encodings.items():
        print(f"  {info['name']}: {len(info['encodings'])} face(s)")
    print()

    # Load untagged faces
    untagged = get_untagged_encodings(conn)
    if not untagged:
        print("No untagged faces with encodings found.")
        conn.close()
        return

    print(f"Comparing {len(untagged)} untagged faces (tolerance={tolerance})...")

    matched = 0
    for face_id, face_enc in untagged:
        best_person_id = None
        best_distance = float("inf")

        for person_id, info in person_encodings.items():
            distances = face_recognition.face_distance(info["encodings"], face_enc)
            min_dist = np.min(distances)
            if min_dist < best_distance:
                best_distance = min_dist
                best_person_id = person_id

        if best_distance <= tolerance and best_person_id is not None:
            conn.execute(
                "UPDATE faces SET person_id = ? WHERE id = ?",
                (best_person_id, face_id),
            )
            matched += 1

    # Update video_people links based on newly tagged faces
    conn.execute("""
        INSERT OR IGNORE INTO video_people (video_id, person_id)
        SELECT DISTINCT video_id, person_id FROM faces
        WHERE person_id IS NOT NULL
    """)

    conn.commit()
    remaining = len(untagged) - matched
    print(f"Auto-tagged {matched} faces ({remaining} remain untagged)")
    conn.close()


def recompute_encodings():
    """Compute encodings for face crops that are missing them."""
    conn = get_db()
    migrate_db(conn)

    rows = conn.execute("SELECT id, path FROM faces WHERE encoding IS NULL").fetchall()

    if not rows:
        print("All faces already have encodings.")
        conn.close()
        return

    print(f"Computing encodings for {len(rows)} faces...")
    updated = 0
    for row in rows:
        img_path = FACES_DIR.parent / row["path"]
        img = face_recognition.load_image_file(str(img_path))
        encs = face_recognition.face_encodings(img)
        if encs:
            conn.execute(
                "UPDATE faces SET encoding = ? WHERE id = ?",
                (encs[0].tobytes(), row["id"]),
            )
            updated += 1

    conn.commit()
    print(f"Computed encodings for {updated}/{len(rows)} faces")
    conn.close()


def check_gps():
    """Scan all videos and report GPS metadata findings."""
    conn = get_db()
    migrate_db(conn)

    videos = conn.execute("SELECT * FROM videos ORDER BY number").fetchall()

    console = Console()
    table = Table(title="GPS Metadata Scan")
    table.add_column("#", justify="right", style="bold")
    table.add_column("Title", max_width=40)
    table.add_column("GPS Lat", justify="right")
    table.add_column("GPS Lon", justify="right")
    table.add_column("Status")

    found = 0
    scanned = 0

    for video in videos:
        video_path = VIDEO_DIR / video["filename"]
        if not video_path.exists():
            continue
        scanned += 1

        gps = extract_gps(video_path)
        if gps:
            lat, lon = gps
            conn.execute(
                "UPDATE videos SET gps_lat = ?, gps_lon = ? WHERE id = ?",
                (lat, lon, video["id"]),
            )
            table.add_row(
                str(video["number"]),
                video["title"] or video["filename"],
                f"{lat:.6f}",
                f"{lon:.6f}",
                "[green]Found[/green]",
            )
            found += 1
        else:
            table.add_row(
                str(video["number"]),
                video["title"] or video["filename"],
                "-",
                "-",
                "[dim]None[/dim]",
            )

    conn.commit()
    console.print(table)
    print(f"\nScanned {scanned} videos, found GPS data in {found}")
    conn.close()


def main():
    """Run frame extraction from the command line."""
    video_number = None
    force = False

    args = sys.argv[1:]

    if "--auto-match" in args:
        tolerance = None
        if "--tolerance" in args:
            idx = args.index("--tolerance")
            if idx + 1 < len(args):
                tolerance = float(args[idx + 1])
        auto_match(tolerance=tolerance)
        return

    if "--recompute-encodings" in args:
        recompute_encodings()
        return

    if "--check-gps" in args:
        check_gps()
        return

    supplement = "--supplement" in args
    if supplement:
        args.remove("--supplement")

    interval = None
    if "--interval" in args:
        idx = args.index("--interval")
        if idx + 1 < len(args):
            interval = int(args[idx + 1])
            args.pop(idx + 1)
        args.pop(idx)

    if "--force" in args:
        force = True
        args.remove("--force")
    if args:
        try:
            video_number = int(args[0])
        except ValueError:
            print("Usage: python3 -m videocatalog.extract [video_number] [--force]")
            print(
                "       python3 -m videocatalog.extract"
                " --supplement [--interval 2] [video_number]"
            )
            print("       python3 -m videocatalog.extract --auto-match [--tolerance 0.6]")
            print("       python3 -m videocatalog.extract --recompute-encodings")
            print("       python3 -m videocatalog.extract --check-gps")
            sys.exit(1)

    extract_all(video_number=video_number, force=force, supplement=supplement, interval=interval)


if __name__ == "__main__":
    main()
