package com.placeholder.sherpai2.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.hilt.work.HiltWorker
import androidx.work.*
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.FaceModelDao
import com.placeholder.sherpai2.data.local.dao.PersonDao
import com.placeholder.sherpai2.domain.clustering.FaceQualityFilter
import com.placeholder.sherpai2.ml.FaceNormalizer
import com.placeholder.sherpai2.data.local.dao.ImageDao
import com.placeholder.sherpai2.data.local.dao.PhotoFaceTagDao
import com.placeholder.sherpai2.data.local.entity.PhotoFaceTagEntity
import com.placeholder.sherpai2.ml.FaceNetModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext

/**
 * LibraryScanWorker - Full library background scan for a trained person
 *
 * PURPOSE: After user approves validation preview, scan entire library
 *
 * STRATEGY:
 * 1. Load all photos with faces (from cache)
 * 2. Scan each photo for the trained person
 * 3. Create PhotoFaceTagEntity for matches
 * 4. Progressive updates to "People" tab
 * 5. Supports pause/resume via WorkManager
 *
 * SCHEDULING:
 * - Runs in background with progress notifications
 * - Can be cancelled by user
 * - Automatically retries on failure
 *
 * INPUT DATA:
 * - personId: String (UUID)
 * - personName: String (for notifications)
 * - threshold: Float (optional, default 0.70)
 *
 * OUTPUT DATA:
 * - matchesFound: Int
 * - photosScanned: Int
 * - errorMessage: String? (if failed)
 */
@HiltWorker
class LibraryScanWorker @AssistedInject constructor(
    @Assisted private val context: Context,
    @Assisted workerParams: WorkerParameters,
    private val imageDao: ImageDao,
    private val faceModelDao: FaceModelDao,
    private val photoFaceTagDao: PhotoFaceTagDao,
    private val personDao: PersonDao
) : CoroutineWorker(context, workerParams) {

    companion object {
        const val WORK_NAME_PREFIX = "library_scan_"
        const val KEY_PERSON_ID = "person_id"
        const val KEY_PERSON_NAME = "person_name"
        const val KEY_THRESHOLD = "threshold"
        const val KEY_PROGRESS_CURRENT = "progress_current"
        const val KEY_PROGRESS_TOTAL = "progress_total"
        const val KEY_MATCHES_FOUND = "matches_found"
        const val KEY_PHOTOS_SCANNED = "photos_scanned"

        private const val DEFAULT_THRESHOLD = 0.62f  // Solo photos
        private const val GROUP_THRESHOLD = 0.68f   // Group photos (stricter)
        private const val BATCH_SIZE = 20
        private const val MAX_RETRIES = 3

        /**
         * Create work request for library scan
         */
        fun createWorkRequest(
            personId: String,
            personName: String,
            threshold: Float = DEFAULT_THRESHOLD
        ): OneTimeWorkRequest {
            val inputData = workDataOf(
                KEY_PERSON_ID to personId,
                KEY_PERSON_NAME to personName,
                KEY_THRESHOLD to threshold
            )

            return OneTimeWorkRequestBuilder<LibraryScanWorker>()
                .setInputData(inputData)
                .setConstraints(
                    Constraints.Builder()
                        .setRequiresBatteryNotLow(true)  // Don't drain battery
                        .build()
                )
                .addTag(WORK_NAME_PREFIX + personId)
                .build()
        }
    }

    override suspend fun doWork(): Result = withContext(Dispatchers.Default) {
        try {
            // Get input parameters
            val personId = inputData.getString(KEY_PERSON_ID)
                ?: return@withContext Result.failure(
                    workDataOf("error" to "Missing person ID")
                )

            val personName = inputData.getString(KEY_PERSON_NAME) ?: "Unknown"
            val threshold = inputData.getFloat(KEY_THRESHOLD, DEFAULT_THRESHOLD)

            // Check if stopped
            if (isStopped) {
                return@withContext Result.failure()
            }

            // Step 1: Get face model
            val faceModel = withContext(Dispatchers.IO) {
                faceModelDao.getFaceModelByPersonId(personId)
            } ?: return@withContext Result.failure(
                workDataOf("error" to "Face model not found")
            )

            setProgress(workDataOf(
                KEY_PROGRESS_CURRENT to 0,
                KEY_PROGRESS_TOTAL to 100
            ))

            // Step 2: Get all photos with faces (from cache)
            val photosWithFaces = withContext(Dispatchers.IO) {
                imageDao.getImagesWithFaces()
            }

            if (photosWithFaces.isEmpty()) {
                return@withContext Result.success(
                    workDataOf(
                        KEY_MATCHES_FOUND to 0,
                        KEY_PHOTOS_SCANNED to 0
                    )
                )
            }

            // Step 2.5: Load person to check isChild flag
            val person = withContext(Dispatchers.IO) {
                personDao.getPersonById(personId)
            }
            val isChildTarget = person?.isChild ?: false

            // Step 3: Initialize ML components
            val faceNetModel = FaceNetModel(context)
            val detector = FaceDetection.getClient(
                FaceDetectorOptions.Builder()
                    .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
                    .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)  // Needed for age estimation
                    .setMinFaceSize(0.15f)
                    .build()
            )

            // Distribution-based minimum threshold (self-calibrating)
            val distributionMin = (faceModel.averageConfidence - 2 * faceModel.similarityStdDev)
                .coerceAtLeast(faceModel.similarityMin - 0.05f)
                .coerceAtLeast(0.50f)  // Never go below 0.50 absolute floor

            // Get ALL centroids for multi-centroid matching (critical for children)
            val modelCentroids = faceModel.getCentroids().map { it.getEmbeddingArray() }
            if (modelCentroids.isEmpty()) {
                return@withContext Result.failure(workDataOf("error" to "No centroids in model"))
            }

            // Load ALL other models for "best match wins" comparison
            // This prevents tagging siblings incorrectly
            val allModels = withContext(Dispatchers.IO) { faceModelDao.getAllActiveFaceModels() }
            val otherModelCentroids = allModels
                .filter { it.id != faceModel.id }
                .map { model -> model.id to model.getCentroids().map { it.getEmbeddingArray() } }

            var matchesFound = 0
            var photosScanned = 0

            try {
                // Step 4: Process in batches
                photosWithFaces.chunked(BATCH_SIZE).forEach { batch ->
                    if (isStopped) {
                        return@forEach
                    }

                    // Scan batch
                    batch.forEach { photo ->
                        try {
                            val tags = scanPhotoForPerson(
                                photo = photo,
                                personId = personId,
                                faceModelId = faceModel.id,
                                modelCentroids = modelCentroids,
                                otherModelCentroids = otherModelCentroids,
                                faceNetModel = faceNetModel,
                                detector = detector,
                                threshold = threshold,
                                distributionMin = distributionMin,
                                isChildTarget = isChildTarget
                            )

                            if (tags.isNotEmpty()) {
                                // Save tags
                                withContext(Dispatchers.IO) {
                                    photoFaceTagDao.insertTags(tags)
                                }
                                matchesFound += tags.size
                            }

                            photosScanned++

                            // Update progress
                            if (photosScanned % 10 == 0) {
                                val progress = (photosScanned * 100 / photosWithFaces.size)
                                setProgress(workDataOf(
                                    KEY_PROGRESS_CURRENT to photosScanned,
                                    KEY_PROGRESS_TOTAL to photosWithFaces.size,
                                    KEY_MATCHES_FOUND to matchesFound
                                ))
                            }

                        } catch (e: Exception) {
                            // Skip failed photos, continue scanning
                        }
                    }
                }

                // Success!
                Result.success(
                    workDataOf(
                        KEY_MATCHES_FOUND to matchesFound,
                        KEY_PHOTOS_SCANNED to photosScanned
                    )
                )

            } finally {
                faceNetModel.close()
                detector.close()
            }

        } catch (e: Exception) {
            // Retry on failure
            if (runAttemptCount < MAX_RETRIES) {
                Result.retry()
            } else {
                Result.failure(
                    workDataOf("error" to (e.message ?: "Unknown error"))
                )
            }
        }
    }

    /**
     * Scan a single photo for the person
     */
    private suspend fun scanPhotoForPerson(
        photo: com.placeholder.sherpai2.data.local.entity.ImageEntity,
        personId: String,
        faceModelId: String,
        modelCentroids: List<FloatArray>,
        otherModelCentroids: List<Pair<String, List<FloatArray>>>,
        faceNetModel: FaceNetModel,
        detector: com.google.mlkit.vision.face.FaceDetector,
        threshold: Float,
        distributionMin: Float,
        isChildTarget: Boolean
    ): List<PhotoFaceTagEntity> = withContext(Dispatchers.IO) {

        try {
            // Load bitmap
            val bitmap = loadBitmapDownsampled(Uri.parse(photo.imageUri), 768)
                ?: return@withContext emptyList()

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

            if (faces.isEmpty()) {
                bitmap.recycle()
                return@withContext emptyList()
            }

            // Use higher threshold for group photos
            val isGroupPhoto = faces.size > 1
            val effectiveThreshold = if (isGroupPhoto) GROUP_THRESHOLD else threshold

            // Track best match (only tag ONE face per image to avoid false positives)
            var bestMatch: PhotoFaceTagEntity? = null
            var bestSimilarity = 0f

            // Check each face (filter by quality first)
            for (face in faces) {
                // Quality check
                if (!FaceQualityFilter.validateForScanning(face, bitmap.width, bitmap.height)) {
                    continue
                }

                // Skip very small faces
                val faceArea = face.boundingBox.width() * face.boundingBox.height()
                val imageArea = bitmap.width * bitmap.height
                if (faceArea.toFloat() / imageArea < 0.02f) continue

                // SIGNAL 2: Age plausibility check (if target is a child)
                if (isChildTarget) {
                    val ageGroup = FaceQualityFilter.estimateAgeGroup(face, bitmap.width, bitmap.height)
                    if (ageGroup == FaceQualityFilter.AgeGroup.ADULT) {
                        continue  // Reject clearly adult faces when searching for a child
                    }
                }

                try {
                    // Crop and normalize face for best recognition
                    val faceBitmap = FaceNormalizer.cropAndNormalize(bitmap, face)
                        ?: continue

                    // Generate embedding
                    val faceEmbedding = faceNetModel.generateEmbedding(faceBitmap)
                    faceBitmap.recycle()

                    // Match against target person's centroids
                    val targetSimilarity = modelCentroids.maxOfOrNull { centroid ->
                        faceNetModel.calculateSimilarity(faceEmbedding, centroid)
                    } ?: 0f

                    // SIGNAL 1: Distribution-based rejection
                    // If similarity is below (mean - 2*stdDev) or (min - 0.05), it's a statistical outlier
                    if (targetSimilarity < distributionMin) {
                        continue  // Too far below training distribution
                    }

                    // SIGNAL 3: Basic threshold check
                    if (targetSimilarity < effectiveThreshold) {
                        continue
                    }

                    // SIGNAL 4: "Best match wins" - check if any OTHER model scores higher
                    // This prevents tagging siblings incorrectly
                    val bestOtherSimilarity = otherModelCentroids.maxOfOrNull { (_, centroids) ->
                        centroids.maxOfOrNull { centroid ->
                            faceNetModel.calculateSimilarity(faceEmbedding, centroid)
                        } ?: 0f
                    } ?: 0f

                    val isTargetBestMatch = targetSimilarity > bestOtherSimilarity

                    // All signals must pass
                    if (isTargetBestMatch && targetSimilarity > bestSimilarity) {
                        bestSimilarity = targetSimilarity
                        bestMatch = PhotoFaceTagEntity.create(
                            imageId = photo.imageId,
                            faceModelId = faceModelId,
                            boundingBox = face.boundingBox,
                            confidence = targetSimilarity,
                            faceEmbedding = faceEmbedding
                        )
                    }
                } catch (e: Exception) {
                    // Skip this face
                }
            }

            bitmap.recycle()

            // Return only the best match (or empty)
            if (bestMatch != null) listOf(bestMatch) else emptyList()

        } catch (e: Exception) {
            emptyList()
        }
    }

    /**
     * Load bitmap with downsampling for memory efficiency
     */
    private fun loadBitmapDownsampled(uri: Uri, maxDim: Int): android.graphics.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
            }

            context.contentResolver.openInputStream(uri)?.use {
                BitmapFactory.decodeStream(it, null, finalOpts)
            }
        } catch (e: Exception) {
            null
        }
    }
}