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.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 java.time.LocalDateTime
import java.util.UUID

/**
 * Выдаёт due plans из БД через попытки и короткий BPMN process per attempt.
 */
@Service
class IssueDuePlanWorker(
    private val planningProperties: PlanningProperties,
    private val plannedPlanRepository: PlannedPlanRepository,
    private val planIssueAttemptRepository: PlanIssueAttemptRepository,
    private val spacecraftPlanningStateJpaRepository: SpacecraftPlanningStateJpaRepository,
    private val bpmnPlanIssueProcessStarter: BpmnPlanIssueProcessStarter,
    private val transactionTemplate: TransactionTemplate
) {
    private val log = LoggerFactory.getLogger(javaClass)

    /** Периодически claim'ит due plans и стартует BPMN вне DB transaction. */
    @Scheduled(fixedDelayString = "\${planning.notification-check-fixed-delay-ms}")
    fun issueDuePlans() {
        val prepared = transactionTemplate.execute { prepareDuePlans(limit = 10) }.orEmpty()
        prepared.forEach { 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)
    }

    private fun prepareDuePlans(limit: Int): List<PreparedAttempt> {
        val now = LocalDateTime.now()
        val issueBefore = now.plusMinutes(planningProperties.notificationBeforeStartMinutes)
        return plannedPlanRepository.findDueForUpdate(now, issueBefore, limit)
            .mapNotNull { plan -> preparePlan(plan, now) }
    }

    private fun prepareSpecificPlan(planId: UUID): PreparedAttempt? {
        val now = LocalDateTime.now()
        val plan = plannedPlanRepository.findById(planId).orElse(null) ?: return null
        if (plan.status !in setOf(PlannedPlanStatus.PLANNED, PlannedPlanStatus.REJECTED)) {
            return null
        }
        return preparePlan(plan, now)
    }

    private fun preparePlan(plan: PlannedPlanEntity, now: LocalDateTime): PreparedAttempt? {
        val state = getOrCreateLockedState(plan.spacecraftId)
        if (state.activeAttemptId != null) {
            return null
        }

        val attemptNo = (planIssueAttemptRepository.findTopByPlanIdOrderByAttemptNoDesc(plan.planId)?.attemptNo ?: 0) + 1
        val attempt = planIssueAttemptRepository.save(
            PlanIssueAttemptEntity(
                attemptId = UUID.randomUUID(),
                planId = plan.planId,
                attemptNo = attemptNo,
                status = PlanIssueAttemptStatus.STARTING,
                createdAt = now,
                updatedAt = now
            )
        )

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

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

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

    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
    )
}
