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 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 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 events = outboxEventRepository.findRequestCompletedPublishableForUpdateSkipLocked(
                        eventType = RequestCompletedOutboxService.REQUEST_COMPLETED_EVENT_TYPE,
                        batchSize = batchSize,
                    ).distinctBy { it.id }

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

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

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

            log.info(
                "Published REQUEST_COMPLETED outbox event: eventId={}, requestId={}, topic={}",
                event.id,
                event.aggregateId,
                outboxProperties.requestCompletedTopic,
            )
        } catch (exception: Exception) {
            val publishException = exception.unwrapExecutionException()
            event.status = OutboxEventStatus.FAILED
            event.errorMessage = publishException.shortMessage()
            outboxEventRepository.save(event)

            log.warn(
                "Failed to publish REQUEST_COMPLETED outbox event: eventId={}, requestId={}, topic={}",
                event.id,
                event.aggregateId,
                outboxProperties.requestCompletedTopic,
                publishException,
            )
        }
    }

    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
    }
}
