package com.placeholder.sherpai2.domain.similarity

import android.util.Log
import com.placeholder.sherpai2.data.local.dao.FaceCacheDao
import com.placeholder.sherpai2.data.local.entity.FaceCacheEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.sqrt

/**
 * FaceSimilarityScorer - Real-time similarity scoring for Rolling Scan
 *
 * CORE RESPONSIBILITIES:
 * 1. Calculate centroid from selected face embeddings
 * 2. Score all unselected photos against centroid
 * 3. Apply quality boosting (solo photos, high confidence, etc.)
 * 4. Rank photos by final score (similarity + quality boost)
 *
 * KEY OPTIMIZATION: Uses cached embeddings from FaceCacheEntity
 * - No embedding generation needed (already done!)
 * - Blazing fast scoring (just cosine similarity)
 * - Can score 1000+ photos in ~100ms
 */
@Singleton
class FaceSimilarityScorer @Inject constructor(
    private val faceCacheDao: FaceCacheDao
) {

    companion object {
        private const val TAG = "FaceSimilarityScorer"

        // Quality boost constants
        private const val SOLO_PHOTO_BOOST = 0.15f
        private const val HIGH_CONFIDENCE_BOOST = 0.05f
        private const val GROUP_PHOTO_PENALTY = -0.10f
        private const val HIGH_QUALITY_BOOST = 0.03f

        // Thresholds
        private const val HIGH_CONFIDENCE_THRESHOLD = 0.8f
        private const val HIGH_QUALITY_THRESHOLD = 0.8f
        private const val GROUP_PHOTO_THRESHOLD = 3
    }

    /**
     * Scored photo with similarity and quality metrics
     */
    data class ScoredPhoto(
        val imageId: String,
        val imageUri: String,
        val faceIndex: Int,
        val similarityScore: Float,      // 0.0 - 1.0 (cosine similarity to centroid)
        val qualityBoost: Float,         // -0.2 to +0.2 (quality adjustments)
        val finalScore: Float,           // similarity + qualityBoost
        val faceCount: Int,              // Number of faces in image
        val faceAreaRatio: Float,        // Size of face in image
        val qualityScore: Float,         // Overall face quality
        val cachedEmbedding: FloatArray  // For further operations
    ) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (other !is ScoredPhoto) return false
            return imageId == other.imageId && faceIndex == other.faceIndex
        }

        override fun hashCode(): Int {
            return imageId.hashCode() * 31 + faceIndex
        }
    }

    /**
     * Calculate centroid from multiple embeddings
     *
     * Centroid = average of all embedding vectors
     * This represents the "average face" of selected photos
     */
    fun calculateCentroid(embeddings: List<FloatArray>): FloatArray {
        if (embeddings.isEmpty()) {
            Log.w(TAG, "Cannot calculate centroid from empty list")
            return FloatArray(192) { 0f }
        }

        val dimension = embeddings.first().size
        val centroid = FloatArray(dimension) { 0f }

        // Sum all embeddings
        embeddings.forEach { embedding ->
            if (embedding.size != dimension) {
                Log.e(TAG, "Embedding size mismatch: ${embedding.size} vs $dimension")
                return@forEach
            }

            embedding.forEachIndexed { i, value ->
                centroid[i] += value
            }
        }

        // Average
        val count = embeddings.size.toFloat()
        centroid.forEachIndexed { i, _ ->
            centroid[i] /= count
        }

        // Normalize to unit length
        return normalizeEmbedding(centroid)
    }

    /**
     * Score a single photo against centroid
     * Uses cosine similarity
     */
    fun scorePhotoAgainstCentroid(
        photoEmbedding: FloatArray,
        centroid: FloatArray
    ): Float {
        return cosineSimilarity(photoEmbedding, centroid)
    }

    /**
     * CRITICAL: Batch score all photos against centroid
     *
     * This is the main function used by RollingScanViewModel
     *
     * @param allImageIds All available image IDs (with cached embeddings)
     * @param selectedImageIds Already selected images (exclude from results)
     * @param centroid Centroid calculated from selected embeddings
     * @return List of scored photos, sorted by finalScore DESC
     */
    suspend fun scorePhotosAgainstCentroid(
        allImageIds: List<String>,
        selectedImageIds: Set<String>,
        centroid: FloatArray
    ): List<ScoredPhoto> = withContext(Dispatchers.Default) {

        if (centroid.all { it == 0f }) {
            Log.w(TAG, "Centroid is all zeros, cannot score")
            return@withContext emptyList()
        }

        Log.d(TAG, "Scoring ${allImageIds.size} photos (excluding ${selectedImageIds.size} selected)")

        try {
            // Get ALL cached face entries for these images
            val cachedFaces = faceCacheDao.getFaceCacheByImageIds(allImageIds)

            Log.d(TAG, "Retrieved ${cachedFaces.size} cached faces")

            // Filter to unselected images with embeddings
            val scorablePhotos = cachedFaces
                .filter { it.imageId !in selectedImageIds }
                .filter { it.embedding != null }

            Log.d(TAG, "Scorable photos: ${scorablePhotos.size}")

            // Score each photo
            val scoredPhotos = scorablePhotos.mapNotNull { cachedFace ->
                try {
                    val embedding = cachedFace.getEmbedding() ?: return@mapNotNull null

                    // Calculate similarity to centroid
                    val similarityScore = cosineSimilarity(embedding, centroid)

                    // Calculate quality boost
                    val qualityBoost = calculateQualityBoost(
                        faceCount = getFaceCountForImage(cachedFace.imageId, cachedFaces),
                        confidence = cachedFace.confidence,
                        qualityScore = cachedFace.qualityScore,
                        faceAreaRatio = cachedFace.faceAreaRatio
                    )

                    // Final score
                    val finalScore = (similarityScore + qualityBoost).coerceIn(0f, 1f)

                    ScoredPhoto(
                        imageId = cachedFace.imageId,
                        imageUri = getImageUri(cachedFace.imageId), // Will need to fetch
                        faceIndex = cachedFace.faceIndex,
                        similarityScore = similarityScore,
                        qualityBoost = qualityBoost,
                        finalScore = finalScore,
                        faceCount = getFaceCountForImage(cachedFace.imageId, cachedFaces),
                        faceAreaRatio = cachedFace.faceAreaRatio,
                        qualityScore = cachedFace.qualityScore,
                        cachedEmbedding = embedding
                    )
                } catch (e: Exception) {
                    Log.w(TAG, "Error scoring photo ${cachedFace.imageId}: ${e.message}")
                    null
                }
            }

            // Sort by final score (highest first)
            val sorted = scoredPhotos.sortedByDescending { it.finalScore }

            Log.d(TAG, "Scored ${sorted.size} photos. Top score: ${sorted.firstOrNull()?.finalScore}")

            sorted

        } catch (e: Exception) {
            Log.e(TAG, "Error in batch scoring", e)
            emptyList()
        }
    }

    /**
     * Calculate quality boost based on photo characteristics
     *
     * Boosts:
     * - Solo photos (faceCount == 1): +0.15
     * - High confidence (>0.8): +0.05
     * - High quality score (>0.8): +0.03
     *
     * Penalties:
     * - Group photos (faceCount >= 3): -0.10
     */
    private fun calculateQualityBoost(
        faceCount: Int,
        confidence: Float,
        qualityScore: Float,
        faceAreaRatio: Float
    ): Float {
        var boost = 0f

        // MAJOR boost for solo photos (easier to verify, less confusion)
        if (faceCount == 1) {
            boost += SOLO_PHOTO_BOOST
        }

        // Penalize group photos (harder to verify correct face)
        if (faceCount >= GROUP_PHOTO_THRESHOLD) {
            boost += GROUP_PHOTO_PENALTY
        }

        // Boost high-confidence detections
        if (confidence > HIGH_CONFIDENCE_THRESHOLD) {
            boost += HIGH_CONFIDENCE_BOOST
        }

        // Boost high-quality faces (large, clear, frontal)
        if (qualityScore > HIGH_QUALITY_THRESHOLD) {
            boost += HIGH_QUALITY_BOOST
        }

        // Coerce to reasonable range
        return boost.coerceIn(-0.2f, 0.2f)
    }

    /**
     * Get face count for an image
     * (Multiple faces in same image share imageId but different faceIndex)
     */
    private fun getFaceCountForImage(
        imageId: String,
        allCachedFaces: List<FaceCacheEntity>
    ): Int {
        return allCachedFaces.count { it.imageId == imageId }
    }

    /**
     * Get image URI for an imageId
     *
     * NOTE: This is a temporary implementation
     * In production, we'd join with ImageEntity or cache URIs
     */
    private suspend fun getImageUri(imageId: String): String {
        // TODO: Implement proper URI retrieval
        // For now, return imageId as placeholder
        return imageId
    }

    /**
     * Cosine similarity calculation
     *
     * Returns value between -1.0 and 1.0
     * Higher = more similar
     */
    private fun cosineSimilarity(a: FloatArray, b: FloatArray): Float {
        if (a.size != b.size) {
            Log.e(TAG, "Embedding size mismatch: ${a.size} vs ${b.size}")
            return 0f
        }

        var dotProduct = 0f
        var normA = 0f
        var normB = 0f

        a.indices.forEach { i ->
            dotProduct += a[i] * b[i]
            normA += a[i] * a[i]
            normB += b[i] * b[i]
        }

        if (normA == 0f || normB == 0f) {
            Log.w(TAG, "Zero norm in similarity calculation")
            return 0f
        }

        val similarity = dotProduct / (sqrt(normA) * sqrt(normB))

        // Handle NaN/Infinity
        if (similarity.isNaN() || similarity.isInfinite()) {
            Log.w(TAG, "Invalid similarity: $similarity")
            return 0f
        }

        return similarity
    }

    /**
     * Normalize embedding to unit length
     */
    private fun normalizeEmbedding(embedding: FloatArray): FloatArray {
        var norm = 0f
        for (value in embedding) {
            norm += value * value
        }
        norm = sqrt(norm)

        return if (norm > 0) {
            FloatArray(embedding.size) { i -> embedding[i] / norm }
        } else {
            Log.w(TAG, "Cannot normalize zero embedding")
            embedding
        }
    }

    /**
     * Incremental scoring for viewport optimization
     *
     * Only scores photos in visible range + next batch
     * Useful for large libraries (5000+ photos)
     */
    suspend fun scorePhotosIncrementally(
        visibleRange: IntRange,
        batchSize: Int = 50,
        allImageIds: List<String>,
        selectedImageIds: Set<String>,
        centroid: FloatArray
    ): List<ScoredPhoto> {

        val rangeToScan = visibleRange.first until
                (visibleRange.last + batchSize).coerceAtMost(allImageIds.size)

        val imageIdsToScan = allImageIds.slice(rangeToScan)

        return scorePhotosAgainstCentroid(
            allImageIds = imageIdsToScan,
            selectedImageIds = selectedImageIds,
            centroid = centroid
        )
    }
}