package space.nstart.pcp_tgu_service.service

import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp_tgu_service.config.PlanningProperties
import space.nstart.pcp_tgu_service.domain.PlanDecision
import space.nstart.pcp_tgu_service.dto.PlanDecisionMessage
import space.nstart.pcp_tgu_service.dto.PlanExecutionFeedbackMessage
import space.nstart.pcp_tgu_service.entity.PlanIssueAttemptEntity
import space.nstart.pcp_tgu_service.entity.PlanIssueAttemptStatus
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.repository.PlanIssueAttemptRepository
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime

/**
 * Обрабатывает decision и закладочные feedback-события идемпотентно по eventId.
 *
 * - ACCEPTED — план оптимистично становится базой (AWAITING_LAYIN), цепочка продолжается.
 * - REJECTED / «не заложен» / «не принят» — план помечается терминально, и на ближайшую ЗРВ
 *   запускается срочная перевыдача ([reissueOnNearestWindow]).
 */
@Service
class HandlePlanDecisionUseCase(
    private val processedPlanDecisionEventRepository: ProcessedPlanDecisionEventRepository,
    private val planIssueAttemptRepository: PlanIssueAttemptRepository,
    private val plannedPlanRepository: PlannedPlanRepository,
    private val spacecraftPlanningStateJpaRepository: SpacecraftPlanningStateJpaRepository,
    private val acceptedPlanSlotsUpdateJobService: AcceptedPlanSlotsUpdateJobService,
    private val planningProperties: PlanningProperties
) {
    private val log = LoggerFactory.getLogger(javaClass)

    /** Применяет решение по attempt; повтор того же eventId — успешный no-op. */
    @Transactional
    fun handle(message: PlanDecisionMessage) {
        if (!claimEvent(message.eventId)) {
            return
        }

        val attempt = planIssueAttemptRepository.lockByAttemptId(message.attemptId)
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Attempt ${message.attemptId} was not found")
        if (attempt.planId != message.planId) {
            throw ResponseStatusException(
                HttpStatus.CONFLICT,
                "Decision attemptId=${message.attemptId} does not belong to planId=${message.planId}"
            )
        }

        val plan = plannedPlanRepository.lockByPlanId(message.planId)
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Plan ${message.planId} was not found")
        if (plan.spacecraftId != message.spacecraftId) {
            throw ResponseStatusException(
                HttpStatus.CONFLICT,
                "Decision spacecraftId=${message.spacecraftId} does not match plan spacecraftId=${plan.spacecraftId}"
            )
        }

        val state = getOrCreateLockedState(message.spacecraftId)
        if (attempt.status.isTerminal()) {
            return
        }
        if (state.activeAttemptId != attempt.attemptId) {
            recordDecisionOnAttempt(message, attempt)
            planIssueAttemptRepository.save(attempt)
            return
        }

        when (message.decision) {
            PlanDecision.ACCEPTED -> acceptPlan(message, attempt, plan, state)
            PlanDecision.REJECTED -> rejectPlan(message, attempt, plan, state)
        }
    }

    /** Событие «не заложен» по базовому плану — срочная перевыдача на ближайшую ЗРВ. */
    @Transactional
    fun handleLayinFailed(message: PlanExecutionFeedbackMessage) {
        applyNegativeFeedback(message, PlannedPlanStatus.LAYIN_FAILED)
    }

    /** Событие «не принят» по базовому плану — пока поведение идентично «не заложен». */
    @Transactional
    fun handleNotAccepted(message: PlanExecutionFeedbackMessage) {
        applyNegativeFeedback(message, PlannedPlanStatus.NOT_ACCEPTED)
    }

    private fun applyNegativeFeedback(message: PlanExecutionFeedbackMessage, terminalStatus: PlannedPlanStatus) {
        if (!claimEvent(message.eventId)) {
            return
        }

        val plan = plannedPlanRepository.lockByPlanId(message.planId)
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Plan ${message.planId} was not found")
        if (plan.spacecraftId != message.spacecraftId) {
            throw ResponseStatusException(
                HttpStatus.CONFLICT,
                "Feedback spacecraftId=${message.spacecraftId} does not match plan spacecraftId=${plan.spacecraftId}"
            )
        }

        val state = getOrCreateLockedState(message.spacecraftId)
        if (state.currentBasePlanId != plan.planId || plan.status != PlannedPlanStatus.AWAITING_LAYIN) {
            // Событие пришло не по текущему базовому плану (устарело/дубль логики) — ничего не делаем.
            log.info(
                "Negative feedback ignored: plan {} is not the current AWAITING_LAYIN base (status={}, base={}).",
                plan.planId, plan.status, state.currentBasePlanId
            )
            return
        }

        val now = LocalDateTime.now()
        plan.status = terminalStatus
        plan.updatedAt = now
        plannedPlanRepository.save(plan)

        reissueOnNearestWindow(state, plan, now)
    }

    private fun recordDecisionOnAttempt(
        message: PlanDecisionMessage,
        attempt: PlanIssueAttemptEntity
    ) {
        val now = LocalDateTime.now()
        attempt.status = when (message.decision) {
            PlanDecision.ACCEPTED -> PlanIssueAttemptStatus.ACCEPTED
            PlanDecision.REJECTED -> PlanIssueAttemptStatus.REJECTED
        }
        attempt.finishedAt = message.decisionTime
        attempt.decisionEventId = message.eventId
        attempt.decisionReason = message.reason
        attempt.updatedAt = now
    }

    private fun acceptPlan(
        message: PlanDecisionMessage,
        attempt: PlanIssueAttemptEntity,
        plan: PlannedPlanEntity,
        state: SpacecraftPlanningStateEntity
    ) {
        val now = LocalDateTime.now()
        recordDecisionOnAttempt(message, attempt)

        plan.status = PlannedPlanStatus.AWAITING_LAYIN
        plan.acceptedAt = message.decisionTime
        plan.updatedAt = now

        state.currentBasePlanId = plan.planId
        state.activePlanId = null
        state.activeAttemptId = null
        state.urgentReissueFrom = null
        state.chainVersion += 1
        state.updatedAt = now

        planIssueAttemptRepository.save(attempt)
        plannedPlanRepository.save(plan)
        spacecraftPlanningStateJpaRepository.save(state)
        acceptedPlanSlotsUpdateJobService.enqueue(plan.planId)
    }

    private fun rejectPlan(
        message: PlanDecisionMessage,
        attempt: PlanIssueAttemptEntity,
        plan: PlannedPlanEntity,
        state: SpacecraftPlanningStateEntity
    ) {
        val now = LocalDateTime.now()
        recordDecisionOnAttempt(message, attempt)

        plan.status = PlannedPlanStatus.REJECTED
        plan.updatedAt = now

        planIssueAttemptRepository.save(attempt)
        plannedPlanRepository.save(plan)

        reissueOnNearestWindow(state, plan, now)
    }

    /**
     * Общий маршрут после негатива (REJECTED/«не заложен»/«не принят»): снимает активную выдачу
     * и взводит срочную перевыдачу на ближайшую ЗРВ. Базовый указатель не трогаем — проекция
     * сама подберёт ближайшее окно (для принятого-и-провалившегося плана он же служит якорем цепочки).
     */
    private fun reissueOnNearestWindow(
        state: SpacecraftPlanningStateEntity,
        failedPlan: PlannedPlanEntity,
        now: LocalDateTime
    ) {
        state.activePlanId = null
        state.activeAttemptId = null
        state.urgentReissueFrom = maxOf(
            now.plusMinutes(planningProperties.reissuePrepMinutes),
            failedPlan.startTime
        )
        state.updatedAt = now
        spacecraftPlanningStateJpaRepository.save(state)
    }

    private fun claimEvent(eventId: java.util.UUID): Boolean =
        processedPlanDecisionEventRepository.insertIfAbsent(eventId, LocalDateTime.now()) != 0

    private fun getOrCreateLockedState(spacecraftId: String): SpacecraftPlanningStateEntity {
        spacecraftPlanningStateJpaRepository.findById(spacecraftId).orElse(null)
            ?: spacecraftPlanningStateJpaRepository.saveAndFlush(SpacecraftPlanningStateEntity(spacecraftId = spacecraftId))
        return spacecraftPlanningStateJpaRepository.lockBySpacecraftId(spacecraftId)
            ?: throw IllegalStateException("Planning state for spacecraftId=$spacecraftId was not found after create")
    }

    private fun PlanIssueAttemptStatus.isTerminal(): Boolean =
        this != PlanIssueAttemptStatus.STARTING && this != PlanIssueAttemptStatus.STARTED
}
