package space.nstart.pcp_tgu_service.service

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.domain.PlanDecision
import space.nstart.pcp_tgu_service.dto.PlanDecisionMessage
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 events идемпотентно по eventId и строго по attemptId.
 */
@Service
class HandlePlanDecisionUseCase(
    private val processedPlanDecisionEventRepository: ProcessedPlanDecisionEventRepository,
    private val planIssueAttemptRepository: PlanIssueAttemptRepository,
    private val plannedPlanRepository: PlannedPlanRepository,
    private val spacecraftPlanningStateJpaRepository: SpacecraftPlanningStateJpaRepository,
    private val planRecalculationJobService: PlanRecalculationJobService,
    private val acceptedPlanSlotsUpdateJobService: AcceptedPlanSlotsUpdateJobService,
    private val planningProperties: space.nstart.pcp_tgu_service.config.PlanningProperties
) {
    /** Применяет решение; повтор того же eventId является успешным no-op. */
    @Transactional
    fun handle(message: PlanDecisionMessage) {
        val inserted = processedPlanDecisionEventRepository.insertIfAbsent(message.eventId, LocalDateTime.now())
        if (inserted == 0) {
            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)
        }
    }

    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.ACCEPTED
        plan.acceptedAt = message.decisionTime
        plan.updatedAt = now

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

        planIssueAttemptRepository.save(attempt)
        plannedPlanRepository.save(plan)
        spacecraftPlanningStateJpaRepository.save(state)
        planRecalculationJobService.enqueue(message.spacecraftId, "PLAN_ACCEPTED")
        acceptedPlanSlotsUpdateJobService.enqueue(plan.planId)
    }

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

        state.activePlanId = null
        state.activeAttemptId = null
        state.updatedAt = now

        if (now < plan.startTime.minusMinutes(planningProperties.minReissueLeadMinutes)) {
            plan.status = PlannedPlanStatus.PLANNED
            plan.issueNotBefore = now
            plan.updatedAt = now
        } else {
            plan.status = PlannedPlanStatus.EXPIRED
            plan.updatedAt = now
            promoteNextPlanOrEnqueueRebuild(plan.spacecraftId, now)
        }

        planIssueAttemptRepository.save(attempt)
        plannedPlanRepository.save(plan)
        spacecraftPlanningStateJpaRepository.save(state)
    }

    /**
     * Decision use case только меняет state. Новую попытку позже создаст IssueDuePlanWorker.
     */
    private fun promoteNextPlanOrEnqueueRebuild(spacecraftId: String, now: LocalDateTime) {
        val nextPlan = plannedPlanRepository.findTopBySpacecraftIdAndStatusAndStartTimeAfterOrderByStartTime(
            spacecraftId = spacecraftId,
            status = PlannedPlanStatus.PLANNED,
            startTime = now
        )

        if (nextPlan == null) {
            planRecalculationJobService.enqueue(spacecraftId, "PLAN_REJECTED_AFTER_CUTOFF")
        } else {
            nextPlan.issueNotBefore = now
            nextPlan.updatedAt = now
            plannedPlanRepository.save(nextPlan)
        }
    }

    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
}
