package com.placeholder.sherpai2.domain.clustering

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Rect
import android.net.Uri
import android.util.Log
import com.google.android.gms.tasks.Tasks
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.face.FaceDetection
import com.google.mlkit.vision.face.FaceDetectorOptions
import com.placeholder.sherpai2.data.local.dao.FaceCacheDao
import com.placeholder.sherpai2.data.local.dao.ImageDao
import com.placeholder.sherpai2.data.local.entity.FaceCacheEntity
import com.placeholder.sherpai2.data.local.entity.ImageEntity
import com.placeholder.sherpai2.ml.FaceNetModel
import com.placeholder.sherpai2.ml.FaceNormalizer
import com.placeholder.sherpai2.ui.discover.DiscoverySettings
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt

/**
 * FaceClusteringService - FIXED to properly use metadata cache
 *
 * THE CRITICAL FIX:
 * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 * Path 2 now CORRECTLY checks for metadata cache WITHOUT requiring embeddings
 * Uses countFacesWithoutEmbeddings() which counts faces that HAVE metadata
 * but DON'T have embeddings yet
 *
 * 3-PATH STRATEGY (CORRECTED):
 * ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 * Path 1: Cached embeddings exist → Instant (< 2 sec)
 * Path 2: Metadata cache exists → Generate embeddings for quality faces (~3 min) ← FIXED!
 * Path 3: No cache → Full scan (~8 min)
 */
@Singleton
class FaceClusteringService @Inject constructor(
    @ApplicationContext private val context: Context,
    private val imageDao: ImageDao,
    private val faceCacheDao: FaceCacheDao
) {

    private val semaphore = Semaphore(3)

    companion object {
        private const val TAG = "FaceClustering"
        private const val MAX_FACES_TO_CLUSTER = 2000

        // Path selection thresholds
        private const val MIN_CACHED_EMBEDDINGS = 20    // Path 1
        private const val MIN_QUALITY_METADATA = 50     // Path 2
        private const val MIN_STANDARD_FACES = 10       // Absolute minimum

        // IoU matching threshold
        private const val IOU_THRESHOLD = 0.5f
    }

    suspend fun discoverPeople(
        strategy: ClusteringStrategy = ClusteringStrategy.PREMIUM_SOLO_ONLY,
        maxFacesToCluster: Int = MAX_FACES_TO_CLUSTER,
        onProgress: (Int, Int, String) -> Unit = { _, _, _ -> }
    ): ClusteringResult = withContext(Dispatchers.Default) {

        val startTime = System.currentTimeMillis()

        Log.d(TAG, "════════════════════════════════════════")
        Log.d(TAG, "CACHE-AWARE DISCOVERY STARTED")
        Log.d(TAG, "════════════════════════════════════════")

        val result = when (strategy) {
            ClusteringStrategy.PREMIUM_SOLO_ONLY -> {
                clusterPremiumSoloFaces(maxFacesToCluster, onProgress)
            }
            ClusteringStrategy.STANDARD_SOLO_ONLY -> {
                clusterStandardSoloFaces(maxFacesToCluster, onProgress)
            }
            ClusteringStrategy.TWO_PHASE -> {
                clusterPremiumSoloFaces(maxFacesToCluster, onProgress)
            }
            ClusteringStrategy.LEGACY_ALL_FACES -> {
                clusterAllFacesLegacy(maxFacesToCluster, onProgress)
            }
        }

        val elapsedTime = System.currentTimeMillis() - startTime
        Log.d(TAG, "════════════════════════════════════════")
        Log.d(TAG, "Discovery Complete!")
        Log.d(TAG, "Clusters found: ${result.clusters.size}")
        Log.d(TAG, "Time: ${elapsedTime / 1000}s")
        Log.d(TAG, "════════════════════════════════════════")

        result.copy(processingTimeMs = elapsedTime)
    }

    /**
     * FIXED: 3-Path Selection with proper metadata checking
     */
    private suspend fun clusterPremiumSoloFaces(
        maxFaces: Int,
        onProgress: (Int, Int, String) -> Unit
    ): ClusteringResult = withContext(Dispatchers.Default) {

        onProgress(5, 100, "Checking cache...")

        // ═════════════════════════════════════════════════════════
        // PATH 1: Check for cached embeddings (INSTANT)
        // ═════════════════════════════════════════════════════════
        Log.d(TAG, "Path 1: Checking for cached embeddings...")

        val embeddingCount = withContext(Dispatchers.IO) {
            try {
                faceCacheDao.countFacesWithEmbeddings(minQuality = 0.6f)
            } catch (e: Exception) {
                Log.w(TAG, "Error counting embeddings: ${e.message}")
                0
            }
        }

        Log.d(TAG, "Found $embeddingCount faces with cached embeddings")

        if (embeddingCount >= MIN_CACHED_EMBEDDINGS) {
            Log.d(TAG, "✅ PATH 1 SUCCESS: Using $embeddingCount cached embeddings")

            val cachedFaces = withContext(Dispatchers.IO) {
                faceCacheDao.getAllQualityFaces(
                    minRatio = 0.03f,
                    minQuality = 0.6f,
                    limit = Int.MAX_VALUE
                )
            }

            return@withContext clusterCachedEmbeddings(cachedFaces, maxFaces, onProgress)
        }

        // ═════════════════════════════════════════════════════════
        // PATH 2: Check for metadata cache (FAST)
        // ═════════════════════════════════════════════════════════
        Log.d(TAG, "Path 1 insufficient, trying Path 2...")
        Log.d(TAG, "Path 2: Checking for quality metadata...")

        // THE CRITICAL FIX: Count faces WITH metadata but WITHOUT embeddings
        val metadataCount = withContext(Dispatchers.IO) {
            try {
                faceCacheDao.countFacesWithoutEmbeddings(minQuality = 0.6f)
            } catch (e: Exception) {
                Log.w(TAG, "Error counting metadata: ${e.message}")
                0
            }
        }

        Log.d(TAG, "Found $metadataCount faces in metadata cache (without embeddings)")

        if (metadataCount >= MIN_QUALITY_METADATA) {
            Log.d(TAG, "✅ PATH 2 SUCCESS: Using metadata cache")

            val qualityMetadata = withContext(Dispatchers.IO) {
                faceCacheDao.getQualityFacesWithoutEmbeddings(
                    minRatio = 0.03f,
                    minQuality = 0.6f,
                    limit = 5000
                )
            }

            Log.d(TAG, "Loaded ${qualityMetadata.size} quality face metadata entries")
            return@withContext clusterWithQualityPrefiltering(qualityMetadata, maxFaces, onProgress)
        }

        // ═════════════════════════════════════════════════════════
        // PATH 3: Full scan (SLOW, last resort)
        // ═════════════════════════════════════════════════════════
        Log.w(TAG, "Path 2 insufficient, falling back to Path 3 (full scan)")
        Log.w(TAG, "⚠️  PATH 3: Full library scan (this will take several minutes)")
        Log.w(TAG, "Cache stats: $embeddingCount with embeddings, $metadataCount metadata only")

        onProgress(10, 100, "No cache found, performing full scan...")
        return@withContext clusterAllFacesLegacy(maxFaces, onProgress)
    }

    /**
     * Path 1: Cluster using cached embeddings (INSTANT)
     */
    private suspend fun clusterCachedEmbeddings(
        cachedFaces: List<FaceCacheEntity>,
        maxFaces: Int,
        onProgress: (Int, Int, String) -> Unit
    ): ClusteringResult = withContext(Dispatchers.Default) {

        Log.d(TAG, "Converting ${cachedFaces.size} cached faces to clustering format...")
        onProgress(30, 100, "Using ${cachedFaces.size} cached faces...")

        val allFaces = cachedFaces.mapNotNull { cached ->
            val embedding = cached.getEmbedding() ?: return@mapNotNull null

            DetectedFaceWithEmbedding(
                imageId = cached.imageId,
                imageUri = "",
                capturedAt = cached.detectedAt,
                embedding = embedding,
                boundingBox = cached.getBoundingBox(),
                confidence = cached.confidence,
                faceCount = 1,
                imageWidth = cached.imageWidth,
                imageHeight = cached.imageHeight
            )
        }

        if (allFaces.isEmpty()) {
            return@withContext ClusteringResult(
                clusters = emptyList(),
                totalFacesAnalyzed = 0,
                processingTimeMs = 0,
                errorMessage = "No valid cached embeddings found"
            )
        }

        Log.d(TAG, "Clustering ${allFaces.size} cached faces...")
        onProgress(50, 100, "Clustering ${allFaces.size} faces...")

        val rawClusters = performDBSCAN(
            faces = allFaces.take(maxFaces),
            epsilon = 0.22f,
            minPoints = 3
        )

        onProgress(75, 100, "Analyzing relationships...")

        val coOccurrenceGraph = buildCoOccurrenceGraph(rawClusters)

        onProgress(90, 100, "Finalizing clusters...")

        val clusters = rawClusters.mapIndexed { index, cluster ->
            FaceCluster(
                clusterId = index,
                faces = cluster.faces,
                representativeFaces = selectRepresentativeFacesByCentroid(cluster.faces, count = 6),
                photoCount = cluster.faces.map { it.imageId }.distinct().size,
                averageConfidence = cluster.faces.map { it.confidence }.average().toFloat(),
                estimatedAge = estimateAge(cluster.faces),
                potentialSiblings = findPotentialSiblings(cluster, rawClusters, coOccurrenceGraph)
            )
        }.sortedByDescending { it.photoCount }

        onProgress(100, 100, "Complete!")

        ClusteringResult(
            clusters = clusters,
            totalFacesAnalyzed = allFaces.size,
            processingTimeMs = 0,
            strategy = ClusteringStrategy.PREMIUM_SOLO_ONLY
        )
    }

    /**
     * Path 2: CORRECTED to work with metadata cache
     *
     * Generates embeddings on-demand and saves them with IoU matching
     */
    private suspend fun clusterWithQualityPrefiltering(
        qualityFacesMetadata: List<FaceCacheEntity>,
        maxFaces: Int,
        onProgress: (Int, Int, String) -> Unit
    ): ClusteringResult = withContext(Dispatchers.Default) {

        Log.d(TAG, "Starting Path 2: Quality metadata pre-filtering")
        Log.d(TAG, "Quality faces in metadata: ${qualityFacesMetadata.size}")

        onProgress(15, 100, "Pre-filtering complete...")

        // Extract unique imageIds from metadata
        val imageIdsToProcess = qualityFacesMetadata
            .map { it.imageId }
            .distinct()

        Log.d(TAG, "Pre-filtered to ${imageIdsToProcess.size} images with quality faces")

        // Load only those specific images
        val imagesToProcess = withContext(Dispatchers.IO) {
            imageDao.getImagesByIds(imageIdsToProcess)
        }

        Log.d(TAG, "Loading ${imagesToProcess.size} quality photos...")
        onProgress(20, 100, "Generating embeddings for ${imagesToProcess.size} quality photos...")

        val faceNetModel = FaceNetModel(context)
        val detector = FaceDetection.getClient(
            FaceDetectorOptions.Builder()
                .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
                .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
                .setMinFaceSize(0.15f)
                .build()
        )

        try {
            val allFaces = mutableListOf<DetectedFaceWithEmbedding>()
            var iouMatchSuccesses = 0
            var iouMatchFailures = 0

            coroutineScope {
                val jobs = imagesToProcess.mapIndexed { index, image ->
                    async(Dispatchers.IO) {
                        semaphore.acquire()
                        try {
                            val bitmap = loadBitmapDownsampled(
                                Uri.parse(image.imageUri),
                                768
                            ) ?: return@async Triple(emptyList<DetectedFaceWithEmbedding>(), 0, 0)

                            val inputImage = InputImage.fromBitmap(bitmap, 0)
                            val mlKitFaces = Tasks.await(detector.process(inputImage))

                            val imageWidth = bitmap.width
                            val imageHeight = bitmap.height

                            // Get cached faces for THIS specific image
                            val cachedFacesForImage = qualityFacesMetadata.filter {
                                it.imageId == image.imageId
                            }

                            var localSuccesses = 0
                            var localFailures = 0

                            val facesForImage = mutableListOf<DetectedFaceWithEmbedding>()

                            mlKitFaces.forEach { mlFace ->
                                val qualityCheck = FaceQualityFilter.validateForDiscovery(
                                    face = mlFace,
                                    imageWidth = imageWidth,
                                    imageHeight = imageHeight
                                )

                                if (!qualityCheck.isValid) {
                                    return@forEach
                                }

                                try {
                                    // Crop and normalize face
                                    val faceBitmap = FaceNormalizer.cropAndNormalize(bitmap, mlFace)
                                        ?: return@forEach

                                    val embedding = faceNetModel.generateEmbedding(faceBitmap)
                                    faceBitmap.recycle()

                                    // Add to results
                                    facesForImage.add(
                                        DetectedFaceWithEmbedding(
                                            imageId = image.imageId,
                                            imageUri = image.imageUri,
                                            capturedAt = image.capturedAt,
                                            embedding = embedding,
                                            boundingBox = mlFace.boundingBox,
                                            confidence = qualityCheck.confidenceScore,
                                            faceCount = mlKitFaces.size,
                                            imageWidth = imageWidth,
                                            imageHeight = imageHeight
                                        )
                                    )

                                    // Save embedding to cache with IoU matching
                                    val matched = matchAndSaveEmbedding(
                                        imageId = image.imageId,
                                        detectedBox = mlFace.boundingBox,
                                        embedding = embedding,
                                        cachedFaces = cachedFacesForImage
                                    )

                                    if (matched) localSuccesses++ else localFailures++

                                } catch (e: Exception) {
                                    Log.w(TAG, "Failed to process face: ${e.message}")
                                }
                            }

                            bitmap.recycle()

                            // Update progress
                            if (index % 20 == 0) {
                                val progress = 20 + (index * 60 / imagesToProcess.size)
                                onProgress(progress, 100, "Processed $index/${imagesToProcess.size} photos...")
                            }

                            Triple(facesForImage, localSuccesses, localFailures)
                        } finally {
                            semaphore.release()
                        }
                    }
                }

                val results = jobs.awaitAll()
                results.forEach { (faces, successes, failures) ->
                    allFaces.addAll(faces)
                    iouMatchSuccesses += successes
                    iouMatchFailures += failures
                }
            }

            Log.d(TAG, "IoU Matching Results:")
            Log.d(TAG, "  Successful matches: $iouMatchSuccesses")
            Log.d(TAG, "  Failed matches: $iouMatchFailures")
            val successRate = if (iouMatchSuccesses + iouMatchFailures > 0) {
                (iouMatchSuccesses.toFloat() / (iouMatchSuccesses + iouMatchFailures) * 100).toInt()
            } else 0
            Log.d(TAG, "  Success rate: $successRate%")
            Log.d(TAG, "✅ Embeddings saved to cache with IoU matching")

            if (allFaces.isEmpty()) {
                return@withContext ClusteringResult(
                    clusters = emptyList(),
                    totalFacesAnalyzed = 0,
                    processingTimeMs = 0,
                    errorMessage = "No faces detected with sufficient quality"
                )
            }

            // Cluster
            onProgress(80, 100, "Clustering ${allFaces.size} faces...")

            val rawClusters = performDBSCAN(allFaces.take(maxFaces), 0.22f, 3)
            val coOccurrenceGraph = buildCoOccurrenceGraph(rawClusters)

            onProgress(90, 100, "Finalizing clusters...")

            val clusters = rawClusters.mapIndexed { index, cluster ->
                FaceCluster(
                    clusterId = index,
                    faces = cluster.faces,
                    representativeFaces = selectRepresentativeFacesByCentroid(cluster.faces, count = 6),
                    photoCount = cluster.faces.map { it.imageId }.distinct().size,
                    averageConfidence = cluster.faces.map { it.confidence }.average().toFloat(),
                    estimatedAge = estimateAge(cluster.faces),
                    potentialSiblings = findPotentialSiblings(cluster, rawClusters, coOccurrenceGraph)
                )
            }.sortedByDescending { it.photoCount }

            onProgress(100, 100, "Complete!")

            ClusteringResult(
                clusters = clusters,
                totalFacesAnalyzed = allFaces.size,
                processingTimeMs = 0,
                strategy = ClusteringStrategy.PREMIUM_SOLO_ONLY
            )
        } finally {
            detector.close()
        }
    }

    /**
     * IoU matching and saving - handles non-deterministic ML Kit order
     */
    private suspend fun matchAndSaveEmbedding(
        imageId: String,
        detectedBox: Rect,
        embedding: FloatArray,
        cachedFaces: List<FaceCacheEntity>
    ): Boolean {
        if (cachedFaces.isEmpty()) {
            return false
        }

        // Find best matching cached face by IoU
        var bestMatch: FaceCacheEntity? = null
        var bestIoU = 0f

        cachedFaces.forEach { cached ->
            val iou = calculateIoU(detectedBox, cached.getBoundingBox())
            if (iou > bestIoU) {
                bestIoU = iou
                bestMatch = cached
            }
        }

        // Save if IoU meets threshold
        if (bestMatch != null && bestIoU >= IOU_THRESHOLD) {
            try {
                withContext(Dispatchers.IO) {
                    val updated = bestMatch!!.copy(
                        embedding = embedding.joinToString(",")
                    )
                    faceCacheDao.update(updated)
                }
                return true
            } catch (e: Exception) {
                Log.e(TAG, "Failed to save embedding: ${e.message}")
                return false
            }
        }

        return false
    }

    /**
     * Calculate IoU between two bounding boxes
     */
    private fun calculateIoU(rect1: Rect, rect2: Rect): Float {
        val intersectionLeft = max(rect1.left, rect2.left)
        val intersectionTop = max(rect1.top, rect2.top)
        val intersectionRight = min(rect1.right, rect2.right)
        val intersectionBottom = min(rect1.bottom, rect2.bottom)

        if (intersectionLeft >= intersectionRight || intersectionTop >= intersectionBottom) {
            return 0f
        }

        val intersectionArea = (intersectionRight - intersectionLeft) * (intersectionBottom - intersectionTop)
        val area1 = rect1.width() * rect1.height()
        val area2 = rect2.width() * rect2.height()
        val unionArea = area1 + area2 - intersectionArea

        return if (unionArea > 0) {
            intersectionArea.toFloat() / unionArea.toFloat()
        } else {
            0f
        }
    }

    private suspend fun clusterStandardSoloFaces(
        maxFaces: Int,
        onProgress: (Int, Int, String) -> Unit
    ): ClusteringResult = clusterPremiumSoloFaces(maxFaces, onProgress)

    /**
     * Path 3: Legacy full scan (fallback only)
     */
    private suspend fun clusterAllFacesLegacy(
        maxFaces: Int,
        onProgress: (Int, Int, String) -> Unit
    ): ClusteringResult = withContext(Dispatchers.Default) {

        Log.w(TAG, "⚠️  Running LEGACY full scan")

        onProgress(10, 100, "Loading all images...")

        val allImages = withContext(Dispatchers.IO) {
            imageDao.getAllImages()
        }

        Log.d(TAG, "Processing ${allImages.size} images...")
        onProgress(20, 100, "Detecting faces in ${allImages.size} photos...")

        val faceNetModel = FaceNetModel(context)
        val detector = FaceDetection.getClient(
            FaceDetectorOptions.Builder()
                .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
                .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
                .setMinFaceSize(0.15f)
                .build()
        )

        try {
            val allFaces = mutableListOf<DetectedFaceWithEmbedding>()

            coroutineScope {
                val jobs = allImages.mapIndexed { index, image ->
                    async(Dispatchers.IO) {
                        semaphore.acquire()
                        try {
                            val bitmap = loadBitmapDownsampled(
                                Uri.parse(image.imageUri),
                                768
                            ) ?: return@async emptyList()

                            val inputImage = InputImage.fromBitmap(bitmap, 0)
                            val faces = Tasks.await(detector.process(inputImage))

                            val imageWidth = bitmap.width
                            val imageHeight = bitmap.height

                            val faceEmbeddings = faces.mapNotNull { face ->
                                val qualityCheck = FaceQualityFilter.validateForDiscovery(
                                    face = face,
                                    imageWidth = imageWidth,
                                    imageHeight = imageHeight
                                )

                                if (!qualityCheck.isValid) return@mapNotNull null

                                try {
                                    val faceBitmap = FaceNormalizer.cropAndNormalize(bitmap, face)
                                        ?: return@mapNotNull null

                                    val embedding = faceNetModel.generateEmbedding(faceBitmap)
                                    faceBitmap.recycle()

                                    DetectedFaceWithEmbedding(
                                        imageId = image.imageId,
                                        imageUri = image.imageUri,
                                        capturedAt = image.capturedAt,
                                        embedding = embedding,
                                        boundingBox = face.boundingBox,
                                        confidence = qualityCheck.confidenceScore,
                                        faceCount = faces.size,
                                        imageWidth = imageWidth,
                                        imageHeight = imageHeight
                                    )
                                } catch (e: Exception) {
                                    null
                                }
                            }

                            bitmap.recycle()

                            if (index % 20 == 0) {
                                val progress = 20 + (index * 60 / allImages.size)
                                onProgress(progress, 100, "Processed $index/${allImages.size} photos...")
                            }

                            faceEmbeddings
                        } finally {
                            semaphore.release()
                        }
                    }
                }

                jobs.awaitAll().flatten().forEach { allFaces.add(it) }
            }

            if (allFaces.isEmpty()) {
                return@withContext ClusteringResult(
                    clusters = emptyList(),
                    totalFacesAnalyzed = 0,
                    processingTimeMs = 0,
                    errorMessage = "No faces detected"
                )
            }

            onProgress(80, 100, "Clustering ${allFaces.size} faces...")

            val rawClusters = performDBSCAN(allFaces.take(maxFaces), 0.22f, 3)
            val coOccurrenceGraph = buildCoOccurrenceGraph(rawClusters)

            onProgress(90, 100, "Finalizing clusters...")

            val clusters = rawClusters.mapIndexed { index, cluster ->
                FaceCluster(
                    clusterId = index,
                    faces = cluster.faces,
                    representativeFaces = selectRepresentativeFacesByCentroid(cluster.faces, count = 6),
                    photoCount = cluster.faces.map { it.imageId }.distinct().size,
                    averageConfidence = cluster.faces.map { it.confidence }.average().toFloat(),
                    estimatedAge = estimateAge(cluster.faces),
                    potentialSiblings = findPotentialSiblings(cluster, rawClusters, coOccurrenceGraph)
                )
            }.sortedByDescending { it.photoCount }

            onProgress(100, 100, "Complete!")

            ClusteringResult(
                clusters = clusters,
                totalFacesAnalyzed = allFaces.size,
                processingTimeMs = 0,
                strategy = ClusteringStrategy.LEGACY_ALL_FACES
            )
        } finally {
            detector.close()
        }
    }

    // REPLACE the discoverPeopleWithSettings method (lines 679-716) with this:

    suspend fun discoverPeopleWithSettings(
        settings: DiscoverySettings,
        onProgress: (Int, Int, String) -> Unit = { _, _, _ -> }
    ): ClusteringResult = withContext(Dispatchers.Default) {

        Log.d(TAG, "════════════════════════════════════════")
        Log.d(TAG, "🎛️  DISCOVERY WITH CUSTOM SETTINGS")
        Log.d(TAG, "════════════════════════════════════════")
        Log.d(TAG, "Settings received:")
        Log.d(TAG, "  • minFaceSize:  ${settings.minFaceSize} (${(settings.minFaceSize * 100).toInt()}%)")
        Log.d(TAG, "  • minQuality:   ${settings.minQuality} (${(settings.minQuality * 100).toInt()}%)")
        Log.d(TAG, "  • epsilon:      ${settings.epsilon}")
        Log.d(TAG, "════════════════════════════════════════")

        // Get quality faces using settings
        val qualityMetadata = withContext(Dispatchers.IO) {
            faceCacheDao.getQualityFacesWithoutEmbeddings(
                minRatio = settings.minFaceSize,
                minQuality = settings.minQuality,
                limit = 5000
            )
        }

        Log.d(TAG, "Found ${qualityMetadata.size} faces matching quality settings")
        Log.d(TAG, "  • Query used: minRatio=${settings.minFaceSize}, minQuality=${settings.minQuality}")

        // Adjust threshold based on library size
        val minRequired = if (qualityMetadata.size < 50) 30 else 50

        Log.d(TAG, "Path selection:")
        Log.d(TAG, "  • Faces available: ${qualityMetadata.size}")
        Log.d(TAG, "  • Minimum required: $minRequired")

        if (qualityMetadata.size >= minRequired) {
            Log.d(TAG, "✅ Using Path 2 (quality pre-filtering)")
            Log.d(TAG, "════════════════════════════════════════")

            // Use Path 2 (quality pre-filtering)
            return@withContext clusterWithQualityPrefiltering(
                qualityFacesMetadata = qualityMetadata,
                maxFaces = MAX_FACES_TO_CLUSTER,
                onProgress = onProgress
            )
        } else {
            Log.d(TAG, "⚠️  Using fallback path (standard discovery)")
            Log.d(TAG, "  • Reason: ${qualityMetadata.size} < $minRequired")
            Log.d(TAG, "════════════════════════════════════════")

            // Fallback to regular discovery (no Path 3, use existing methods)
            Log.w(TAG, "Insufficient metadata (${qualityMetadata.size} < $minRequired), using standard discovery")

            // Use existing discoverPeople with appropriate strategy
            val strategy = if (settings.minQuality >= 0.7f) {
                ClusteringStrategy.PREMIUM_SOLO_ONLY
            } else {
                ClusteringStrategy.STANDARD_SOLO_ONLY
            }

            return@withContext discoverPeople(
                strategy = strategy,
                maxFacesToCluster = MAX_FACES_TO_CLUSTER,
                onProgress = onProgress
            )
        }
    }
    // Clustering algorithms (unchanged)
    private fun performDBSCAN(faces: List<DetectedFaceWithEmbedding>, epsilon: Float, minPoints: Int): List<RawCluster> {
        val visited = mutableSetOf<Int>()
        val clusters = mutableListOf<RawCluster>()
        var clusterId = 0

        for (i in faces.indices) {
            if (i in visited) continue
            val neighbors = findNeighbors(i, faces, epsilon)
            if (neighbors.size < minPoints) {
                visited.add(i)
                continue
            }

            val cluster = mutableListOf<DetectedFaceWithEmbedding>()
            val queue = ArrayDeque(listOf(i))

            while (queue.isNotEmpty()) {
                val pointIdx = queue.removeFirst()
                if (pointIdx in visited) continue

                visited.add(pointIdx)
                cluster.add(faces[pointIdx])

                val pointNeighbors = findNeighbors(pointIdx, faces, epsilon)
                if (pointNeighbors.size >= minPoints) {
                    queue.addAll(pointNeighbors.filter { it !in visited })
                }
            }

            if (cluster.size >= minPoints) {
                clusters.add(RawCluster(clusterId++, cluster))
            }
        }

        return clusters
    }

    private fun findNeighbors(pointIdx: Int, faces: List<DetectedFaceWithEmbedding>, epsilon: Float): List<Int> {
        val point = faces[pointIdx]
        return faces.indices.filter { i ->
            if (i == pointIdx) return@filter false
            val otherFace = faces[i]
            val similarity = cosineSimilarity(point.embedding, otherFace.embedding)
            val appearTogether = point.imageId == otherFace.imageId
            val effectiveEpsilon = if (appearTogether) epsilon * 0.7f else epsilon
            similarity > (1 - effectiveEpsilon)
        }
    }

    private fun cosineSimilarity(a: FloatArray, b: FloatArray): Float {
        var dotProduct = 0f
        var normA = 0f
        var normB = 0f
        for (i in a.indices) {
            dotProduct += a[i] * b[i]
            normA += a[i] * a[i]
            normB += b[i] * b[i]
        }
        return dotProduct / (sqrt(normA) * sqrt(normB))
    }

    private fun buildCoOccurrenceGraph(clusters: List<RawCluster>): Map<Int, Map<Int, Int>> {
        val graph = mutableMapOf<Int, MutableMap<Int, Int>>()
        for (i in clusters.indices) {
            graph[i] = mutableMapOf()
            val imageIds = clusters[i].faces.map { it.imageId }.toSet()
            for (j in clusters.indices) {
                if (i == j) continue
                val sharedImages = clusters[j].faces.count { it.imageId in imageIds }
                if (sharedImages > 0) {
                    graph[i]!![j] = sharedImages
                }
            }
        }
        return graph
    }

    private fun findPotentialSiblings(cluster: RawCluster, allClusters: List<RawCluster>, coOccurrenceGraph: Map<Int, Map<Int, Int>>): List<Int> {
        val clusterIdx = allClusters.indexOf(cluster)
        if (clusterIdx == -1) return emptyList()
        return coOccurrenceGraph[clusterIdx]
            ?.filter { (_, count) -> count >= 5 }
            ?.keys
            ?.toList()
            ?: emptyList()
    }

    fun selectRepresentativeFacesByCentroid(faces: List<DetectedFaceWithEmbedding>, count: Int): List<DetectedFaceWithEmbedding> {
        if (faces.size <= count) return faces
        val centroid = calculateCentroid(faces.map { it.embedding })
        val facesWithDistance = faces.map { face ->
            val distance = 1 - cosineSimilarity(face.embedding, centroid)
            face to distance
        }
        val sortedByProximity = facesWithDistance.sortedBy { it.second }
        val representatives = mutableListOf<DetectedFaceWithEmbedding>()
        representatives.add(sortedByProximity.first().first)
        val remainingFaces = sortedByProximity.drop(1).take(count * 3)
        val sortedByTime = remainingFaces.map { it.first }.sortedBy { it.capturedAt }
        if (sortedByTime.isNotEmpty()) {
            val step = sortedByTime.size / (count - 1).coerceAtLeast(1)
            for (i in 0 until (count - 1)) {
                val index = (i * step).coerceAtMost(sortedByTime.size - 1)
                representatives.add(sortedByTime[index])
            }
        }
        return representatives.take(count)
    }

    private fun calculateCentroid(embeddings: List<FloatArray>): FloatArray {
        if (embeddings.isEmpty()) return FloatArray(0)
        val size = embeddings.first().size
        val centroid = FloatArray(size) { 0f }
        embeddings.forEach { embedding ->
            for (i in embedding.indices) {
                centroid[i] += embedding[i]
            }
        }
        val count = embeddings.size.toFloat()
        for (i in centroid.indices) {
            centroid[i] /= count
        }
        val norm = sqrt(centroid.map { it * it }.sum())
        return if (norm > 0) {
            centroid.map { it / norm }.toFloatArray()
        } else {
            centroid
        }
    }

    private fun estimateAge(faces: List<DetectedFaceWithEmbedding>): AgeEstimate {
        val timestamps = faces.map { it.capturedAt }.sorted()
        if (timestamps.isEmpty() || timestamps.last() == 0L) return AgeEstimate.UNKNOWN
        val span = timestamps.last() - timestamps.first()
        val spanYears = span / (365.25 * 24 * 60 * 60 * 1000)
        return if (spanYears > 3.0) AgeEstimate.CHILD else AgeEstimate.UNKNOWN
    }

    private fun loadBitmapDownsampled(uri: Uri, maxDim: Int): Bitmap? {
        return try {
            val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
            context.contentResolver.openInputStream(uri)?.use {
                BitmapFactory.decodeStream(it, null, opts)
            }
            var sample = 1
            while (opts.outWidth / sample > maxDim || opts.outHeight / sample > maxDim) {
                sample *= 2
            }
            val finalOpts = BitmapFactory.Options().apply {
                inSampleSize = sample
                inPreferredConfig = Bitmap.Config.RGB_565
            }
            context.contentResolver.openInputStream(uri)?.use {
                BitmapFactory.decodeStream(it, null, finalOpts)
            }
        } catch (e: Exception) {
            null
        }
    }
}

enum class ClusteringStrategy {
    PREMIUM_SOLO_ONLY,
    STANDARD_SOLO_ONLY,
    TWO_PHASE,
    LEGACY_ALL_FACES
}

data class DetectedFaceWithEmbedding(
    val imageId: String,
    val imageUri: String,
    val capturedAt: Long,
    val embedding: FloatArray,
    val boundingBox: android.graphics.Rect,
    val confidence: Float,
    val faceCount: Int = 1,
    val imageWidth: Int = 0,
    val imageHeight: Int = 0
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false
        other as DetectedFaceWithEmbedding
        return imageId == other.imageId
    }
    override fun hashCode(): Int = imageId.hashCode()
}

data class RawCluster(
    val clusterId: Int,
    val faces: List<DetectedFaceWithEmbedding>
)

data class FaceCluster(
    val clusterId: Int,
    val faces: List<DetectedFaceWithEmbedding>,
    val representativeFaces: List<DetectedFaceWithEmbedding>,
    val photoCount: Int,
    val averageConfidence: Float,
    val estimatedAge: AgeEstimate,
    val potentialSiblings: List<Int>
)

data class ClusteringResult(
    val clusters: List<FaceCluster>,
    val totalFacesAnalyzed: Int,
    val processingTimeMs: Long,
    val errorMessage: String? = null,
    val strategy: ClusteringStrategy = ClusteringStrategy.PREMIUM_SOLO_ONLY
)

enum class AgeEstimate {
    CHILD,
    ADULT,
    UNKNOWN
}