package space.nstart.pcp.complan.service

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionTemplate
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp.complan.apl.BasicPlanCalculator
import space.nstart.pcp.complan.db.entity.DynamicPlanIntervalEntity
import space.nstart.pcp.complan.db.entity.DynamicPlanRouteEntity
import space.nstart.pcp.complan.db.entity.DynamicPlanRunEntity
import space.nstart.pcp.complan.db.repository.DynamicPlanIntervalRepository
import space.nstart.pcp.complan.db.repository.DynamicPlanRouteRepository
import space.nstart.pcp.complan.db.repository.DynamicPlanRunRepository
import space.nstart.pcp.complan.types.ComplexPlanCalculationOutput
import space.nstart.pcp.complan.types.ComplexPlanCalculationRequestDTO
import space.nstart.pcp.complan.types.ComplexPlanCalculationResultDTO
import space.nstart.pcp.complan.types.DynamicPlanIntervalDTO
import space.nstart.pcp.complan.types.DynamicPlanRouteDTO
import space.nstart.pcp.complan.types.DynamicPlanRunDTO
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
import space.nstart.pcp.complan.types.IntervalItemDTO
import space.nstart.pcp.complan.types.SurveyContour
import java.time.LocalDateTime
import java.util.UUID
import java.util.concurrent.ExecutorService
import kotlin.math.roundToLong
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes

@Service
class DynamicPlanRunService(
    private val runRepository: DynamicPlanRunRepository,
    private val routeRepository: DynamicPlanRouteRepository,
    private val intervalRepository: DynamicPlanIntervalRepository,
    private val basePlan: BasicPlanCalculator,
    private val aplSettingsLoader: AplSettingsLoader,
    private val objectMapper: ObjectMapper,
    private val transactionTemplate: TransactionTemplate,
    private val dynamicPlanExecutor: ExecutorService
) {
    private val log = LoggerFactory.getLogger(this::class.java)
    private val satelliteIdsType = object : TypeReference<List<Long>>() {}

    fun startCalculation(request: ComplexPlanCalculationRequestDTO): DynamicPlanRunDTO {
        validateRequest(request)

        val runId = UUID.randomUUID()
        val createdRun = transactionTemplate.execute {
            runRepository.save(
                DynamicPlanRunEntity(
                    id = runId,
                    status = DynamicPlanRunStatus.PENDING,
                    requestId = request.requestId,
                    satelliteIds = objectMapper.writeValueAsString(request.satelliteIds.distinct()),
                    calculationStart = request.calculationStart,
                    calculationEnd = request.calculationEnd,
                    calculationMode = request.calculationMode,
                    revolutionMode = request.revolutionMode,
                    filterCoveredRoutes = request.filterCoveredRoutes,
                    createdAt = UtcDateTimes.now()
                )
            )
        } ?: throw ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create calculation run")

        dynamicPlanExecutor.submit {
            executeCalculation(runId)
        }

        return createdRun.toDto()
    }

    fun getRun(runId: UUID): DynamicPlanRunDTO =
        findRun(runId).toDto()

    fun listRuns(status: DynamicPlanRunStatus?, limit: Int, offset: Int): List<DynamicPlanRunDTO> {
        validatePage(limit, offset)
        val runs = when (status) {
            null -> runRepository.findRuns(limit, offset)
            else -> runRepository.findRunsByStatus(status.name, limit, offset)
        }
        return runs.map { run -> run.toDto() }
    }

    fun getResult(runId: UUID): ComplexPlanCalculationResultDTO {
        val run = findRun(runId)
        if (run.status != DynamicPlanRunStatus.COMPLETED) {
            throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
        }

        val resultJson = run.resultJson
            ?: throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId has no result")

        val resultNode = objectMapper.readTree(resultJson)
        if (resultNode is ObjectNode && !resultNode.has("calculationMode")) {
            resultNode.put("calculationMode", run.calculationMode.name)
        }
        if (resultNode is ObjectNode && !resultNode.has("revolutionMode")) {
            resultNode.put("revolutionMode", run.revolutionMode.name)
        }
        if (resultNode is ObjectNode && !resultNode.has("filterCoveredRoutes")) {
            resultNode.put("filterCoveredRoutes", run.filterCoveredRoutes)
        }

        return objectMapper.treeToValue(resultNode, ComplexPlanCalculationResultDTO::class.java)
    }

    fun getRoutes(runId: UUID, limit: Int, offset: Int): List<DynamicPlanRouteDTO> {
        validatePage(limit, offset)
        val run = findRun(runId)
        if (run.status != DynamicPlanRunStatus.COMPLETED) {
            throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
        }
        return routeRepository.findRoutesByRunId(runId, limit, offset)
            .map { route -> route.toDto() }
    }

    fun getIntervals(runId: UUID, limit: Int, offset: Int): List<DynamicPlanIntervalDTO> {
        validatePage(limit, offset)
        val run = findRun(runId)
        if (run.status != DynamicPlanRunStatus.COMPLETED) {
            throw ResponseStatusException(HttpStatus.CONFLICT, "Calculation $runId is not completed")
        }
        return intervalRepository.findIntervalsByRunId(runId, limit, offset)
            .map { interval -> interval.toDto() }
    }

    fun recoverInterruptedRuns() {
        val recoveredCount = transactionTemplate.execute {
            val runs = runRepository.findAllByStatusIn(
                listOf(DynamicPlanRunStatus.PENDING, DynamicPlanRunStatus.RUNNING)
            )
            if (runs.isEmpty()) {
                return@execute 0
            }

            val finishedAt = UtcDateTimes.now()
            runs.forEach { run ->
                run.status = DynamicPlanRunStatus.FAILED
                run.finishedAt = finishedAt
                run.errorMessage = "Calculation was interrupted by service restart"
            }
            runRepository.saveAll(runs)
            runs.size
        } ?: 0

        if (recoveredCount > 0) {
            log.warn("Recovered {} interrupted dynamic plan runs after startup", recoveredCount)
        }
    }

    private fun executeCalculation(runId: UUID) {
        val request = markRunningAndBuildRequest(runId)
        if (request == null) {
            log.warn("Dynamic plan run {} was not found before execution", runId)
            return
        }

        try {
            aplSettingsLoader.loadLatestSettings()
            val output = basePlan.calculate(request)
            markCompleted(runId, output)
        } catch (error: Exception) {
            log.error("Dynamic plan run {} failed", runId, error)
            markFailed(runId, error)
        }
    }

    private fun markRunningAndBuildRequest(runId: UUID): ComplexPlanCalculationRequestDTO? =
        transactionTemplate.execute {
            val run = runRepository.findById(runId).orElse(null) ?: return@execute null
            run.status = DynamicPlanRunStatus.RUNNING
            run.startedAt = UtcDateTimes.now()
            run.errorMessage = null
            runRepository.save(run)
            run.toRequest()
        }

    private fun markCompleted(runId: UUID, output: ComplexPlanCalculationOutput) {
        transactionTemplate.execute {
            val run = runRepository.findById(runId).orElseThrow()
            val result = output.result
            run.status = DynamicPlanRunStatus.COMPLETED
            run.finishedAt = UtcDateTimes.now()
            run.durationMs = result.durationMs
            run.resultStatus = result.status.name
            run.routesCount = result.routesCount
            run.resultJson = objectMapper.writeValueAsString(result)
            run.errorMessage = null
            intervalRepository.saveAll(
                output.intervals.mapIndexed { index, interval -> interval.toEntity(run, result.requestId, index) }
            )
            routeRepository.saveAll(output.routes.mapIndexed { index, route -> route.toEntity(run, result.requestId, index) })
            runRepository.save(run)
        }
    }

    private fun markFailed(runId: UUID, error: Exception) {
        transactionTemplate.execute {
            val run = runRepository.findById(runId).orElseThrow()
            run.status = DynamicPlanRunStatus.FAILED
            run.finishedAt = UtcDateTimes.now()
            run.errorMessage = error.message ?: error::class.java.simpleName
            runRepository.save(run)
        }
    }

    private fun findRun(runId: UUID): DynamicPlanRunEntity =
        runRepository.findById(runId)
            .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Calculation $runId was not found") }

    private fun validateRequest(request: ComplexPlanCalculationRequestDTO) {
        if (request.calculationEnd <= request.calculationStart) {
            throw ResponseStatusException(HttpStatus.BAD_REQUEST, "calculationEnd must be after calculationStart")
        }
        if (request.satelliteIds.isEmpty()) {
            throw ResponseStatusException(HttpStatus.BAD_REQUEST, "satelliteIds must not be empty")
        }
    }

    private fun validatePage(limit: Int, offset: Int) {
        if (limit !in 1..10_000) {
            throw ResponseStatusException(HttpStatus.BAD_REQUEST, "limit must be between 1 and 10000")
        }
        if (offset < 0) {
            throw ResponseStatusException(HttpStatus.BAD_REQUEST, "offset must be greater than or equal to 0")
        }
    }

    private fun DynamicPlanRunEntity.toRequest(): ComplexPlanCalculationRequestDTO =
        ComplexPlanCalculationRequestDTO(
            requestId = requestId ?: throw IllegalStateException("Run $id has no requestId"),
            satelliteIds = readSatelliteIds(satelliteIds),
            calculationStart = calculationStart ?: throw IllegalStateException("Run $id has no calculationStart"),
            calculationEnd = calculationEnd ?: throw IllegalStateException("Run $id has no calculationEnd"),
            calculationMode = calculationMode,
            revolutionMode = revolutionMode,
            filterCoveredRoutes = filterCoveredRoutes
        )

    private fun DynamicPlanRunEntity.toDto(): DynamicPlanRunDTO =
        DynamicPlanRunDTO(
            runId = id ?: throw IllegalStateException("Run has no id"),
            status = status,
            requestId = requestId ?: throw IllegalStateException("Run $id has no requestId"),
            satelliteIds = readSatelliteIds(satelliteIds),
            calculationStart = calculationStart ?: throw IllegalStateException("Run $id has no calculationStart"),
            calculationEnd = calculationEnd ?: throw IllegalStateException("Run $id has no calculationEnd"),
            calculationMode = calculationMode,
            revolutionMode = revolutionMode,
            filterCoveredRoutes = filterCoveredRoutes,
            createdAt = createdAt,
            startedAt = startedAt,
            finishedAt = finishedAt,
            durationMs = durationMs,
            resultStatus = resultStatus,
            routesCount = routesCount,
            errorMessage = errorMessage
        )

    private fun readSatelliteIds(value: String): List<Long> =
        objectMapper.readValue(value, satelliteIdsType)

    private fun IntervalItemDTO.toEntity(
        run: DynamicPlanRunEntity,
        requestId: UUID,
        intervalOrder: Int
    ): DynamicPlanIntervalEntity =
        DynamicPlanIntervalEntity(
            run = run,
            requestId = requestId,
            intervalId = id,
            regionId = regionId,
            revSign = revSign,
            contourWkt = geometry,
            observationParametersCount = mpl.size,
            intervalOrder = intervalOrder
        )

    private fun SurveyContour.toEntity(
        run: DynamicPlanRunEntity,
        requestId: UUID,
        routeOrder: Int
    ): DynamicPlanRouteEntity =
        DynamicPlanRouteEntity(
            run = run,
            requestId = requestId,
            satelliteId = mpl.noradId,
            startTime = time,
            endTime = time.plusNanos((duration * 1_000_000_000.0).roundToLong().coerceAtLeast(1L)),
            duration = duration,
            revolution = rev,
            roll = gamma,
            contourWkt = geometry,
            routeOrder = routeOrder
        )

    private fun DynamicPlanIntervalEntity.toDto(): DynamicPlanIntervalDTO =
        DynamicPlanIntervalDTO(
            id = id ?: throw IllegalStateException("Interval has no id"),
            runId = run?.id ?: throw IllegalStateException("Interval $id has no run id"),
            requestId = requestId ?: throw IllegalStateException("Interval $id has no request id"),
            intervalId = intervalId ?: throw IllegalStateException("Interval $id has no interval id"),
            regionId = regionId ?: throw IllegalStateException("Interval $id has no region id"),
            revSign = revSign,
            contourWkt = contourWkt,
            observationParametersCount = observationParametersCount
        )

    private fun DynamicPlanRouteEntity.toDto(): DynamicPlanRouteDTO =
        DynamicPlanRouteDTO(
            id = id ?: throw IllegalStateException("Route has no id"),
            runId = run?.id ?: throw IllegalStateException("Route $id has no run id"),
            requestId = requestId ?: throw IllegalStateException("Route $id has no request id"),
            satelliteId = satelliteId,
            startTime = startTime,
            endTime = endTime,
            duration = duration,
            revolution = revolution,
            roll = roll,
            contourWkt = contourWkt
        )
}
