package org.nstart.dep265.requestservice.service

import org.nstart.dep265.requestservice.config.RequestExpiryProperties
import org.nstart.dep265.requestservice.entity.RequestStatus
import org.nstart.dep265.requestservice.repository.RequestRepository
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes
import java.time.Clock
import java.time.LocalDateTime

/**
 * Планировщик истечения заявок.
 *
 * [RequestLifecycleService.refreshStatus] вызывается только при матчинге маршрута, поэтому заявка,
 * к которой не пришло ни одного маршрута, никогда не уходила в EXPIRED после окончания окна.
 * Этот воркер периодически переводит такие заявки в EXPIRED.
 *
 * Несколько инстансов безопасны: батч строк захватывается через FOR UPDATE SKIP LOCKED, так что
 * параллельные инстансы разбирают непересекающиеся заявки.
 */
@Service
class RequestExpiryService(
    private val requestRepository: RequestRepository,

    private val requestLifecycleService: RequestLifecycleService,

    private val requestStatusOutboxService: RequestStatusOutboxService,

    private val requestExpiryProperties: RequestExpiryProperties,

    @param:Qualifier("requestExpiryTransactionOperations")
    private val transactionOperations: TransactionOperations,
    private val clock: Clock = Clock.systemUTC(),
) {
    private val log = LoggerFactory.getLogger(this::class.java)

    @Scheduled(fixedDelayString = "\${pcp.request.expiry.fixed-delay-ms:60000}")
    fun expireOverdueScheduledBatch() {
        if (!requestExpiryProperties.enabled) {
            return
        }
        expireOverdue()
    }

    /**
     * Переводит просроченные заявки (ACCEPTED/ACTIVE с истёкшим окном) в EXPIRED.
     *
     * @return число заявок, перешедших в EXPIRED за прогон.
     */
    fun expireOverdue(): Int {
        val batchSize = requestExpiryProperties.batchSize.coerceAtLeast(0)
        if (batchSize == 0) {
            return 0
        }
        val maxBatches = requestExpiryProperties.maxBatchesPerRun.coerceAtLeast(1)
        val now = UtcDateTimes.now(clock)

        var expiredTotal = 0
        var batchIndex = 0
        while (batchIndex < maxBatches) {
            val result = try {
                transactionOperations.execute(
                    TransactionCallback { expireBatch(now, batchSize) },
                ) ?: ExpireBatchResult.EMPTY
            } catch (exception: Exception) {
                log.error("Failed to process request expiry batch", exception)
                break
            }

            expiredTotal += result.expiredCount
            batchIndex += 1
            if (result.claimedCount < batchSize) {
                break
            }
        }

        if (expiredTotal > 0) {
            log.info("Request expiry sweep finished: expired={}", expiredTotal)
        }
        return expiredTotal
    }

    private fun expireBatch(now: LocalDateTime, batchSize: Int): ExpireBatchResult {
        val candidates = requestRepository.findExpirableForUpdateSkipLocked(now, batchSize)
        var expiredCount = 0

        candidates.forEach { request ->
            val refresh = requestLifecycleService.refreshStatus(request, now)
            if (refresh.statusChanged) {
                request.updatedAt = now
                requestRepository.save(request)
                if (request.status == RequestStatus.EXPIRED) {
                    expiredCount += 1
                    // Терминальный статус → уведомляем geoportal через тот же transactional outbox.
                    requestStatusOutboxService.createTerminalIfAbsent(request, now)
                    log.info(
                        "Request expired: requestId={}, endDateTime={}",
                        request.id,
                        request.endDateTime,
                    )
                }
            }
        }

        return ExpireBatchResult(claimedCount = candidates.size, expiredCount = expiredCount)
    }

    private data class ExpireBatchResult(
        val claimedCount: Int,
        val expiredCount: Int,
    ) {
        companion object {
            val EMPTY = ExpireBatchResult(claimedCount = 0, expiredCount = 0)
        }
    }
}
