package org.nstart.dep265.requestservice.service

import org.locationtech.jts.geom.Geometry
import org.nstart.dep265.requestservice.dto.RouteDto
import org.nstart.dep265.requestservice.entity.RequestEntity
import org.nstart.dep265.requestservice.entity.RequestRouteMatchEntity
import org.nstart.dep265.requestservice.entity.RequestStatus
import org.nstart.dep265.requestservice.repository.RequestRepository
import org.nstart.dep265.requestservice.repository.RequestRouteMatchRepository
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.dao.DuplicateKeyException
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.LocalDateTime
import java.util.UUID
import kotlin.math.max
import kotlin.math.min

@Service
class RouteMatchingService(
    private val requestRepository: RequestRepository,

    private val requestRouteMatchRepository: RequestRouteMatchRepository,

    private val requestValidationService: RequestValidationService,

    private val geometryService: GeometryService,

    private val requestLifecycleService: RequestLifecycleService,

    private val requestStatusOutboxService: RequestStatusOutboxService,

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

    fun process(route: RouteDto) {
        requestValidationService.validate(route)

        val routeId = route.routeId
        val routeRollRange = route.rollAngle
        val matchedAt: LocalDateTime = UtcDateTimes.now(clock)
        val routeGeometry = geometryService.parsePolygonalGeometry(route.geometry)

        val candidateRequestIds = requestRepository.findRequestIdsForRoute(
            statuses = listOf(RequestStatus.ACCEPTED, RequestStatus.ACTIVE),
            routeBegin = route.intervalBegin,
            routeEnd = route.intervalEnd,
        )

        var actualMatches = 0

        candidateRequestIds.forEach { requestId ->
            if (requestRouteMatchRepository.existsByRequestIdAndRouteId(requestId, routeId)) {
                return@forEach
            }

            try {
                val contributed = routeMatchingTransactionOperations.execute(
                    TransactionCallback {
                        applyRouteToRequest(
                            requestId = requestId,
                            routeId = routeId,
                            routeGeometry = routeGeometry,
                            matchedAt = matchedAt,
                        )
                    },
                ) ?: false

                if (contributed) {
                    actualMatches += 1
                }
            } catch (exception: DataIntegrityViolationException) {
                if (!exception.isDuplicateRouteMatchViolation()) {
                    throw exception
                }
                log.debug(
                    "Route match already exists after concurrent insert: requestId={}, routeId={}",
                    requestId,
                    routeId,
                )
            }
        }

        log.info(
            "Route processed: routeId={}, candidatesMatched={}, actualMatches={}, roll={}",
            routeId,
            candidateRequestIds.size,
            actualMatches,
            routeRollRange,
        )
    }

    private fun applyRouteToRequest(
        requestId: UUID,
        routeId: UUID,
        routeGeometry: Geometry,
        matchedAt: LocalDateTime,
    ): Boolean {
        val request = requestRepository.findByIdForUpdate(requestId).orElse(null) ?: return false

        if (request.status !in ACTIVE_ROUTE_MATCHING_STATUSES || request.deletedAt != null) {
            return false
        }

        if (requestRouteMatchRepository.existsByRequestIdAndRouteId(request.id, routeId)) {
            return false
        }

        val originalGeometry = geometryService.parsePolygonalGeometry(request.geometry)
        val originalIntersection = geometryService.keepPolygonalGeometry(originalGeometry.intersection(routeGeometry))
        val originalIntersectionArea = geometryService.area(originalIntersection)

        if (!originalIntersection.hasPositiveArea(originalIntersectionArea)) {
            return false
        }

        val remainingGeometry = geometryService.parsePolygonalGeometry(request.remainingGeometry)
        val remainingIntersection = geometryService.keepPolygonalGeometry(remainingGeometry.intersection(routeGeometry))
        val remainingIntersectionArea = geometryService.area(remainingIntersection)

        if (!remainingIntersection.hasPositiveArea(remainingIntersectionArea)) {
            saveRouteMatch(
                request = request,
                routeId = routeId,
                originalIntersection = originalIntersection,
                originalIntersectionArea = originalIntersectionArea,
                appliedIntersection = null,
                appliedIntersectionArea = 0.0,
                coverageDeltaPercent = 0.0,
                contributesToCoverage = false,
                matchedAt = matchedAt,
            )
            return false
        }

        val updatedRemainingGeometry = geometryService.keepPolygonalGeometry(
            remainingGeometry.difference(remainingIntersection),
        )
        val updatedRemainingArea = max(geometryService.area(updatedRemainingGeometry), 0.0)
        val coverageDeltaPercent = coverageDeltaPercent(
            originalArea = request.geometryArea,
            appliedIntersectionArea = remainingIntersectionArea,
        )

        saveRouteMatch(
            request = request,
            routeId = routeId,
            originalIntersection = originalIntersection,
            originalIntersectionArea = originalIntersectionArea,
            appliedIntersection = remainingIntersection,
            appliedIntersectionArea = remainingIntersectionArea,
            coverageDeltaPercent = coverageDeltaPercent,
            contributesToCoverage = true,
            matchedAt = matchedAt,
        )

        request.remainingGeometry = geometryService.toWkt(updatedRemainingGeometry)
        request.remainingArea = updatedRemainingArea
        request.matchCount = request.matchCount + 1
        request.lastMatchedAt = matchedAt
        request.updatedAt = matchedAt

        val statusRefresh = requestLifecycleService.refreshStatus(request, matchedAt)
        requestRepository.save(request)
        if (statusRefresh.completedNow) {
            requestStatusOutboxService.createTerminalIfAbsent(request, matchedAt)
        }
        return true
    }

    private fun saveRouteMatch(
        request: RequestEntity,
        routeId: UUID,
        originalIntersection: Geometry,
        originalIntersectionArea: Double,
        appliedIntersection: Geometry?,
        appliedIntersectionArea: Double,
        coverageDeltaPercent: Double,
        contributesToCoverage: Boolean,
        matchedAt: LocalDateTime,
    ) {
        val requestRouteMatch = RequestRouteMatchEntity(
            requestId = request.id,
            routeId = routeId,
            originalIntersectionGeometry = geometryService.toWkt(originalIntersection),
            originalIntersectionArea = originalIntersectionArea,
            appliedIntersectionGeometry = appliedIntersection?.let { geometryService.toWkt(it) },
            appliedIntersectionArea = appliedIntersectionArea,
            coverageDeltaPercent = coverageDeltaPercent,
            contributesToCoverage = contributesToCoverage,
            matchedAt = matchedAt,
        )

        requestRouteMatchRepository.saveAndFlush(requestRouteMatch)
    }

    private fun coverageDeltaPercent(originalArea: Double, appliedIntersectionArea: Double): Double {
        if (originalArea <= 0.0) {
            return 0.0
        }

        val percent = (appliedIntersectionArea / originalArea) * 100.0
        return min(max(percent, 0.0), 100.0)
    }

    private fun Geometry.hasPositiveArea(area: Double): Boolean = !isEmpty && area > 0.0

    private fun DataIntegrityViolationException.isDuplicateRouteMatchViolation(): Boolean {
        if (this is DuplicateKeyException) {
            return true
        }

        val message = listOfNotNull(message, mostSpecificCause.message)
            .joinToString(separator = " ")
            .lowercase()

        return message.contains("uk_request_route_match") ||
            message.contains("request_id, route_id") ||
            (message.contains("duplicate") && message.contains("unique"))
    }

    companion object {
        private val ACTIVE_ROUTE_MATCHING_STATUSES = setOf(RequestStatus.ACCEPTED, RequestStatus.ACTIVE)
    }
}
