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.pcp_types_lib.dto.routes.ParsedRoutePassportMessage
import space.nstart.pcp.pcp_types_lib.dto.routes.RoutePassportDto
import space.nstart.pcp.pcp_types_lib.message.KafkaMessage
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
import space.nstart.pcp.routepassportconsumer.config.KafkaTopicsProperties
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportEntity
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxEntity
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxStatus
import space.nstart.pcp.routepassportconsumer.repository.RoutePassportOutboxRepository
import space.nstart.pcp.routepassportconsumer.repository.RoutePassportRepository
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper as JpaJsonObjectMapper
import tools.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes

/**
 * Сохраняет разобранный route passport и outbox-события в одной DB transaction.
 */
@Service
class RoutePassportIngestionService(
    private val routePassportRepository: RoutePassportRepository,
    private val routePassportOutboxRepository: RoutePassportOutboxRepository,
    private val objectMapper: ObjectMapper,
    private val kafkaTopicsProperties: KafkaTopicsProperties,
) {
    private val log = LoggerFactory.getLogger(this::class.java)
    private val jpaJsonObjectMapper = JpaJsonObjectMapper().findAndRegisterModules()

    @Transactional
    fun ingest(
        parsedMessage: ParsedRoutePassportMessage,
        rawPayload: String,
        sourceRecord: RoutePassportSourceRecordMetadata,
    ) {
        val now = UtcDateTimes.now()
        val routePassportDto = parsedMessage.routePassportDto
        val existingRoutePassport = routePassportRepository.findById(routePassportDto.routeId)
        val routeCreated = existingRoutePassport.isEmpty

        val routePassportEntity = existingRoutePassport
            .map { existingEntity ->
                existingEntity.applyRoutePassport(
                    routePassportDto = routePassportDto,
                    parsedMessage = parsedMessage,
                    rawPayload = rawPayload,
                    sourceRecord = sourceRecord,
                    updatedAt = now,
                )
            }
            .orElseGet {
                routePassportDto.toEntity(
                    parsedMessage = parsedMessage,
                    rawPayload = rawPayload,
                    sourceRecord = sourceRecord,
                    now = now,
                )
            }

        routePassportRepository.save(routePassportEntity)

        val outboxResults = listOf(
            createOutboxIfAbsent(
                eventType = PcpKafkaEvent.ModeStatusChangedEvent,
                topic = kafkaTopicsProperties.processedRoute,
                parsedMessage = parsedMessage,
                now = now,
            ),
            createOutboxIfAbsent(
                eventType = PcpKafkaEvent.RouteGeoRefEvent,
                topic = kafkaTopicsProperties.routeGeoreference,
                parsedMessage = parsedMessage,
                now = now,
            ),
        )

        log.info(
            "Route passport persisted: routeId={}, routeNameFull={}, traceId={}, correlationId={}, sourceTopic={}, sourcePartition={}, sourceOffset={}, routeCreated={}, outboxEventsCreated={}, outboxEventsExisting={}",
            routePassportDto.routeId,
            routePassportDto.routeNameFull,
            parsedMessage.metadata.traceId,
            parsedMessage.metadata.correlationId,
            sourceRecord.topic,
            sourceRecord.partition,
            sourceRecord.offset,
            routeCreated,
            outboxResults.filter { it.created }.map { "${it.eventType}:${it.topic}" },
            outboxResults.filterNot { it.created }.map { "${it.eventType}:${it.topic}" },
        )
    }

    private fun createOutboxIfAbsent(
        eventType: PcpKafkaEvent,
        topic: String,
        parsedMessage: ParsedRoutePassportMessage,
        now: LocalDateTime,
    ): OutboxEventResult {
        val routeId = parsedMessage.routePassportDto.routeId
        val routeNameFull = parsedMessage.routePassportDto.routeNameFull
        val eventTypeName = eventType.name

        if (routePassportOutboxRepository.existsByRouteIdAndEventType(routeId, eventTypeName)) {
            log.debug(
                "Route passport outbox event already exists: routeId={}, routeNameFull={}, traceId={}, correlationId={}, eventType={}, topic={}",
                routeId,
                routeNameFull,
                parsedMessage.metadata.traceId,
                parsedMessage.metadata.correlationId,
                eventTypeName,
                topic,
            )
            return OutboxEventResult(eventType = eventTypeName, topic = topic, created = false)
        }

        val outbox = RoutePassportOutboxEntity(
            id = UUID.randomUUID(),
            routeId = routeId,
            eventType = eventTypeName,
            topic = topic,
            messageKey = routeId.toString(),
            payload = buildOutboxPayload(eventType, parsedMessage),
            headers = null,
            status = RoutePassportOutboxStatus.PENDING,
            attempts = 0,
            maxAttempts = 10,
            nextAttemptAt = now,
            lockedUntil = null,
            lastError = null,
            createdAt = now,
            updatedAt = now,
            publishedAt = null,
        )
        routePassportOutboxRepository.save(outbox)

        log.debug(
            "Route passport outbox event created: outboxId={}, routeId={}, routeNameFull={}, traceId={}, correlationId={}, eventType={}, topic={}, status={}",
            outbox.id,
            routeId,
            routeNameFull,
            parsedMessage.metadata.traceId,
            parsedMessage.metadata.correlationId,
            eventTypeName,
            topic,
            outbox.status,
        )

        return OutboxEventResult(eventType = eventTypeName, topic = topic, created = true)
    }

    private fun buildOutboxPayload(
        eventType: PcpKafkaEvent,
        parsedMessage: ParsedRoutePassportMessage,
    ): JsonNode {
        val message = KafkaMessage(
            type = eventType,
            data = parsedMessage.routePassportDto,
            traceId = parsedMessage.metadata.traceId,
            correlationId = parsedMessage.metadata.correlationId,
            group = "pcp",
        ).apply {
            source = "route-processing"
        }

        return toJpaJsonNode(message)
    }

    private fun toJpaJsonNode(value: Any): JsonNode =
        jpaJsonObjectMapper.readTree(objectMapper.writeValueAsString(value))

    private fun RoutePassportEntity.applyRoutePassport(
        routePassportDto: RoutePassportDto,
        parsedMessage: ParsedRoutePassportMessage,
        rawPayload: String,
        sourceRecord: RoutePassportSourceRecordMetadata,
        updatedAt: LocalDateTime,
    ): RoutePassportEntity {
        routeNameFull = routePassportDto.routeNameFull
        routeNameShort = routePassportDto.routeNameShort
        kaShort = routePassportDto.kaShort
        kaFull = routePassportDto.kaFull
        orbitNumber = routePassportDto.orbitNumber
        orbitState = routePassportDto.orbitState
        intervalBegin = routePassportDto.intervalBegin
        intervalEnd = routePassportDto.intervalEnd
        rollAngleMin = routePassportDto.rollAngle.min
        rollAngleMax = routePassportDto.rollAngle.max
        visirAngleMin = routePassportDto.visirAngle.min
        visirAngleMax = routePassportDto.visirAngle.max
        resolutionRange = routePassportDto.resolutionRange
        resolutionAzimuth = routePassportDto.resolutionAzimuth
        polarisation = toJpaJsonNode(routePassportDto.polarisation)
        processingLevel = routePassportDto.processingLevel
        geometryWkt = routePassportDto.geometry
        routeStatus = routePassportDto.routeStatus.name
        traceId = parsedMessage.metadata.traceId
        correlationId = parsedMessage.metadata.correlationId
        sourceTopic = sourceRecord.topic
        sourcePartition = sourceRecord.partition
        sourceOffset = sourceRecord.offset
        sourceKey = sourceRecord.key
        this.rawPayload = jpaJsonObjectMapper.readTree(rawPayload)
        this.updatedAt = updatedAt

        return this
    }

    private fun RoutePassportDto.toEntity(
        parsedMessage: ParsedRoutePassportMessage,
        rawPayload: String,
        sourceRecord: RoutePassportSourceRecordMetadata,
        now: LocalDateTime,
    ): RoutePassportEntity =
        RoutePassportEntity(
            routeId = routeId,
            routeNameFull = routeNameFull,
            routeNameShort = routeNameShort,
            kaShort = kaShort,
            kaFull = kaFull,
            orbitNumber = orbitNumber,
            orbitState = orbitState,
            intervalBegin = intervalBegin,
            intervalEnd = intervalEnd,
            rollAngleMin = rollAngle.min,
            rollAngleMax = rollAngle.max,
            visirAngleMin = visirAngle.min,
            visirAngleMax = visirAngle.max,
            resolutionRange = resolutionRange,
            resolutionAzimuth = resolutionAzimuth,
            polarisation = toJpaJsonNode(polarisation),
            processingLevel = processingLevel,
            geometryWkt = geometry,
            routeStatus = routeStatus.name,
            traceId = parsedMessage.metadata.traceId,
            correlationId = parsedMessage.metadata.correlationId,
            sourceTopic = sourceRecord.topic,
            sourcePartition = sourceRecord.partition,
            sourceOffset = sourceRecord.offset,
            sourceKey = sourceRecord.key,
            rawPayload = jpaJsonObjectMapper.readTree(rawPayload),
            createdAt = now,
            updatedAt = now,
        )

    private data class OutboxEventResult(
        val eventType: String,
        val topic: String,
        val created: Boolean,
    )
}
