package space.nstart.pcp.routepassportconsumer.listener

import org.apache.kafka.clients.consumer.ConsumerRecord
import org.slf4j.LoggerFactory
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.kafka.support.Acknowledgment
import org.springframework.stereotype.Component
import space.nstart.pcp.routepassportconsumer.config.KafkaTopicsProperties
import space.nstart.pcp.routepassportconsumer.service.RoutePassportIngestionService
import space.nstart.pcp.routepassportconsumer.service.RoutePassportJsonParserService
import space.nstart.pcp.routepassportconsumer.service.RoutePassportMetrics
import space.nstart.pcp.routepassportconsumer.service.RoutePassportSourceRecordMetadata

/**
 * Kafka listener, который получает JSON паспорта маршрута и передаёт его в транзакционный ingestion.
 */
@Component
class RoutePassportMessageListener(

    /** Сервис парсинга входного JSON сообщения в итоговый DTO. */
    private val routePassportJsonParserService: RoutePassportJsonParserService,

    /** Сервис транзакционного сохранения паспорта маршрута и outbox-событий. */
    private val routePassportIngestionService: RoutePassportIngestionService,

    /** Метрики ingestion flow. */
    private val routePassportMetrics: RoutePassportMetrics,

    /** Набор Kafka topic'ов, который приходит из config server и валидируется при старте. */
    val kafkaTopicsProperties: KafkaTopicsProperties,
) {
    private val log = LoggerFactory.getLogger(this::class.java)

    /**
     * Обрабатывает входное сообщение Kafka.
     *
     * Offset подтверждается только после успешного возврата из transactional ingestion.
     */
    @KafkaListener(
        topics = ["#{__listener.kafkaTopicsProperties.input}"],
        groupId = "\${spring.kafka.consumer.group-id}",
    )
    fun listen(
        record: ConsumerRecord<String, String>,
        acknowledgment: Acknowledgment,
    ) {
        val rawPayload = record.value()
        val sourceRecord = RoutePassportSourceRecordMetadata(
            topic = record.topic(),
            partition = record.partition(),
            offset = record.offset(),
            key = record.key(),
        )

        try {
            log.debug(
                "Kafka record received: sourceTopic={}, partition={}, offset={}, key={}, payloadSize={}",
                sourceRecord.topic,
                sourceRecord.partition,
                sourceRecord.offset,
                sourceRecord.key,
                rawPayload.length,
            )

            val parsedMessage = routePassportJsonParserService.parse(rawPayload)
            val routePassportDto = parsedMessage.routePassportDto
            routePassportIngestionService.ingest(
                parsedMessage = parsedMessage,
                rawPayload = rawPayload,
                sourceRecord = sourceRecord,
            )

            // Transactional ingest commits before returning; only then the Kafka offset is acknowledged.
            acknowledgment.acknowledge()
            routePassportMetrics.incrementIngestSuccess()

            log.info(
                "Route passport ingested: sourceTopic={}, partition={}, offset={}, key={}, payloadSize={}, routeId={}, routeNameFull={}, traceId={}, correlationId={}",
                sourceRecord.topic,
                sourceRecord.partition,
                sourceRecord.offset,
                sourceRecord.key,
                rawPayload.length,
                routePassportDto.routeId,
                routePassportDto.routeNameFull,
                parsedMessage.metadata.traceId,
                parsedMessage.metadata.correlationId,
            )
        } catch (exception: Exception) {
            routePassportMetrics.incrementIngestFailure()
            log.error(
                "Failed to process Kafka record: sourceTopic={}, partition={}, offset={}, key={}, exceptionClass={}, exceptionMessage={}",
                sourceRecord.topic,
                sourceRecord.partition,
                sourceRecord.offset,
                sourceRecord.key,
                exception::class.java.name,
                exception.message,
                exception,
            )
            throw exception
        }
    }
}
