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)

        // Пауза после REJECTED: цепочка стоит, пока оператор вручную не создаст исправленный план
        // (ручная выдача/промоут снимут флаг). Будущего автоматически быть не может — отдаём пусто.
        // Это единый источник паузы: воркер выдачи получает пустую проекцию и ничего не материализует.
        if (state?.pausedAfterReject == true) {
            return emptyList()
        }

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

    /**
     * «Сырые» окна ЗРВ КА из последнего снапшота — границы реальных зон, из которых оператор
     * вручную набирает окно нового плана. Пусто, если снапшота нет. В отличие от [project] это
     * не цепочка, а исходные окна (без подбора длительности и нахлёста).
     */
    @Transactional(readOnly = true)
    fun rawZrvWindows(spacecraftId: String): List<CalculatedPlan> {
        val snapshot = spacecraftVisibilitySnapshotRepository.findById(spacecraftId).orElse(null)
            ?: return emptyList()
        val points = visibilityPayloadParser.parse(spacecraftId, snapshot.payloadJson)
        return planCalculationService.rawWindows(points)
    }

    /**
     * Прогноз будущей цепочки, если новый план занял окно [afterPlan]: считается тем же
     * [PlanCalculationService.calculatePlansAfter] поверх текущего снапшота ЗРВ, но якорь — не
     * состояние КА, а переданное окно. Используется при создании плана новой цепочки, чтобы сразу
     * показать оператору, как ляжет цепочка «от него». Состояние КА не трогается (what-if).
     */
    @Transactional(readOnly = true)
    fun projectAfterWindow(spacecraftId: String, afterPlan: CalculatedPlan): List<CalculatedPlan> {
        val snapshot = spacecraftVisibilitySnapshotRepository.findById(spacecraftId).orElse(null)
            ?: return emptyList()
        val points = visibilityPayloadParser.parse(spacecraftId, snapshot.payloadJson)
        return planCalculationService.calculatePlansAfter(points, afterPlan)
    }

    /**
     * Якорь, после которого строится будущая цепочка: самое дальнее (по endTime) зафиксированное
     * окно КА среди ISSUING/WAITING_DECISION/AWAITING_LAYIN/LAID_IN.
     *
     * Берём именно крайнее зафиксированное окно (а не «принятую базу»), потому что выдач по КА может
     * быть несколько одновременно (исходный план + коррекция), и базой цепочка становится только на
     * закладке (LAID_IN). Якорение на крайнее окно не даёт проекции заново вывести уже выданный слот
     * и нарисовать его призрак-дубль PLANNED при дрейфе снапшота ЗРВ. Если зафиксированных окон нет —
     * откатываемся на `currentBasePlanId` (последняя заложенная база), иначе строим с нуля.
     */
    private fun resolveAnchorPlan(spacecraftId: String, state: SpacecraftPlanningStateEntity?): PlannedPlanEntity? {
        val committed = plannedPlanRepository
            .findAllBySpacecraftIdAndStatusIn(spacecraftId, COMMITTED_STATUSES)
            .maxByOrNull { plan -> plan.endTime }
        if (committed != null) {
            return committed
        }
        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 COMMITTED_STATUSES = setOf(
            PlannedPlanStatus.ISSUING,
            PlannedPlanStatus.WAITING_DECISION,
            PlannedPlanStatus.AWAITING_LAYIN,
            PlannedPlanStatus.LAID_IN
        )
    }
}
