package kr.lunaticbum.back.lun.model

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactor.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import kr.lunaticbum.back.lun.utils.LogService
import org.bson.BsonType
import org.bson.codecs.pojo.annotations.BsonId
import org.bson.codecs.pojo.annotations.BsonRepresentation
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.annotation.Profile
import org.springframework.context.event.EventListener
import org.springframework.data.mongodb.core.mapping.Document
import org.springframework.data.mongodb.repository.Aggregation
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.io.File
import java.nio.file.Files
import java.util.concurrent.atomic.AtomicBoolean
import javax.imageio.ImageIO


@Document(collection = "ImageMeta") // 이미지 메타데이터를 저장할 컬렉션
data class ImageMeta(
    @BsonId
    @BsonRepresentation(BsonType.OBJECT_ID)
    var id: String? = null,

    var fileName: String,            // 저장된 파일명 (예: uuid.jpg)
    var originalFileName: String?,   // 원본 파일명
    var fileType: String?,           // MIME 타입 (예: image/jpeg)
    var fileSize: Long,              // 파일 크기 (bytes)
    var width: Int,                  // 이미지 가로 픽셀
    var height: Int,                 // 이미지 세로 픽셀
    var uploadTime: Long,            // 등록일시 (Timestamp)
    var path: String,                 // 이미지 접근 가능 URL 경로

    var isBannerCandidate: Boolean = false
)

/**
 * ImageMeta 컬렉션을 위한 Spring Data Repository
 */
interface ImageMetaRepository : ReactiveMongoRepository<ImageMeta, String> {

    @Aggregation(pipeline = [ "{ \$sample: { size: 1 } }" ])
    fun findRandomImage(): Mono<ImageMeta>

    // [신규 추가] isBannerCandidate가 true인 이미지 중에서만 랜덤으로 1개를 선택
    @Aggregation(pipeline = [
        "{ \$match: { isBannerCandidate: true } }",
        "{ \$sample: { size: 1 } }"
    ])
    fun findRandomBannerCandidate(): Mono<ImageMeta>

    fun findByFileName(fileName: String): Mono<ImageMeta>

    // [신규 추가] 파일 이름 리스트를 기반으로 문서를 전부 삭제하는 기능
    fun deleteAllByFileNameIn(fileNames: List<String>): Mono<Void>
}

/**
 * 이미지 메타데이터 로직을 처리할 서비스
 */
@Service
class ImageMetaService(
    private val repository: ImageMetaRepository,
    private val logService: LogService, // LogService 주입
    @Value("\${image.upload.path}") private val uploadPath: String, // application.properties의 업로드 경로 주입
    @Value("\${build.config.run}") private val build_config_run: String
) {

    // [신규 추가] 백그라운드 작업용 Coroutine Scope 정의
    private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    // [신규 추가] 동기화 작업이 중복 실행되는 것을 방지하기 위한 Atomic Lock
    private val isSyncRunning = AtomicBoolean(false)

    /**
     * 공개 메소드: 메타데이터 저장 (BlogController에서 사용)
     */
    fun save(imageMeta: ImageMeta): Mono<ImageMeta> {
        return repository.save(imageMeta)
    }

    /**
     * 공개 메소드: 랜덤 이미지 가져오기 (Home 컨트롤러에서 사용)
     */
    fun getRandomImage(): Mono<ImageMeta> {
        return repository.findRandomImage()
    }

     // application.properties의 업로드 경로 주입
    /**
     * Spring Boot가 준비되었을 때(부팅 완료) 실행되는 리스너
     */
    @Profile("!local")
    @EventListener(ApplicationReadyEvent::class)
    fun onApplicationReady() {
        logService.log("Application ${build_config_run} ready.  Launching initial image DB sync task...")
        if (build_config_run.contains("prd")) {
            launchSyncTask()
        }
    }

    /**
     * 동기화 작업을 비동기(Coroutine)로 실행하는 런처 (잠금 처리)
     * BlogController에서도 이 함수를 호출할 수 있습니다.
     */
    fun launchSyncTask() {
        // AtomicBoolean을 사용해 현재 동기화 작업이 실행 중이 아닐 때만 true로 설정하고 새 작업을 시작
        if (isSyncRunning.compareAndSet(false, true)) {
            logService.log("Starting background image sync...")

            serviceScope.launch {
                try {
                    // 실제 동기화 로직 실행
                    runFileSystemSync()
                } catch (e: Exception) {
                    logService.log("Unhandled error in sync launcher: ${e.message}")
                } finally {
                    // 작업이 성공하든, 실패(중단)하든 항상 잠금을 해제하여 다음 작업을 허용
                    isSyncRunning.set(false)
                    logService.log("Background image sync finished. Lock released.")
                }
            }
        } else {
            logService.log("Skipping sync launch: Task is already running.")
        }
    }

    /**
     * 실제 파일 시스템과 DB를 동기화하는 핵심 로직
     */
    // ImageMetaService.kt의 runFileSystemSync 함수를 아래 내용으로 교체하세요.
    private suspend fun runFileSystemSync() {
        // 1. [변경] 실제 업로드 폴더에서 파일 '이름' 목록을 Set으로 가져옵니다.
        val physicalFilenames = File(uploadPath).listFiles { _, name ->
            !name.contains("_thumbnail.") && (name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png") || name.endsWith(".gif"))
        }?.map { it.name }?.toSet() ?: emptySet()

        // 2. [변경] DB에서 모든 이미지 '파일 이름'을 Set으로 가져옵니다.
        val dbFilenames = repository.findAll().map { it.fileName }.collectList().awaitSingle().toSet()

        // 3. 파일 시스템과 DB의 파일 목록이 완전히 일치하면 동기화가 필요 없습니다.
        if (physicalFilenames == dbFilenames) {
            logService.log("Image sync check: File lists are identical. No sync needed.")
            return
        }

        logService.log("Image sync: Mismatch detected. Syncing...")

        try {
            // 4. [추가] DB에 추가해야 할 파일 목록을 계산합니다. (실제 파일 O, DB X)
            val filesToAdd = physicalFilenames - dbFilenames
            if (filesToAdd.isNotEmpty()) {
                logService.log("Sync: Found ${filesToAdd.size} files to add to DB...")
                for (fileName in filesToAdd) {
                    val file = File(uploadPath, fileName)
                    if (file.exists()) {
                        val bufferedImage = ImageIO.read(file)
                        val metadata = ImageMeta(
                            fileName = file.name,
                            originalFileName = "Scanned from disk",
                            fileType = Files.probeContentType(file.toPath()),
                            fileSize = file.length(),
                            width = bufferedImage.width,
                            height = bufferedImage.height,
                            uploadTime = file.lastModified(),
                            path = "/blog/post/images/${file.name}"
                        )
                        repository.save(metadata).awaitSingle()
                    }
                }
            }

            // 5. [추가] DB에서 삭제해야 할 '고아 데이터' 목록을 계산합니다. (실제 파일 X, DB O)
            val filesToDelete = dbFilenames - physicalFilenames
            if (filesToDelete.isNotEmpty()) {
                logService.log("Sync: Found ${filesToDelete.size} orphan DB entries to delete...")
                repository.deleteAllByFileNameIn(filesToDelete.toList()).awaitSingleOrNull()
            }

            logService.log("Image sync finished successfully.")

        } catch (e: Exception) {
            logService.log("CRITICAL SYNC FAILED: ${e.message}. Halting sync task.")
            e.printStackTrace()
        }
    }

    /**
     * [이름 변경 및 로직 수정] 기존 getRandomImage -> getRandomBannerImage
     * 배너 후보로 지정된 이미지 중에서 랜덤으로 하나를 가져옵니다.
     */
    fun getRandomBannerImage(): Mono<ImageMeta> {
        return repository.findRandomBannerCandidate()
    }

    // [신규 추가] 관리자 페이지에서 모든 이미지를 조회하기 위한 메서드
    fun getAllImages(): Flux<ImageMeta> {
        return repository.findAll()
    }

    // [신규 추가] 특정 이미지를 배너 후보로 승인하는 메서드
    fun approveForBanner(imageId: String): Mono<ImageMeta> {
        return repository.findById(imageId).flatMap { image ->
            image.isBannerCandidate = true
            repository.save(image)
        }
    }

    // [신규 추가] 특정 이미지의 배너 후보 자격을 해제하는 메서드
    fun revokeBannerApproval(imageId: String): Mono<ImageMeta> {
        return repository.findById(imageId).flatMap { image ->
            image.isBannerCandidate = false
            repository.save(image)
        }
    }
}