package space.nstart.pcp.routepassportconsumer.service

import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp.routepassportconsumer.config.RoutePassportOutboxProperties
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxEntity
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxStatus
import space.nstart.pcp.routepassportconsumer.repository.RoutePassportOutboxRepository
import java.time.Duration
import java.time.LocalDateTime
import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes

/**
 * Owns transactional claim and status transitions for route passport outbox rows.
 */
@Service
class RoutePassportOutboxService(
    private val routePassportOutboxRepository: RoutePassportOutboxRepository,
    private val outboxProperties: RoutePassportOutboxProperties,
    private val routePassportMetrics: RoutePassportMetrics,
) {
    private val log = LoggerFactory.getLogger(this::class.java)

    /**
     * Claims due pending rows in one DB transaction using SELECT FOR UPDATE SKIP LOCKED.
     */
    @Transactional
    fun claimDueBatch(): List<RoutePassportOutboxEntity> {
        if (!outboxProperties.enabled) {
            log.debug("Route passport outbox claim skipped because worker is disabled")
            return emptyList()
        }

        val now = UtcDateTimes.now()
        val lockedUntil = now.plus(Duration.ofMillis(outboxProperties.lockTimeoutMs))
        val claimedRows = routePassportOutboxRepository.findDueForUpdateSkipLocked(outboxProperties.batchSize)

        claimedRows.forEach { outbox ->
            outbox.lockedUntil = lockedUntil
            outbox.updatedAt = now
        }

        val savedRows = routePassportOutboxRepository.saveAll(claimedRows).toList()

        log.debug(
            "Route passport outbox rows claimed: claimedCount={}, batchSize={}, lockedUntil={}",
            savedRows.size,
            outboxProperties.batchSize,
            lockedUntil,
        )
        savedRows.forEach { outbox ->
            log.debug(
                "Route passport outbox row claimed: outboxId={}, routeId={}, eventType={}, status={}, attempts={}, nextAttemptAt={}, lockedUntil={}",
                outbox.id,
                outbox.routeId,
                outbox.eventType,
                outbox.status,
                outbox.attempts,
                outbox.nextAttemptAt,
                outbox.lockedUntil,
            )
        }

        return savedRows
    }

    /**
     * Marks a successfully published row as terminally published.
     */
    @Transactional
    fun markPublished(outboxId: UUID) {
        val now = UtcDateTimes.now()
        val outbox = findOutbox(outboxId)

        outbox.status = RoutePassportOutboxStatus.PUBLISHED
        outbox.publishedAt = now
        outbox.lockedUntil = null
        outbox.lastError = null
        outbox.updatedAt = now

        routePassportOutboxRepository.save(outbox)
        routePassportMetrics.incrementOutboxPublished()

        log.info(
            "Route passport outbox status changed: outboxId={}, routeId={}, eventType={}, transition=PENDING->PUBLISHED, attempts={}, nextAttemptAt={}, lockedUntil={}, publishedAt={}",
            outbox.id,
            outbox.routeId,
            outbox.eventType,
            outbox.attempts,
            outbox.nextAttemptAt,
            outbox.lockedUntil,
            outbox.publishedAt,
        )
    }

    /**
     * Records a publish failure and either schedules bounded retry or marks the row as failed.
     */
    @Transactional
    fun markRetryOrFailed(outboxId: UUID, exception: Exception) {
        val now = UtcDateTimes.now()
        val outbox = findOutbox(outboxId)

        outbox.attempts += 1
        outbox.lastError = compactError(exception)
        outbox.lockedUntil = null
        outbox.updatedAt = now

        if (outbox.attempts >= outbox.maxAttempts) {
            outbox.status = RoutePassportOutboxStatus.FAILED
        } else {
            outbox.status = RoutePassportOutboxStatus.PENDING
            outbox.nextAttemptAt = now.plus(Duration.ofMillis(calculateBackoffMs(outbox.attempts)))
        }

        routePassportOutboxRepository.save(outbox)
        if (outbox.status == RoutePassportOutboxStatus.FAILED) {
            routePassportMetrics.incrementOutboxFailed()
        } else {
            routePassportMetrics.incrementOutboxRetry()
        }

        log.warn(
            "Route passport outbox status changed after publish failure: outboxId={}, routeId={}, eventType={}, transition=LOCKED->{}, attempts={}, maxAttempts={}, nextAttemptAt={}, lockedUntil={}, exceptionClass={}, exceptionMessage={}",
            outbox.id,
            outbox.routeId,
            outbox.eventType,
            outbox.status,
            outbox.attempts,
            outbox.maxAttempts,
            outbox.nextAttemptAt,
            outbox.lockedUntil,
            exception::class.java.name,
            exception.message,
        )
    }

    private fun findOutbox(outboxId: UUID): RoutePassportOutboxEntity =
        routePassportOutboxRepository.findById(outboxId)
            .orElseThrow { IllegalStateException("Route passport outbox row not found: $outboxId") }

    private fun calculateBackoffMs(attempts: Int): Long {
        var backoffMs = outboxProperties.initialBackoffMs.coerceAtMost(outboxProperties.maxBackoffMs)

        repeat((attempts - 1).coerceAtLeast(0)) {
            backoffMs = if (backoffMs >= outboxProperties.maxBackoffMs / 2) {
                outboxProperties.maxBackoffMs
            } else {
                (backoffMs * 2).coerceAtMost(outboxProperties.maxBackoffMs)
            }
        }

        return backoffMs
    }

    private fun compactError(exception: Exception): String =
        "${exception::class.java.name}: ${exception.message ?: ""}".take(MAX_ERROR_LENGTH)

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