package space.nstart.pcp_tgu_service.service

import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionTemplate
import space.nstart.pcp_tgu_service.config.PlanningProperties
import space.nstart.pcp_tgu_service.entity.AcceptedPlanSlotsUpdateJobEntity
import space.nstart.pcp_tgu_service.entity.AcceptedPlanSlotsUpdateJobStatus
import space.nstart.pcp_tgu_service.repository.AcceptedPlanSlotsUpdateJobRepository
import java.net.InetAddress
import java.time.LocalDateTime
import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes

/**
 * Scale-out safe worker, который запускает updateAcceptedPlanSlots для accepted plan.
 */
@Service
class AcceptedPlanSlotsUpdateWorker(
    private val planningProperties: PlanningProperties,
    private val acceptedPlanSlotsUpdateJobRepository: AcceptedPlanSlotsUpdateJobRepository,
    private val acceptedPlanSlotsUpdateProcessStarter: AcceptedPlanSlotsUpdateProcessStarter,
    private val transactionTemplate: TransactionTemplate
) {
    private val log = LoggerFactory.getLogger(javaClass)
    private val workerId = "${InetAddress.getLocalHost().hostName}-${UUID.randomUUID()}"

    /** Периодически claim'ит jobs и стартует короткий BPMN вне DB transaction. */
    @Scheduled(fixedDelayString = "\${planning.notification-check-fixed-delay-ms}")
    fun processPendingJobs() {
        val jobs = claimJobs(limit = 10)
        jobs.forEach { job -> processJob(job.planId) }
    }

    /** Claim видим для тестов; в production защищён FOR UPDATE SKIP LOCKED. */
    fun claimJobs(limit: Int): List<AcceptedPlanSlotsUpdateJobEntity> =
        transactionTemplate.execute {
            val now = UtcDateTimes.now()
            val jobs = acceptedPlanSlotsUpdateJobRepository.findClaimableForUpdate(now, limit)
            jobs.forEach { job ->
                job.status = AcceptedPlanSlotsUpdateJobStatus.RUNNING
                job.lockedBy = workerId
                job.lockedUntil = now.plusSeconds(planningProperties.workerLockTtlSeconds)
                job.attempts += 1
                job.updatedAt = now
                job.lastError = null
            }
            acceptedPlanSlotsUpdateJobRepository.saveAll(jobs)
            jobs
        }.orEmpty()

    private fun processJob(planId: UUID) {
        val processInstanceKey = try {
            val processInstanceKey = acceptedPlanSlotsUpdateProcessStarter.start(planId)
            processInstanceKey
        } catch (ex: Exception) {
            log.warn("Accepted plan slots update process start failed. planId={}", planId, ex)
            markAmbiguous(planId, null, ex)
            return
        }

        try {
            markDone(planId, processInstanceKey)
        } catch (ex: Exception) {
            log.warn("Accepted plan slots update job could not be marked DONE after process start. planId={}", planId, ex)
            markAmbiguous(planId, processInstanceKey, ex)
        }
    }

    private fun markDone(planId: UUID, processInstanceKey: Long) {
        transactionTemplate.executeWithoutResult {
            val now = UtcDateTimes.now()
            val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult
            job.status = AcceptedPlanSlotsUpdateJobStatus.DONE
            job.processInstanceKey = processInstanceKey
            job.lockedBy = null
            job.lockedUntil = null
            job.lastError = null
            job.updatedAt = now
            acceptedPlanSlotsUpdateJobRepository.save(job)
        }
    }

    private fun markAmbiguous(planId: UUID, processInstanceKey: Long?, ex: Exception) {
        transactionTemplate.executeWithoutResult {
            val now = UtcDateTimes.now()
            val job = acceptedPlanSlotsUpdateJobRepository.findById(planId).orElse(null) ?: return@executeWithoutResult
            job.status = AcceptedPlanSlotsUpdateJobStatus.AMBIGUOUS
            job.processInstanceKey = processInstanceKey ?: job.processInstanceKey
            job.lockedBy = null
            job.lockedUntil = null
            job.lastError = ex.message?.take(MAX_ERROR_LENGTH) ?: ex.javaClass.name
            job.updatedAt = now
            acceptedPlanSlotsUpdateJobRepository.save(job)
        }
    }

    private companion object {
        const val MAX_ERROR_LENGTH = 2_000
    }
}
