package space.nstart.pcp_tgu_service.service

import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionTemplate
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp_tgu_service.config.PlanningProperties
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
import space.nstart.pcp_tgu_service.dto.ProcessStartResponse
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.SpacecraftPlanningStateJpaRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime
import java.util.UUID

/**
 * Выдаёт дозревшие планы. В on-demand-модели голова цепочки считается из снапшота ЗРВ
 * ([PlanProjectionService]) и материализуется в planned_plan ровно в момент выдачи (статус ISSUING).
 */
@Service
class IssueDuePlanWorker(
    private val planningProperties: PlanningProperties,
    private val spacecraftSnapshotRepository: SpacecraftSnapshotRepository,
    private val plannedPlanRepository: PlannedPlanRepository,
    private val planIssueAttemptRepository: PlanIssueAttemptRepository,
    private val spacecraftPlanningStateJpaRepository: SpacecraftPlanningStateJpaRepository,
    private val planProjectionService: PlanProjectionService,
    private val bpmnPlanIssueProcessStarter: BpmnPlanIssueProcessStarter,
    private val transactionTemplate: TransactionTemplate
) {
    private val log = LoggerFactory.getLogger(javaClass)

    /** Периодически по активным КА считает голову проекции и выдаёт дозревший план. */
    @Scheduled(fixedDelayString = "\${planning.notification-check-fixed-delay-ms}")
    fun issueDuePlans() {
        spacecraftSnapshotRepository.findAllByActiveTrue().forEach { snapshot ->
            val prepared = transactionTemplate.execute { prepareDueForSpacecraft(snapshot.spacecraftId) }
            prepared?.let { preparedAttempt -> startPreparedAttempt(preparedAttempt) }
        }
    }

    /** Test-only/manual hook: создаёт новую attempt для конкретного planId и стартует её. */
    fun issuePlanNow(planId: UUID): ProcessStartResponse {
        val prepared = transactionTemplate.execute { prepareSpecificPlan(planId) }
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Plan with planId=$planId was not found or cannot be issued")
        return startPreparedAttempt(prepared)
    }

    /** Готовит выдачу головы проекции для КА под lock'ом состояния (scale-out safe). */
    private fun prepareDueForSpacecraft(spacecraftId: String): PreparedAttempt? {
        val now = LocalDateTime.now()
        val state = getOrCreateLockedState(spacecraftId)
        if (state.activeAttemptId != null && !tryAdvancePastUnconfirmedPlan(state, now)) {
            return null
        }

        val projection = planProjectionService.project(spacecraftId)
        if (projection.isEmpty()) {
            return null
        }

        val urgentFrom = state.urgentReissueFrom
        val head = if (urgentFrom != null) {
            projection.firstOrNull { plan -> !plan.startTime.isBefore(urgentFrom) }
        } else {
            projection.first()
        } ?: return null

        if (!head.startTime.isAfter(now)) {
            return null
        }

        val issueNotBefore = head.startTime.minusMinutes(planningProperties.notificationBeforeStartMinutes)
        val due = urgentFrom != null || !now.isBefore(issueNotBefore)
        if (!due) {
            return null
        }

        val plan = materializeIssuingPlan(head, state, issueNotBefore, now)
        val attempt = createStartingAttempt(plan.planId, now)

        state.activePlanId = plan.planId
        state.activeAttemptId = attempt.attemptId
        state.urgentReissueFrom = null
        state.updatedAt = now
        spacecraftPlanningStateJpaRepository.save(state)

        return PreparedAttempt(plan.planId, attempt.attemptId)
    }

    /**
     * Если активная выдача — это план в WAITING_DECISION, по которому уже прошло
     * [PlanningProperties.waitingDecisionGraceMinutes] минут после старта и решения так и нет,
     * перестаём ждать подтверждение: снимаем блокировку (activePlanId/activeAttemptId),
     * двигаем якорь цепочки на этот план и идём дальше. Статус плана НЕ меняем — он остаётся
     * WAITING_DECISION (молчаливое продолжение, а не ACCEPTED). Attempt оставляем как есть:
     * запоздалое решение по нему просто запишется на attempt, не трогая ушедшую вперёд цепочку.
     *
     * @return true, если блокировка снята и можно выдавать следующий план в этом же тике.
     */
    private fun tryAdvancePastUnconfirmedPlan(state: SpacecraftPlanningStateEntity, now: LocalDateTime): Boolean {
        val activePlanId = state.activePlanId ?: return false
        val plan = plannedPlanRepository.findById(activePlanId).orElse(null) ?: return false
        if (plan.status != PlannedPlanStatus.WAITING_DECISION) {
            return false
        }
        if (now.isBefore(plan.startTime.plusMinutes(planningProperties.waitingDecisionGraceMinutes))) {
            return false
        }

        state.currentBasePlanId = plan.planId
        state.activePlanId = null
        state.activeAttemptId = null
        state.urgentReissueFrom = null
        state.chainVersion += 1
        state.updatedAt = now
        spacecraftPlanningStateJpaRepository.save(state)

        log.info(
            "Plan {} (spacecraft {}) stayed WAITING_DECISION past grace; continuing chain without confirmation.",
            plan.planId, plan.spacecraftId
        )
        return true
    }

    private fun prepareSpecificPlan(planId: UUID): PreparedAttempt? {
        val now = LocalDateTime.now()
        val plan = plannedPlanRepository.findById(planId).orElse(null) ?: return null
        val state = getOrCreateLockedState(plan.spacecraftId)
        if (state.activeAttemptId != null) {
            return null
        }
        if (plan.status !in REISSUABLE_STATUSES) {
            return null
        }

        plan.status = PlannedPlanStatus.ISSUING
        plan.updatedAt = now
        plannedPlanRepository.save(plan)

        val attempt = createStartingAttempt(plan.planId, now)
        state.activePlanId = plan.planId
        state.activeAttemptId = attempt.attemptId
        state.urgentReissueFrom = null
        state.updatedAt = now
        spacecraftPlanningStateJpaRepository.save(state)

        return PreparedAttempt(plan.planId, attempt.attemptId)
    }

    private fun materializeIssuingPlan(
        head: CalculatedPlan,
        state: SpacecraftPlanningStateEntity,
        issueNotBefore: LocalDateTime,
        now: LocalDateTime
    ): PlannedPlanEntity =
        plannedPlanRepository.insertIssuingPlan(
            planId = UUID.randomUUID(),
            spacecraftId = head.spacecraftId,
            startTime = head.startTime,
            endTime = head.endTime,
            kppId = head.kppId,
            issueNotBefore = issueNotBefore,
            basePlanId = state.currentBasePlanId,
            chainVersion = state.chainVersion,
            now = now
        )

    private fun createStartingAttempt(planId: UUID, now: LocalDateTime): PlanIssueAttemptEntity {
        val attemptNo = (planIssueAttemptRepository.findTopByPlanIdOrderByAttemptNoDesc(planId)?.attemptNo ?: 0) + 1
        return planIssueAttemptRepository.save(
            PlanIssueAttemptEntity(
                attemptId = UUID.randomUUID(),
                planId = planId,
                attemptNo = attemptNo,
                status = PlanIssueAttemptStatus.STARTING,
                createdAt = now,
                updatedAt = now
            )
        )
    }

    private fun startPreparedAttempt(preparedAttempt: PreparedAttempt): ProcessStartResponse {
        val plan = plannedPlanRepository.findById(preparedAttempt.planId)
            .orElseThrow { IllegalStateException("Prepared plan ${preparedAttempt.planId} disappeared") }
        val attempt = planIssueAttemptRepository.findById(preparedAttempt.attemptId)
            .orElseThrow { IllegalStateException("Prepared attempt ${preparedAttempt.attemptId} disappeared") }

        return try {
            val response = bpmnPlanIssueProcessStarter.startProcess(plan, attempt)
            markStarted(response)
            response
        } catch (ex: Exception) {
            log.warn("BPMN start failed after attempt was prepared. planId={}, attemptId={}", plan.planId, attempt.attemptId, ex)
            markAmbiguous(preparedAttempt)
            throw ex
        }
    }

    private fun markStarted(response: ProcessStartResponse) {
        transactionTemplate.executeWithoutResult {
            val now = LocalDateTime.now()
            val attempt = planIssueAttemptRepository.findById(response.attemptId).orElse(null) ?: return@executeWithoutResult
            val plan = plannedPlanRepository.findById(response.planId).orElse(null) ?: return@executeWithoutResult

            if (attempt.processInstanceKey == null) {
                attempt.processInstanceKey = response.processInstanceKey
            }
            if (attempt.startedAt == null) {
                attempt.startedAt = now
            }
            if (attempt.status == PlanIssueAttemptStatus.STARTING && plan.status == PlannedPlanStatus.ISSUING) {
                attempt.status = PlanIssueAttemptStatus.STARTED
                plan.status = PlannedPlanStatus.WAITING_DECISION
                plan.updatedAt = now
            }
            attempt.updatedAt = now

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

    private fun markAmbiguous(preparedAttempt: PreparedAttempt) {
        transactionTemplate.executeWithoutResult {
            val now = LocalDateTime.now()
            val attempt = planIssueAttemptRepository.findById(preparedAttempt.attemptId).orElse(null) ?: return@executeWithoutResult
            val plan = plannedPlanRepository.findById(preparedAttempt.planId).orElse(null) ?: return@executeWithoutResult
            attempt.status = PlanIssueAttemptStatus.AMBIGUOUS
            attempt.updatedAt = now
            plan.status = PlannedPlanStatus.START_AMBIGUOUS
            plan.updatedAt = now
            planIssueAttemptRepository.save(attempt)
            plannedPlanRepository.save(plan)
        }
    }

    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 data class PreparedAttempt(
        val planId: UUID,
        val attemptId: UUID
    )

    private companion object {
        /** Статусы persisted-плана, который допустимо выдать вручную через issuePlanNow (test-only). */
        val REISSUABLE_STATUSES = setOf(
            PlannedPlanStatus.PLANNED,
            PlannedPlanStatus.REJECTED,
            PlannedPlanStatus.LAYIN_FAILED,
            PlannedPlanStatus.NOT_ACCEPTED,
            PlannedPlanStatus.START_AMBIGUOUS
        )
    }
}
