package space.nstart.pcp_tgu_service.service

import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
import space.nstart.pcp_tgu_service.entity.PlannedPlanEntity
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
import space.nstart.pcp_tgu_service.entity.SpacecraftPlanningStateEntity
import space.nstart.pcp_tgu_service.integration.api.VisibilityPayloadParser
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftVisibilitySnapshotRepository

/**
 * Считает будущую цепочку планов КА на лету из последнего снапшота ЗРВ и текущей базы.
 *
 * Проекция нигде не сохраняется: это детерминированная функция от (снапшот + база + конфиг).
 * Её используют read-путь (UI) и воркер выдачи, чтобы понять, какой план выдавать следующим.
 */
@Service
class PlanProjectionService(
    private val spacecraftVisibilitySnapshotRepository: SpacecraftVisibilitySnapshotRepository,
    private val spacecraftPlanningStateJpaRepository: SpacecraftPlanningStateJpaRepository,
    private val plannedPlanRepository: PlannedPlanRepository,
    private val visibilityPayloadParser: VisibilityPayloadParser,
    private val planCalculationService: PlanCalculationService
) {
    /** Полная проекция будущей цепочки для КА; пусто, если нет снапшота или окон. */
    @Transactional(readOnly = true)
    fun project(spacecraftId: String): List<CalculatedPlan> {
        val snapshot = spacecraftVisibilitySnapshotRepository.findById(spacecraftId).orElse(null)
            ?: return emptyList()
        val points = visibilityPayloadParser.parse(spacecraftId, snapshot.payloadJson)

        val state = spacecraftPlanningStateJpaRepository.findById(spacecraftId).orElse(null)
        val anchorPlan = resolveAnchorPlan(state)

        return if (anchorPlan == null) {
            planCalculationService.calculatePlans(points)
        } else {
            planCalculationService.calculatePlansAfter(points, anchorPlan.toCalculatedPlan())
        }
    }

    /**
     * Якорь, после которого строится будущая цепочка: самая дальняя уже зафиксированная точка.
     *
     * Если есть выданный, но ещё не принятый план в полёте (ISSUING/WAITING_DECISION) — цепочку
     * продолжаем ПОСЛЕ него, а не после принятого базового плана. Иначе проекция заново вывела бы
     * уже выданный слот и при дрейфе снапшота ЗРВ (сместился КПП или границы окна) нарисовала бы
     * его призрак-дубль PLANNED рядом с реальным WAITING_DECISION: точечный дедуп по
     * businessKey в [PlanQueryService] такой рассинхрон не ловит.
     *
     * `activePlanId` заполнен только вместе с активной выдачей; в пути выдачи он к моменту
     * вызова проекции уже снят, поэтому якорение на план в полёте не влияет на выбор головы.
     */
    private fun resolveAnchorPlan(state: SpacecraftPlanningStateEntity?): PlannedPlanEntity? {
        val inFlight = state?.activePlanId
            ?.let { planId -> plannedPlanRepository.findById(planId).orElse(null) }
            ?.takeIf { plan -> plan.status in IN_FLIGHT_STATUSES }
        if (inFlight != null) {
            return inFlight
        }
        return state?.currentBasePlanId
            ?.let { planId -> plannedPlanRepository.findById(planId).orElse(null) }
    }

    private fun PlannedPlanEntity.toCalculatedPlan(): CalculatedPlan =
        CalculatedPlan(
            spacecraftId = spacecraftId,
            startTime = startTime,
            endTime = endTime,
            kppId = kppId
        )

    private companion object {
        /** Статусы выданного, но ещё не принятого плана — он уже фиксирует слот цепочки. */
        val IN_FLIGHT_STATUSES = setOf(PlannedPlanStatus.ISSUING, PlannedPlanStatus.WAITING_DECISION)
    }
}
