"""
Face Recognition Service
Face detection, verification, and recognition
"""
import logging
from typing import List, Dict, Any, Optional, Tuple
from io import BytesIO
import numpy as np
from PIL import Image
import cv2

logger = logging.getLogger(__name__)


class FaceService:
    """Face detection and recognition"""

    def __init__(self):
        self.face_cascade = None
        self.face_recognition_available = False
        self._init_detectors()

    def _init_detectors(self):
        """Initialize face detection models"""
        # OpenCV Haar Cascade
        try:
            cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
            self.face_cascade = cv2.CascadeClassifier(cascade_path)
            logger.info("Face cascade loaded")
        except Exception as e:
            logger.warning(f"Face cascade not loaded: {e}")

        # Face recognition library
        try:
            import face_recognition
            self.face_recognition = face_recognition
            self.face_recognition_available = True
            logger.info("Face recognition library available")
        except ImportError:
            logger.warning("face_recognition library not available")

    async def detect_faces(
        self,
        image_bytes: bytes,
        return_landmarks: bool = False
    ) -> Dict[str, Any]:
        """
        Detect faces in an image

        Args:
            image_bytes: Image data
            return_landmarks: Whether to return facial landmarks

        Returns:
            Face detection results
        """
        image = Image.open(BytesIO(image_bytes))
        image_np = np.array(image)

        faces = []

        if self.face_recognition_available:
            faces = await self._detect_with_face_recognition(image_np, return_landmarks)
        elif self.face_cascade is not None:
            faces = await self._detect_with_cascade(image_np)

        return {
            "face_count": len(faces),
            "faces": faces,
            "image_size": {"width": image.width, "height": image.height}
        }

    async def _detect_with_face_recognition(
        self,
        image: np.ndarray,
        return_landmarks: bool
    ) -> List[Dict]:
        """Detect faces using face_recognition library"""
        # Convert RGB to BGR (face_recognition expects RGB)
        if len(image.shape) == 3 and image.shape[2] == 3:
            rgb_image = image
        else:
            rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

        # Detect face locations
        face_locations = self.face_recognition.face_locations(rgb_image)

        faces = []
        for i, (top, right, bottom, left) in enumerate(face_locations):
            face_data = {
                "id": i + 1,
                "bbox": {
                    "x": left,
                    "y": top,
                    "width": right - left,
                    "height": bottom - top
                },
                "confidence": 0.95  # face_recognition doesn't provide confidence
            }

            if return_landmarks:
                landmarks = self.face_recognition.face_landmarks(
                    rgb_image,
                    face_locations=[(top, right, bottom, left)]
                )
                if landmarks:
                    face_data["landmarks"] = landmarks[0]

            faces.append(face_data)

        return faces

    async def _detect_with_cascade(self, image: np.ndarray) -> List[Dict]:
        """Detect faces using Haar Cascade"""
        if len(image.shape) == 3:
            gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
        else:
            gray = image

        detected = self.face_cascade.detectMultiScale(
            gray,
            scaleFactor=1.1,
            minNeighbors=5,
            minSize=(30, 30)
        )

        faces = []
        for i, (x, y, w, h) in enumerate(detected):
            faces.append({
                "id": i + 1,
                "bbox": {
                    "x": int(x),
                    "y": int(y),
                    "width": int(w),
                    "height": int(h)
                },
                "confidence": 0.8
            })

        return faces

    async def get_face_encoding(self, image_bytes: bytes) -> Dict[str, Any]:
        """
        Get face encoding for a single face image

        Args:
            image_bytes: Image containing a face

        Returns:
            Face encoding
        """
        if not self.face_recognition_available:
            return {"error": "Face recognition not available"}

        image = Image.open(BytesIO(image_bytes))
        image_np = np.array(image)

        # Get face locations
        face_locations = self.face_recognition.face_locations(image_np)

        if not face_locations:
            return {"error": "No face detected"}

        if len(face_locations) > 1:
            return {"error": "Multiple faces detected, please provide single face image"}

        # Get encoding
        encodings = self.face_recognition.face_encodings(image_np, face_locations)

        if encodings:
            return {
                "encoding": encodings[0].tolist(),
                "face_location": {
                    "top": face_locations[0][0],
                    "right": face_locations[0][1],
                    "bottom": face_locations[0][2],
                    "left": face_locations[0][3]
                }
            }

        return {"error": "Could not generate face encoding"}

    async def verify_faces(
        self,
        image1_bytes: bytes,
        image2_bytes: bytes,
        tolerance: float = 0.6
    ) -> Dict[str, Any]:
        """
        Verify if two images contain the same person

        Args:
            image1_bytes: First face image
            image2_bytes: Second face image
            tolerance: Distance threshold (lower = stricter)

        Returns:
            Verification result
        """
        if not self.face_recognition_available:
            return {"error": "Face recognition not available"}

        # Get encodings for both images
        encoding1_result = await self.get_face_encoding(image1_bytes)
        encoding2_result = await self.get_face_encoding(image2_bytes)

        if "error" in encoding1_result:
            return {"error": f"Image 1: {encoding1_result['error']}"}
        if "error" in encoding2_result:
            return {"error": f"Image 2: {encoding2_result['error']}"}

        encoding1 = np.array(encoding1_result["encoding"])
        encoding2 = np.array(encoding2_result["encoding"])

        # Calculate distance
        distance = np.linalg.norm(encoding1 - encoding2)

        # Check if same person
        is_same_person = distance <= tolerance

        # Calculate confidence
        if is_same_person:
            confidence = max(0, (tolerance - distance) / tolerance)
        else:
            confidence = max(0, (distance - tolerance) / (1 - tolerance))

        return {
            "is_same_person": bool(is_same_person),
            "confidence": round(float(confidence), 3),
            "distance": round(float(distance), 4),
            "tolerance": tolerance
        }

    async def find_face_in_group(
        self,
        target_image_bytes: bytes,
        group_image_bytes: bytes,
        tolerance: float = 0.6
    ) -> Dict[str, Any]:
        """
        Find a person in a group photo

        Args:
            target_image_bytes: Image of the person to find
            group_image_bytes: Group photo to search in
            tolerance: Distance threshold

        Returns:
            Search results
        """
        if not self.face_recognition_available:
            return {"error": "Face recognition not available"}

        # Get target encoding
        target_result = await self.get_face_encoding(target_image_bytes)
        if "error" in target_result:
            return {"error": f"Target image: {target_result['error']}"}

        target_encoding = np.array(target_result["encoding"])

        # Load group image and get all faces
        group_image = Image.open(BytesIO(group_image_bytes))
        group_np = np.array(group_image)

        face_locations = self.face_recognition.face_locations(group_np)
        face_encodings = self.face_recognition.face_encodings(group_np, face_locations)

        if not face_encodings:
            return {
                "found": False,
                "faces_in_group": 0,
                "message": "No faces found in group image"
            }

        # Compare with each face
        matches = []
        for i, (encoding, location) in enumerate(zip(face_encodings, face_locations)):
            distance = np.linalg.norm(target_encoding - encoding)
            is_match = distance <= tolerance

            if is_match:
                confidence = max(0, (tolerance - distance) / tolerance)
                matches.append({
                    "face_index": i + 1,
                    "confidence": round(float(confidence), 3),
                    "distance": round(float(distance), 4),
                    "location": {
                        "top": location[0],
                        "right": location[1],
                        "bottom": location[2],
                        "left": location[3]
                    }
                })

        return {
            "found": len(matches) > 0,
            "faces_in_group": len(face_locations),
            "matches": matches,
            "best_match": matches[0] if matches else None
        }

    async def analyze_face(self, image_bytes: bytes) -> Dict[str, Any]:
        """
        Analyze facial features (placeholder for age/gender/emotion)

        Args:
            image_bytes: Face image

        Returns:
            Face analysis results
        """
        # Detect face first
        detection_result = await self.detect_faces(image_bytes, return_landmarks=True)

        if detection_result["face_count"] == 0:
            return {"error": "No face detected"}

        face = detection_result["faces"][0]

        # Basic analysis based on proportions
        # In production, this would use a trained model
        bbox = face["bbox"]
        aspect_ratio = bbox["width"] / bbox["height"] if bbox["height"] > 0 else 1

        return {
            "face_detected": True,
            "bbox": bbox,
            "landmarks": face.get("landmarks"),
            "face_shape": "oval" if 0.7 <= aspect_ratio <= 0.9 else "round" if aspect_ratio > 0.9 else "long",
            "analysis_note": "Full face analysis requires additional ML models"
        }


# Singleton instance
face_service = FaceService()
