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.JobStatus
import space.nstart.pcp_tgu_service.entity.VisibilityRefreshJobEntity
import space.nstart.pcp_tgu_service.integration.api.ExternalPointsClient
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import space.nstart.pcp_tgu_service.repository.VisibilityRefreshJobRepository
import java.net.InetAddress
import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes

/**
 * Claim'ит visibility_refresh_job, читает raw payload у баллистики и сохраняет snapshot.
 */
@Service
class VisibilityRefreshWorker(
    private val planningProperties: PlanningProperties,
    private val visibilityRefreshJobRepository: VisibilityRefreshJobRepository,
    private val spacecraftSnapshotRepository: SpacecraftSnapshotRepository,
    private val externalPointsClient: ExternalPointsClient,
    private val spacecraftVisibilitySnapshotService: SpacecraftVisibilitySnapshotService,
    private val transactionTemplate: TransactionTemplate
) {
    private val log = LoggerFactory.getLogger(javaClass)
    private val workerId = "${InetAddress.getLocalHost().hostName}-${java.util.UUID.randomUUID()}"

    /** Периодически обрабатывает refresh jobs; один job claim'ится только одним pod. */
    @Scheduled(fixedDelayString = "\${planning.notification-check-fixed-delay-ms}")
    fun processPendingJobs() {
        val jobs = claimJobs(limit = 10)
        jobs.forEach { job -> processJob(job.spacecraftId) }
    }

    private fun claimJobs(limit: Int): List<VisibilityRefreshJobEntity> =
        transactionTemplate.execute {
            val now = UtcDateTimes.now()
            val jobs = visibilityRefreshJobRepository.findPendingForUpdate(now, limit)
            jobs.forEach { job ->
                job.status = JobStatus.RUNNING
                job.lockedBy = workerId
                job.lockedUntil = now.plusSeconds(planningProperties.workerLockTtlSeconds)
                job.attempts += 1
                job.updatedAt = now
            }
            visibilityRefreshJobRepository.saveAll(jobs)
            jobs
        }.orEmpty()

    private fun processJob(spacecraftId: String) {
        val job = visibilityRefreshJobRepository.findById(spacecraftId).orElse(null) ?: return
        val noradId = job.noradId
            ?: spacecraftSnapshotRepository.findById(spacecraftId).orElse(null)?.noradId
            ?: run {
                markFailed(spacecraftId, "Norad id is missing")
                return
            }

        try {
            val timeStart = UtcDateTimes.now().truncatedTo(ChronoUnit.MINUTES)
            val timeStop = timeStart.plusDays(planningProperties.externalPointsLookaheadDays)
            val payload = externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop)
            spacecraftVisibilitySnapshotService.saveSnapshot(spacecraftId, noradId, payload, UtcDateTimes.now())
            markDone(spacecraftId)
        } catch (ex: Exception) {
            log.warn("Visibility refresh job failed. spacecraftId={}, noradId={}", spacecraftId, noradId, ex)
            releaseOrFail(spacecraftId)
        }
    }

    private fun markDone(spacecraftId: String) {
        transactionTemplate.executeWithoutResult {
            val job = visibilityRefreshJobRepository.findById(spacecraftId).orElse(null) ?: return@executeWithoutResult
            job.status = if (job.rerunRequested) JobStatus.PENDING else JobStatus.DONE
            job.rerunRequested = false
            job.lockedBy = null
            job.lockedUntil = null
            job.updatedAt = UtcDateTimes.now()
            visibilityRefreshJobRepository.save(job)
        }
    }

    private fun releaseOrFail(spacecraftId: String) {
        transactionTemplate.executeWithoutResult {
            val job = visibilityRefreshJobRepository.findById(spacecraftId).orElse(null) ?: return@executeWithoutResult
            job.status = if (job.attempts >= MAX_ATTEMPTS) JobStatus.FAILED else JobStatus.PENDING
            job.lockedBy = null
            job.lockedUntil = null
            job.updatedAt = UtcDateTimes.now()
            visibilityRefreshJobRepository.save(job)
        }
    }

    private fun markFailed(spacecraftId: String, reason: String) {
        transactionTemplate.executeWithoutResult {
            val job = visibilityRefreshJobRepository.findById(spacecraftId).orElse(null) ?: return@executeWithoutResult
            job.status = JobStatus.FAILED
            job.reason = reason
            job.lockedBy = null
            job.lockedUntil = null
            job.updatedAt = UtcDateTimes.now()
            visibilityRefreshJobRepository.save(job)
        }
    }

    private companion object {
        const val MAX_ATTEMPTS = 3
    }
}
