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.domain.CalculatedPlan
import space.nstart.pcp_tgu_service.dto.CreateChainPlanRequest
import space.nstart.pcp_tgu_service.dto.CreateChainPlanResponse
import space.nstart.pcp_tgu_service.dto.DraftMode
import space.nstart.pcp_tgu_service.dto.PlanResponse
import space.nstart.pcp_tgu_service.entity.PlannedPlanEntity
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
import space.nstart.pcp_tgu_service.integration.api.MissionBlankRequest
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). Кандидат не меняет состояние КА: он становится рабочим только после ручной отправки
 * ([IssueDuePlanWorker.promoteDraftNow]) и приёмки (якорение цепочки в acceptPlan).
 *
 * Два режима наполнения:
 *  - **с базой** (`basePlanId != null`): клон включений прошлого плана, пересёкшихся с окном
 *    (тот же fork-forward-клон), `base_plan_id = basePlanId` (история тянется);
 *  - **без базы**: пустая миссия, `base_plan_id = null` (новая история с нуля).
 *
 * Атомарность как в [CreateDraftUseCase]: DRAFT сохраняется только после успешного создания миссии;
 * при сбое сохранения — компенсация (удаление миссии).
 */
@Service
class CreateChainPlanUseCase(
    private val plannedPlanRepository: PlannedPlanRepository,
    private val planProjectionService: PlanProjectionService,
    private val missionPlaningClient: MissionPlaningClient,
    private val spacecraftPlanningStateJpaRepository: SpacecraftPlanningStateJpaRepository
) {
    private val log = LoggerFactory.getLogger(javaClass)

    fun createChainPlan(req: CreateChainPlanRequest): CreateChainPlanResponse {
        val now = UtcDateTimes.now()

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

        // База (если задана) должна принадлежать тому же КА — иначе клон чужого содержимого.
        val base: PlannedPlanEntity? = req.basePlanId?.let { basePlanId ->
            val plan = plannedPlanRepository.findById(basePlanId)
                .orElseThrow { ResponseStatusException(HttpStatus.NOT_FOUND, "Базовый план $basePlanId не найден") }
            if (plan.spacecraftId != req.spacecraftId) {
                throw ResponseStatusException(
                    HttpStatus.UNPROCESSABLE_ENTITY,
                    "Базовый план $basePlanId принадлежит другому КА (${plan.spacecraftId}, ожидался ${req.spacecraftId})"
                )
            }
            if (plan.status == PlannedPlanStatus.DRAFT) {
                throw ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Нельзя взять за основу черновик")
            }
            plan
        }

        // Гард на дубли: сочетание (КА + начало + конец) уникально среди черновиков. Повторное создание
        // того же окна не плодит второй черновик — оператор не теряется в одинаковых кандидатах.
        val duplicate = plannedPlanRepository
            .findAllBySpacecraftIdAndStartTimeAndEndTimeAndStatus(
                req.spacecraftId, req.windowStart, req.windowStop, PlannedPlanStatus.DRAFT
            )
            .firstOrNull()
        if (duplicate != null) {
            throw ResponseStatusException(
                HttpStatus.CONFLICT,
                "Черновик на окно [${req.windowStart}–${req.windowStop}] для КА ${req.spacecraftId} уже есть " +
                    "(${duplicate.planId}) — откройте его или удалите"
            )
        }

        // Окно должно совпадать с реальными ЗРВ снапшота (старт+КПП одной зоны, конец — другой).
        val rawWindows = planProjectionService.rawZrvWindows(req.spacecraftId)
        if (!ZrvWindowMatcher.matches(rawWindows, req.windowStart, req.windowStop, req.windowKppId)) {
            throw ResponseStatusException(
                HttpStatus.UNPROCESSABLE_ENTITY,
                "Окно [${req.windowStart}–${req.windowStop}, kpp=${req.windowKppId}] не набирается из реальных ЗРВ КА ${req.spacecraftId}"
            )
        }

        // Создаём миссию-носитель содержимого: клон базы в окно либо пустая миссия.
        val newMissionId = if (base != null) {
            missionPlaningClient.cloneMission(
                missionId = base.planId,
                req = MissionCloneRequest(
                    mode = DraftMode.FORK_FORWARD,
                    windowStart = req.windowStart,
                    windowStop = req.windowStop
                )
            ).newMissionId
        } else {
            missionPlaningClient.createBlankMission(
                MissionBlankRequest(
                    satelliteId = parseSatelliteId(req.spacecraftId),
                    missionStart = req.windowStart,
                    missionStop = req.windowStop
                )
            ).newMissionId
        }

        val draft = saveDraftWithCompensation(req, base, newMissionId)

        // Прогноз будущей цепочки от окна нового плана (what-if, состояние КА не тронуто).
        val projected = planProjectionService
            .projectAfterWindow(req.spacecraftId, draft.toCalculatedPlan())
            .map(PlanResponse::projected)

        return CreateChainPlanResponse(draftPlanId = draft.planId, projectedChain = projected)
    }

    private fun saveDraftWithCompensation(
        req: CreateChainPlanRequest,
        base: PlannedPlanEntity?,
        newMissionId: UUID
    ): PlannedPlanEntity {
        // Снимок версии цепочки — оптимистичный токен для promote (как в CreateDraftUseCase).
        val baseChainVersion = spacecraftPlanningStateJpaRepository.findById(req.spacecraftId)
            .map { it.chainVersion }
            .orElse(0L)

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

    private fun parseSatelliteId(spacecraftId: String): Long =
        spacecraftId.toLongOrNull()
            ?: throw ResponseStatusException(
                HttpStatus.UNPROCESSABLE_ENTITY,
                "spacecraftId '$spacecraftId' не числовой — нельзя создать пустую миссию без базы"
            )

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