from __future__ import annotations

import logging
import re
import sqlite3
import time
from pathlib import Path

from muleline.config import FACE_CROP_DIR, FACE_MATCH_THRESHOLD
from muleline.db import get_db

log = logging.getLogger(__name__)

MAX_IMAGE_PIXELS = 25_000_000  # ~25MP, prevents OOM on huge images

_SAFE_CROP_RE = re.compile(r"^face_\d+_\d+\.webp$")


def detect_faces(image_path: Path) -> list[dict]:
    try:
        import face_recognition
    except ImportError:
        log.error("face_recognition library not installed")
        return []

    try:
        from PIL import Image
        with Image.open(image_path) as img:
            w, h = img.size
            if w * h > MAX_IMAGE_PIXELS:
                log.warning("Image too large for face detection (%dx%d): %s", w, h, image_path)
                return []
    except Exception:
        pass

    try:
        image = face_recognition.load_image_file(str(image_path))
    except Exception as e:
        log.error("Failed to load image %s: %s", image_path, e)
        return []

    try:
        locations = face_recognition.face_locations(image, model="hog")
    except Exception as e:
        log.error("face_locations failed for %s: %s", image_path, e)
        return []

    if not locations:
        return []

    try:
        encodings = face_recognition.face_encodings(image, locations)
    except Exception as e:
        log.error("face_encodings failed for %s: %s", image_path, e)
        return []

    results = []
    for (top, right, bottom, left), encoding in zip(locations, encodings):
        results.append({
            "bbox": {
                "x": left,
                "y": top,
                "w": right - left,
                "h": bottom - top,
            },
            "encoding": encoding,
            "confidence": 1.0,
        })
    return results


def match_face(
    encoding,
    known_encodings: list[tuple[int, object]],
    threshold: float = FACE_MATCH_THRESHOLD,
) -> tuple[int | None, float]:
    if not known_encodings:
        return None, float("inf")

    try:
        import face_recognition
        import numpy as np
    except ImportError:
        return None, float("inf")

    person_ids = [pid for pid, _ in known_encodings]
    enc_array = [enc for _, enc in known_encodings]

    distances = face_recognition.face_distance(enc_array, encoding)
    best_idx = int(np.argmin(distances))
    best_dist = float(distances[best_idx])

    if best_dist <= threshold:
        return person_ids[best_idx], best_dist
    return None, best_dist


def load_known_encodings(conn: sqlite3.Connection, owner_id: str) -> list[tuple[int, object]]:
    rows = conn.execute(
        """SELECT fe.person_id, fe.embedding
           FROM face_embeddings fe
           JOIN known_people kp ON fe.person_id = kp.id
           WHERE kp.owner_id = ? AND fe.person_id IS NOT NULL""",
        (owner_id,),
    ).fetchall()

    result = []
    for row in rows:
        try:
            import numpy as np
            enc = np.frombuffer(row["embedding"], dtype=np.float64)
            result.append((row["person_id"], enc))
        except Exception as e:
            log.warning("Failed to deserialize embedding for person %s: %s", row["person_id"], e)
    return result


def generate_face_crop(image_path: Path, bbox: dict, item_id: int, face_idx: int) -> str | None:
    FACE_CROP_DIR.mkdir(parents=True, exist_ok=True)

    try:
        from PIL import Image, ImageOps
        img = Image.open(image_path)
        img = ImageOps.exif_transpose(img)
        img_w, img_h = img.size

        x, y, w, h = bbox["x"], bbox["y"], bbox["w"], bbox["h"]
        pad_x = int(w * 0.30)
        pad_y = int(h * 0.30)

        x1 = max(0, x - pad_x)
        y1 = max(0, y - pad_y)
        x2 = min(img_w, x + w + pad_x)
        y2 = min(img_h, y + h + pad_y)

        crop = img.crop((x1, y1, x2, y2))
        crop = crop.resize((150, 150), Image.LANCZOS)

        filename = f"face_{item_id}_{face_idx}.webp"
        crop.save(FACE_CROP_DIR / filename, "WEBP", quality=90)
        return filename
    except Exception as e:
        log.error("Face crop failed for item %s face %s: %s", item_id, face_idx, e)
        return None


def cluster_unknown_faces(
    embeddings: list[tuple[int, object]],
    distance_threshold: float = 0.5,
) -> dict[int, int]:
    if len(embeddings) < 2:
        return {}

    try:
        from sklearn.cluster import AgglomerativeClustering
        import numpy as np
    except ImportError:
        log.error("sklearn not installed — cannot cluster faces")
        return {}

    face_ids = [fid for fid, _ in embeddings]
    X = np.array([enc for _, enc in embeddings])

    model = AgglomerativeClustering(
        n_clusters=None,
        distance_threshold=distance_threshold,
        metric="euclidean",
        linkage="average",
    )
    labels = model.fit_predict(X)
    return {face_id: int(label) for face_id, label in zip(face_ids, labels)}


def process_item_faces(
    item_id: int,
    image_path: Path,
    conn: sqlite3.Connection,
    owner_id: str,
) -> dict:
    summary = {"faces_found": 0, "faces_matched": 0, "matches": []}

    try:
        faces = detect_faces(image_path)
        summary["faces_found"] = len(faces)

        if not faces:
            return summary

        known_encodings = load_known_encodings(conn, owner_id)
        now = time.time()

        for idx, face in enumerate(faces):
            bbox = face["bbox"]
            encoding = face["encoding"]
            confidence = face["confidence"]

            crop_filename = generate_face_crop(image_path, bbox, item_id, idx) or ""

            person_id, distance = match_face(encoding, known_encodings)

            if person_id is not None:
                person_row = conn.execute(
                    "SELECT name FROM known_people WHERE id = ?", (person_id,)
                ).fetchone()
                person_name = person_row["name"] if person_row else ""
                summary["faces_matched"] += 1
                summary["matches"].append({
                    "person_id": person_id,
                    "person_name": person_name,
                    "distance": distance,
                })

            conn.execute(
                """INSERT INTO face_embeddings
                   (item_id, person_id, bbox_x, bbox_y, bbox_w, bbox_h,
                    embedding, confidence, crop_filename, cluster_id, source, created_at)
                   VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
                (
                    item_id,
                    person_id,
                    bbox["x"], bbox["y"], bbox["w"], bbox["h"],
                    encoding.tobytes(),
                    confidence,
                    crop_filename,
                    None,
                    "auto",
                    now,
                ),
            )

            if person_id is not None:
                existing = conn.execute(
                    "SELECT id FROM item_people WHERE item_id = ? AND person_id = ?",
                    (item_id, person_id),
                ).fetchone()
                if not existing:
                    conn.execute(
                        "INSERT INTO item_people (item_id, person_id, source, created_at) VALUES (?,?,?,?)",
                        (item_id, person_id, "face_recognition", now),
                    )

        conn.commit()
    except Exception as e:
        log.error("process_item_faces failed for item %s: %s", item_id, e)

    return summary


def delete_item_faces(item_id: int, conn: sqlite3.Connection):
    rows = conn.execute(
        "SELECT crop_filename FROM face_embeddings WHERE item_id = ?",
        (item_id,),
    ).fetchall()

    conn.execute("DELETE FROM face_embeddings WHERE item_id = ?", (item_id,))
    conn.commit()

    resolved_dir = FACE_CROP_DIR.resolve()
    for row in rows:
        crop = row["crop_filename"]
        if crop and _SAFE_CROP_RE.match(crop):
            path = FACE_CROP_DIR / crop
            if path.resolve().is_relative_to(resolved_dir) and path.exists():
                try:
                    path.unlink()
                except Exception as e:
                    log.warning("Failed to delete face crop %s: %s", path, e)
