"""Face registration and identification service using insightface + pgvector."""

import logging
import uuid
from pathlib import Path

import cv2
import numpy as np
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession

from app.config import GLOBAL_USER_ID, settings
from app.models.face import FaceEmbedding
from app.models.user import User
from app.services.user_service import get_or_create_user
from app.utils.face_analyzer import FaceAnalyzer

logger = logging.getLogger(__name__)


class FaceService:
    def __init__(self, analyzer: FaceAnalyzer):
        self.analyzer = analyzer

    async def register(
        self,
        db: AsyncSession,
        image_bytes: bytes,
        user_name: str,
    ) -> dict:
        """Register a face for a user.

        1. Extract face embedding from image
        2. Try matching against existing faces
        3. If match → add embedding to matched user (face takes priority)
        4. If no match → get-or-create user by name → add embedding

        Returns dict with user_id, user_name, is_new_user, confidence.
        """
        embedding = self._extract_embedding(image_bytes)
        if embedding is None:
            raise ValueError("얼굴을 감지할 수 없습니다.")

        # Try matching existing user by face
        match = await self._find_best_match(db, embedding)

        if match is not None:
            matched_user, confidence = match
            # Add new embedding to face-matched user
            image_path = await self._save_face_image(image_bytes, matched_user.id)
            face_record = FaceEmbedding(
                user_id=matched_user.id,
                embedding=embedding.tolist(),
                image_path=image_path,
            )
            db.add(face_record)
            await db.commit()
            return {
                "user_id": matched_user.id,
                "user_name": matched_user.name,
                "is_new_user": False,
                "confidence": confidence,
            }

        # No face match → get-or-create user by name
        user, created = await get_or_create_user(db, user_name)

        image_path = await self._save_face_image(image_bytes, user.id)
        face_record = FaceEmbedding(
            user_id=user.id,
            embedding=embedding.tolist(),
            image_path=image_path,
        )
        db.add(face_record)
        await db.commit()

        return {
            "user_id": user.id,
            "user_name": user.name,
            "is_new_user": created,
            "confidence": 1.0,
        }

    async def identify(
        self,
        db: AsyncSession,
        image_bytes: bytes,
    ) -> dict | None:
        """Identify user from face image.

        Returns dict with user_id, user_name, confidence or None if no match.
        """
        embedding = self._extract_embedding(image_bytes)
        if embedding is None:
            raise ValueError("얼굴을 감지할 수 없습니다.")

        match = await self._find_best_match(db, embedding)
        if match is None:
            return None

        user, confidence = match
        return {
            "user_id": user.id,
            "user_name": user.name,
            "confidence": confidence,
        }

    def _extract_embedding(self, image_bytes: bytes) -> np.ndarray | None:
        """Decode image bytes and extract largest face embedding."""
        arr = np.frombuffer(image_bytes, dtype=np.uint8)
        image = cv2.imdecode(arr, cv2.IMREAD_COLOR)
        if image is None:
            raise ValueError("이미지를 디코딩할 수 없습니다.")
        return self.analyzer.get_largest_face_embedding(image)

    async def _find_best_match(
        self,
        db: AsyncSession,
        embedding: np.ndarray,
    ) -> tuple[User, float] | None:
        """Find best matching user via pgvector cosine distance.

        Returns (User, confidence) if similarity > threshold, else None.
        Confidence = 1 - cosine_distance (higher = more similar).
        """
        threshold = settings.FACE_MATCH_THRESHOLD
        embedding_str = "[" + ",".join(str(x) for x in embedding.tolist()) + "]"

        # pgvector cosine distance: <=> operator
        # Returns distance (0=identical, 2=opposite), we want similarity = 1 - distance
        query = text("""
            SELECT fe.id, fe.user_id, fe.embedding <=> :emb AS distance
            FROM face_embeddings fe
            JOIN users u ON u.id = fe.user_id
            WHERE u.id != :global_id
            ORDER BY fe.embedding <=> :emb
            LIMIT 1
        """)

        result = await db.execute(query, {"emb": embedding_str, "global_id": str(GLOBAL_USER_ID)})
        row = result.first()

        if row is None:
            return None

        distance = float(row.distance)
        confidence = 1.0 - distance

        if confidence < threshold:
            return None

        # Fetch the full user object
        user_result = await db.execute(
            select(User).where(User.id == row.user_id)
        )
        user = user_result.scalar_one()
        return user, confidence

    async def _save_face_image(self, image_bytes: bytes, user_id: uuid.UUID) -> str:
        """Save face image to disk and return path relative to UPLOAD_DIR."""
        rel_path = Path("faces") / str(user_id)
        dest_dir = Path(settings.UPLOAD_DIR) / rel_path
        dest_dir.mkdir(parents=True, exist_ok=True)
        filename = f"{uuid.uuid4().hex}.jpg"
        (dest_dir / filename).write_bytes(image_bytes)
        return str(rel_path / filename)
