import os
from collections import defaultdict
from io import BytesIO

from PIL import Image, UnidentifiedImageError

from app.aws_services import AWSService
from app.email_service import send_email_to_user
from models.user_model import (
    create_match,
    get_all_event_images,
    get_user_by_face_id,
    get_site_assets,
    is_delivery_logged,
    log_email_deliveries,
)


SIMILARITY_THRESHOLD = float(os.getenv("FACE_MATCH_THRESHOLD", "95"))
FACE_PADDING_RATIO = 0.2
MIN_FACE_SIZE_PIXELS = 40


class FaceMatcher:
    """Run Rekognition on each detected face in processed event images."""

    def __init__(self, aws_service=None, similarity_threshold=SIMILARITY_THRESHOLD):
        self.aws_service = aws_service or AWSService()
        self.similarity_threshold = similarity_threshold

    def send_grouped_matches(self):
        print("\nStarting face matching process...\n")
        images = get_all_event_images()

        if not images:
            print("No event images found in DB.")
            return {}

        deliveries_by_email = defaultdict(
            lambda: {
                "user_id": None,
                "event_image_ids": [],
                "user_face_image": None,
            }
        )
        site_assets = get_site_assets()
        header_image_url = site_assets["header"]["image_path"] if site_assets["header"] else ""

        for image in images:
            self._process_single_image(image, deliveries_by_email)

        print("\nSending emails...\n")

        delivery_summary = {}
        for email, payload in deliveries_by_email.items():
            event_image_ids = payload["event_image_ids"]
            user_id = payload["user_id"]
            user_face_image = payload["user_face_image"]

            if not user_id or not user_face_image:
                continue

            try:
                send_email_to_user(email, user_id, user_face_image, header_image_url)
                log_email_deliveries(user_id, event_image_ids)
                delivery_summary[email] = {
                    "user_id": user_id,
                    "user_face_image": user_face_image,
                }
                print(f"Email sent to {email}")
            except Exception as exc:
                print(f"Email failed for {email}: {exc}")

        print("\nMatching + Email process completed.\n")
        return delivery_summary

    def _process_single_image(self, image, deliveries_by_email):
        photo_id = image["id"]
        processed_s3_key = image.get("processed_s3_key")
        if not processed_s3_key:
            print(f"Skipping image {photo_id}: processed_s3_key is missing.")
            return

        print(f"Processing image for Rekognition: {processed_s3_key}")

        try:
            processed_bytes = self.aws_service.download_file_bytes(processed_s3_key)
            face_details = self.aws_service.detect_faces(processed_s3_key)
        except Exception as exc:
            print(f"Error preparing image {photo_id}: {exc}")
            return

        if not face_details:
            print("No faces detected in image.")
            return

        best_match = self._find_best_match_from_faces(processed_bytes, face_details)
        if not best_match:
            print("No face matches found.")
            return

        similarity = float(best_match["Similarity"])
        face_id = best_match["Face"]["FaceId"]
        user = get_user_by_face_id(face_id)
        if not user:
            print("No user found for FaceId.")
            return

        user_id = user["id"]
        email = user["email"]

        create_match(user_id, photo_id, similarity)

        if is_delivery_logged(user_id, photo_id):
            print(f"Skipping already delivered image {photo_id} for {email}.")
            return

        payload = deliveries_by_email[email]
        payload["user_id"] = user_id
        payload["user_face_image"] = user.get("s3_image_url")
        if photo_id not in payload["event_image_ids"]:
            payload["event_image_ids"].append(photo_id)

        print(f"Match found for {email} ({similarity:.2f}%).")

    def _find_best_match_from_faces(self, processed_bytes, face_details):
        try:
            with Image.open(BytesIO(processed_bytes)) as image:
                image = image.convert("RGB")
                best_match = None

                for index, face_detail in enumerate(face_details, start=1):
                    face_crop = self._crop_face(image, face_detail.get("BoundingBox", {}))
                    if face_crop is None:
                        continue

                    try:
                        matches = self.aws_service.search_face_by_bytes(
                            face_crop,
                            face_match_threshold=self.similarity_threshold,
                            max_faces=1,
                        )
                    except Exception as exc:
                        print(f"Face crop {index} search failed: {exc}")
                        continue

                    if not matches:
                        continue

                    candidate = matches[0]
                    if candidate["Similarity"] < self.similarity_threshold:
                        continue

                    if best_match is None or candidate["Similarity"] > best_match["Similarity"]:
                        best_match = candidate

                return best_match
        except (UnidentifiedImageError, OSError) as exc:
            print(f"Failed to open processed image for cropping: {exc}")
            return None

    def _crop_face(self, image, bounding_box):
        if not bounding_box:
            return None

        image_width, image_height = image.size
        left = int(bounding_box.get("Left", 0) * image_width)
        top = int(bounding_box.get("Top", 0) * image_height)
        width = int(bounding_box.get("Width", 0) * image_width)
        height = int(bounding_box.get("Height", 0) * image_height)

        if width < MIN_FACE_SIZE_PIXELS or height < MIN_FACE_SIZE_PIXELS:
            return None

        horizontal_padding = int(width * FACE_PADDING_RATIO)
        vertical_padding = int(height * FACE_PADDING_RATIO)

        crop_left = max(0, left - horizontal_padding)
        crop_top = max(0, top - vertical_padding)
        crop_right = min(image_width, left + width + horizontal_padding)
        crop_bottom = min(image_height, top + height + vertical_padding)

        if crop_right <= crop_left or crop_bottom <= crop_top:
            return None

        face_image = image.crop((crop_left, crop_top, crop_right, crop_bottom))
        buffer = BytesIO()
        face_image.save(buffer, format="JPEG", quality=90)
        return buffer.getvalue()


def process_matching():
    """Backward-compatible entrypoint."""
    matcher = FaceMatcher()
    return matcher.send_grouped_matches()


if __name__ == "__main__":
    process_matching()
