package space.nstart.pcp_tgu_service.service

import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp_tgu_service.config.PlanningProperties
import space.nstart.pcp_tgu_service.dto.CreateDraftRequest
import space.nstart.pcp_tgu_service.dto.CreateDraftResponse
import space.nstart.pcp_tgu_service.dto.DraftMode
import space.nstart.pcp_tgu_service.entity.PlannedPlanEntity
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
import space.nstart.pcp_tgu_service.integration.api.MissionCloneRequest
import space.nstart.pcp_tgu_service.integration.api.MissionPlaningClient
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.util.UUID
import space.nstart.pcp.pcp_types_lib.utils.UtcDateTimes

/**
 * Создаёт черновик плана (DRAFT) на основе существующего базового плана.
 *
 * Режим **replace**: базовый план ещё правим (до точки невозврата) — черновик наследует
 * его окно (startTime/endTime/kppId) и предзаполняется клоном его миссии целиком.
 *
 * Режим **fork-forward**: базовый план невозвратен (заложен или прошло время) — черновик
 * создаётся на выбранной оператором будущей ЗРВ; клон миссии содержит только включения,
 * пересёкшиеся с окном новой ЗРВ.
 *
 * Атомарность: DRAFT-план создаётся только после успешного клона миссии; при сбое клона
 * план не сохраняется — осиротевших строк нет. Если сохранение плана падает после клона —
 * выполняется компенсация (удаление клона миссии).
 */
@Service
class CreateDraftUseCase(
    private val plannedPlanRepository: PlannedPlanRepository,
    private val planProjectionService: PlanProjectionService,
    private val missionPlaningClient: MissionPlaningClient,
    private val planningProperties: PlanningProperties,
    private val spacecraftPlanningStateJpaRepository: SpacecraftPlanningStateJpaRepository
) {
    private val log = LoggerFactory.getLogger(javaClass)

    fun createDraft(basePlanId: UUID, req: CreateDraftRequest): CreateDraftResponse {
        val base = plannedPlanRepository.findById(basePlanId)
            .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Plan $basePlanId not found") }

        val now = UtcDateTimes.now()

        return when (req.mode) {
            DraftMode.REPLACE -> createReplaceDraft(base, now)
            DraftMode.FORK_FORWARD -> createForkForwardDraft(base, req, now)
            else -> throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Unknown draft mode: ${req.mode}")
        }
    }

    private fun createReplaceDraft(base: PlannedPlanEntity, now: LocalDateTime): CreateDraftResponse {
        if (base.status == PlannedPlanStatus.DRAFT) {
            throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Нельзя создать черновик от черновика")
        }
        if (base.status == PlannedPlanStatus.LAID_IN) {
            throw ResponseStatusException(
                HttpStatus.UNPROCESSABLE_ENTITY,
                "План ${base.planId} заложен (LAID_IN) — используйте fork-forward"
            )
        }
        val freezeAt = base.startTime.minusMinutes(planningProperties.layinFreezeMinutes)
        if (!now.isBefore(freezeAt)) {
            throw ResponseStatusException(
                HttpStatus.UNPROCESSABLE_ENTITY,
                "До старта плана ${base.planId} осталось меньше ${planningProperties.layinFreezeMinutes} мин " +
                    "— окно выдачи заморожено, используйте fork-forward"
            )
        }

        // Идемпотентность: один открытый черновик на слот (КА + окно). Повторный клик не плодит
        // вторую миссию-клон и вторую DRAFT-строку. Окно replace-черновика 1:1 совпадает с базой.
        assertNoOpenDraftForWindow(base.spacecraftId, base.startTime, base.endTime)

        val cloneResponse = missionPlaningClient.cloneMission(
            missionId = base.planId,
            req = MissionCloneRequest(mode = DraftMode.REPLACE)
        )

        // Replace-черновик — это КОРРЕКЦИЯ исходного плана: он занимает тот же слот цепочки, что и
        // база, поэтому его предшественник (basePlanId) = предшественник базы (base.basePlanId), а
        // НЕ сама база. Так после закладки коррекция вытеснит исходный план на том же окне, а на
        // таймлайне стрелка коррекции ведёт к тому же предыдущему плану, что и у исходного.
        return saveDraftWithCompensation(
            draftPlanId = cloneResponse.newMissionId,
            base = base,
            draftBasePlanId = base.basePlanId,
            startTime = base.startTime,
            endTime = base.endTime,
            kppId = base.kppId,
            clonedMissionId = cloneResponse.newMissionId
        )
    }

    private fun createForkForwardDraft(
        base: PlannedPlanEntity,
        req: CreateDraftRequest,
        now: LocalDateTime
    ): CreateDraftResponse {
        val windowStart = req.windowStart
            ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "windowStart обязателен для fork-forward")
        val windowStop = req.windowStop
            ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "windowStop обязателен для fork-forward")
        val windowKppId = req.windowKppId
            ?: throw ResponseStatusException(HttpStatus.BAD_REQUEST, "windowKppId обязателен для fork-forward")

        if (!windowStart.isAfter(now)) {
            throw ResponseStatusException(
                HttpStatus.UNPROCESSABLE_ENTITY,
                "windowStart ($windowStart) должен быть строго в будущем"
            )
        }
        if (!windowStop.isAfter(windowStart)) {
            throw ResponseStatusException(
                HttpStatus.UNPROCESSABLE_ENTITY,
                "windowStop должен быть позже windowStart"
            )
        }

        // Валидируем, что выбранное окно соответствует реальной будущей ЗРВ из проекции.
        assertWindowMatchesProjection(base.spacecraftId, windowStart, windowStop, windowKppId)

        // Идемпотентность: один открытый черновик на слот (КА + окно будущей ЗРВ).
        assertNoOpenDraftForWindow(base.spacecraftId, windowStart, windowStop)

        val cloneResponse = missionPlaningClient.cloneMission(
            missionId = base.planId,
            req = MissionCloneRequest(mode = DraftMode.FORK_FORWARD, windowStart = windowStart, windowStop = windowStop)
        )

        // Fork-forward — это ПРОДОЛЖЕНИЕ цепочки на будущую ЗРВ ПОСЛЕ базы (база заложена/в прошлом),
        // поэтому предшественник нового плана = сама база (base.planId).
        return saveDraftWithCompensation(
            draftPlanId = cloneResponse.newMissionId,
            base = base,
            draftBasePlanId = base.planId,
            startTime = windowStart,
            endTime = windowStop,
            kppId = windowKppId,
            clonedMissionId = cloneResponse.newMissionId
        )
    }

    /**
     * Гард дублей: на (КА + окно) допускается не более одного открытого черновика. Заменяет прежний
     * гард «один черновик на базовый план» — теперь ключ слота это окно, а не исходный план.
     */
    private fun assertNoOpenDraftForWindow(spacecraftId: String, start: LocalDateTime, end: LocalDateTime) {
        val existing = plannedPlanRepository
            .findAllBySpacecraftIdAndStartTimeAndEndTimeAndStatus(spacecraftId, start, end, PlannedPlanStatus.DRAFT)
            .firstOrNull()
        if (existing != null) {
            throw ResponseStatusException(
                HttpStatus.CONFLICT,
                "По КА $spacecraftId уже есть черновик ${existing.planId} на это окно — завершите или отмените его"
            )
        }
    }

    /**
     * Проверяет, что тройка (windowStart, windowStop, kppId) соответствует хотя бы одному
     * плану в будущей проекции КА. Точность сравнения — 1 секунда (компенсирует погрешность
     * сериализации ISO-строк на фронте).
     */
    private fun assertWindowMatchesProjection(
        spacecraftId: String,
        windowStart: LocalDateTime,
        windowStop: LocalDateTime,
        kppId: String
    ) {
        val projection = planProjectionService.project(spacecraftId)
        val matched = projection.any { plan ->
            plan.kppId == kppId &&
                diffSeconds(plan.startTime, windowStart) < 1 &&
                diffSeconds(plan.endTime, windowStop) < 1
        }
        if (!matched) {
            throw ResponseStatusException(
                HttpStatus.UNPROCESSABLE_ENTITY,
                "Окно [$windowStart – $windowStop, kpp=$kppId] не соответствует ни одной будущей ЗРВ из проекции КА $spacecraftId"
            )
        }
    }

    private fun diffSeconds(a: LocalDateTime, b: LocalDateTime): Long =
        Math.abs(java.time.Duration.between(a, b).toSeconds())

    /**
     * Сохраняет DRAFT-план с переданным planId (= clonedMissionId, инвариант planId==missionId).
     * При сбое сохранения логирует компенсацию; удаление клона миссии — best-effort (сбой логируется).
     */
    private fun saveDraftWithCompensation(
        draftPlanId: UUID,
        base: PlannedPlanEntity,
        draftBasePlanId: UUID?,
        startTime: LocalDateTime,
        endTime: LocalDateTime,
        kppId: String,
        clonedMissionId: UUID
    ): CreateDraftResponse {
        // Снимок версии цепочки КА на момент создания черновика — оптимистичный токен для promote (Ф5):
        // любой ACCEPT/grace-сдвиг базы инкрементит chain_version, и «Применить» по устаревшему черновику
        // упадёт 409 «база уехала». chain_version у DRAFT хранит именно этот снимок (== живой при promote).
        val baseChainVersion = spacecraftPlanningStateJpaRepository.findById(base.spacecraftId)
            .map { it.chainVersion }
            .orElse(0L)

        return try {
            val draft = plannedPlanRepository.save(
                PlannedPlanEntity(
                    planId = draftPlanId,
                    spacecraftId = base.spacecraftId,
                    startTime = startTime,
                    endTime = endTime,
                    kppId = kppId,
                    status = PlannedPlanStatus.DRAFT,
                    basePlanId = draftBasePlanId,
                    chainVersion = baseChainVersion
                )
            )
            log.info(
                "Создан черновик плана: draftPlanId={}, srcPlanId={}, basePlanId={}, spacecraftId={}, chainVersion={}",
                draft.planId, base.planId, draftBasePlanId, base.spacecraftId, baseChainVersion
            )
            CreateDraftResponse(draftPlanId = draft.planId)
        } catch (ex: Exception) {
            log.error(
                "Сбой сохранения DRAFT-плана {} после клона миссии {}; попытка компенсации (удалить клон миссии)",
                draftPlanId, clonedMissionId, ex
            )
            try {
                missionPlaningClient.deleteMission(clonedMissionId)
            } catch (compEx: Exception) {
                log.error(
                    "Компенсация не удалась: клон миссии {} остался осиротевшим",
                    clonedMissionId, compEx
                )
            }
            throw ex
        }
    }
}
