package org.nstart.dep265.requestservice.service

import org.nstart.dep265.requestservice.config.OutboxProperties
import org.nstart.dep265.requestservice.entity.OutboxEventEntity
import org.nstart.dep265.requestservice.entity.OutboxEventStatus
import org.nstart.dep265.requestservice.repository.OutboxEventRepository
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.kafka.core.KafkaTemplate
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.Duration
import java.time.LocalDateTime
import java.util.concurrent.ExecutionException

@Service
class OutboxPublisherService(
    private val outboxEventRepository: OutboxEventRepository,

    private val kafkaTemplate: KafkaTemplate<String, String>,

    private val outboxProperties: OutboxProperties,

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

    @Scheduled(fixedDelayString = "\${pcp.outbox.publish-fixed-delay-ms:5000}")
    fun publishScheduledBatch() {
        publishPending()
    }

    fun publishPending(): Int {
        val batchSize = outboxProperties.publishBatchSize.coerceAtLeast(0)
        if (batchSize == 0) {
            return 0
        }

        return try {
            transactionOperations.execute(
                TransactionCallback {
                    val now = UtcDateTimes.now(clock)
                    val events = outboxEventRepository.findPublishableForUpdateSkipLocked(
                        eventType = RequestStatusOutboxService.REQUEST_STATUS_EVENT_TYPE,
                        now = now,
                        maxAttempts = outboxProperties.maxAttempts,
                        batchSize = batchSize,
                    ).distinctBy { it.id }

                    events.forEach { event ->
                        publishAndUpdateStatus(event, now)
                    }
                    events.size
                },
            ) ?: 0
        } catch (exception: Exception) {
            log.error("Failed to process REQUEST_TERMINAL_STATUS outbox event batch", exception)
            0
        }
    }

    private fun publishAndUpdateStatus(event: OutboxEventEntity, now: LocalDateTime) {
        try {
            kafkaTemplate
                .send(
                    outboxProperties.requestStatusTopic,
                    event.aggregateId.toString(),
                    event.payload.toString(),
                )
                .get()

            event.status = OutboxEventStatus.PUBLISHED
            event.publishedAt = now
            event.errorMessage = null
            outboxEventRepository.save(event)

            log.info(
                "Published REQUEST_TERMINAL_STATUS outbox event: eventId={}, requestId={}, topic={}",
                event.id,
                event.aggregateId,
                outboxProperties.requestStatusTopic,
            )
        } catch (exception: Exception) {
            val publishException = exception.unwrapExecutionException()

            event.attempts += 1
            event.status = OutboxEventStatus.FAILED
            event.errorMessage = publishException.shortMessage()
            // attempts >= maxAttempts → терминальный FAILED (выборка публикации его уже не вернёт);
            // иначе планируем повтор с backoff.
            if (event.attempts < outboxProperties.maxAttempts) {
                event.nextAttemptAt = now.plus(Duration.ofMillis(calculateBackoffMs(event.attempts)))
            }
            outboxEventRepository.save(event)

            val exhausted = event.attempts >= outboxProperties.maxAttempts
            log.warn(
                "Failed to publish REQUEST_TERMINAL_STATUS outbox event: eventId={}, requestId={}, " +
                    "topic={}, attempts={}, maxAttempts={}, exhausted={}, nextAttemptAt={}",
                event.id,
                event.aggregateId,
                outboxProperties.requestStatusTopic,
                event.attempts,
                outboxProperties.maxAttempts,
                exhausted,
                event.nextAttemptAt,
                publishException,
            )
        }
    }

    /**
     * Удваивает backoff от [OutboxProperties.initialBackoffMs] до [OutboxProperties.maxBackoffMs].
     */
    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 Exception.unwrapExecutionException(): Throwable {
        return if (this is ExecutionException && cause != null) {
            cause!!
        } else {
            this
        }
    }

    private fun Throwable.shortMessage(): String {
        val message = message?.takeIf { it.isNotBlank() } ?: javaClass.simpleName
        return message.take(MAX_ERROR_MESSAGE_LENGTH)
    }

    companion object {
        private const val MAX_ERROR_MESSAGE_LENGTH = 512
    }
}
