package com.ffii.fpsms.modules.jobOrder.service

import com.ffii.fpsms.modules.jobOrder.entity.JobOrder
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus
import com.ffii.fpsms.modules.pickOrder.entity.PickOrder
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.productProcess.entity.ProductProcess
import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository
import com.ffii.fpsms.modules.productProcess.enums.ProductProcessStatus
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.service.StockInLineService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDateTime
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Daily batch after plan day ends: at run time (default 00:00:15), process job orders whose
 * [JobOrder.planStart] fell on the previous calendar day.
 *
 * - Branch A: no pick submitted, product process pending → hide job order.
 * - Branch B: pick submitted, product process still pending → reschedule to today 00:00:00 and renumber.
 */
@Service
open class JobOrderPlanStartAutoService(
    private val jobOrderRepository: JobOrderRepository,
    private val pickOrderRepository: PickOrderRepository,
    private val productProcessRepository: ProductProcessRepository,
    private val stockInLineRepository: StockInLineRepository,
    private val jobOrderService: JobOrderService,
    private val stockInLineService: StockInLineService,
    private val transactionTemplate: TransactionTemplate,
) {
    private val logger = LoggerFactory.getLogger(javaClass)
    private val inFlight = AtomicBoolean(false)

    data class JobOrderPlanStartAutoReport(
        val runAt: LocalDateTime,
        val targetPlanDayFrom: LocalDateTime,
        val targetPlanDayToExclusive: LocalDateTime,
        val candidates: Int = 0,
        val hidden: Int = 0,
        val rescheduled: Int = 0,
        val skipped: Int = 0,
        val errors: Int = 0,
    )

    open fun runAutoProcess(runAt: LocalDateTime = LocalDateTime.now()): JobOrderPlanStartAutoReport {
        if (!inFlight.compareAndSet(false, true)) {
            logger.warn("Job order plan-start auto process skipped: previous run still in flight")
            val targetDay = runAt.toLocalDate().minusDays(1)
            return JobOrderPlanStartAutoReport(
                runAt = runAt,
                targetPlanDayFrom = targetDay.atStartOfDay(),
                targetPlanDayToExclusive = targetDay.plusDays(1).atStartOfDay(),
            )
        }
        try {
            return runAutoProcessInternal(runAt)
        } finally {
            inFlight.set(false)
        }
    }

    private fun runAutoProcessInternal(runAt: LocalDateTime): JobOrderPlanStartAutoReport {
        val targetDay = runAt.toLocalDate().minusDays(1)
        val from = targetDay.atStartOfDay()
        val toExclusive = targetDay.plusDays(1).atStartOfDay()
        val newPlanStart = runAt.toLocalDate().atStartOfDay()

        var hidden = 0
        var rescheduled = 0
        var skipped = 0
        var errors = 0

        val jobOrders = jobOrderRepository
            .findByDeletedFalseAndPlanStartFromBeforeExclusiveOrderByIdAsc(from, toExclusive)
            .filter { isEligibleCandidate(it) }

        val joIds = jobOrders.mapNotNull { it.id }
        val pickOrdersByJoId = loadPickOrdersByJobOrderId(joIds)
        val productProcessesByJoId = loadProductProcessesByJobOrderId(joIds)

        logger.info(
            "Job order plan-start auto: runAt={}, targetPlanDay=[{}, {}), candidates={}",
            runAt,
            from,
            toExclusive,
            jobOrders.size,
        )

        for (jo in jobOrders) {
            val joId = jo.id ?: continue
            try {
                when (
                    classify(
                        jo,
                        pickOrdersByJoId[joId].orEmpty(),
                        productProcessesByJoId[joId],
                    )
                ) {
                    Branch.HIDE -> {
                        transactionTemplate.executeWithoutResult {
                            applyHide(jo, runAt)
                        }
                        hidden++
                    }
                    Branch.RESCHEDULE -> {
                        transactionTemplate.executeWithoutResult {
                            applyReschedule(jo, productProcessesByJoId[joId], newPlanStart, runAt)
                        }
                        rescheduled++
                    }
                    Branch.SKIP -> skipped++
                }
            } catch (e: Exception) {
                errors++
                logger.error("Job order plan-start auto failed for joId={} code={}: {}", joId, jo.code, e.message, e)
            }
        }

        val report = JobOrderPlanStartAutoReport(
            runAt = runAt,
            targetPlanDayFrom = from,
            targetPlanDayToExclusive = toExclusive,
            candidates = jobOrders.size,
            hidden = hidden,
            rescheduled = rescheduled,
            skipped = skipped,
            errors = errors,
        )
        logger.info("Job order plan-start auto finished: {}", report)
        return report
    }

    private fun isEligibleCandidate(jo: JobOrder): Boolean {
        if (jo.isHidden == true) return false
        if (jo.status == JobOrderStatus.COMPLETED) return false
        return true
    }

    private enum class Branch {
        HIDE,
        RESCHEDULE,
        SKIP,
    }

    private fun classify(
        jo: JobOrder,
        pickOrders: List<PickOrder>,
        productProcess: ProductProcess?,
    ): Branch {
        if (!isProductProcessPendingNotStarted(productProcess)) {
            return Branch.SKIP
        }
        val maxSubmittedLines = pickOrders.maxOfOrNull { it.submittedLines ?: 0 } ?: 0
        return when {
            maxSubmittedLines == 0 -> Branch.HIDE
            maxSubmittedLines > 0 -> Branch.RESCHEDULE
            else -> Branch.SKIP
        }
    }

    private fun isProductProcessPendingNotStarted(productProcess: ProductProcess?): Boolean {
        if (productProcess == null) return false
        if (productProcess.deleted) return false
        if (productProcess.status != ProductProcessStatus.PENDING) return false
        if (productProcess.startTime != null) return false
        return true
    }

    private fun loadPickOrdersByJobOrderId(jobOrderIds: List<Long>): Map<Long, List<PickOrder>> {
        if (jobOrderIds.isEmpty()) return emptyMap()
        return pickOrderRepository
            .findAllByJobOrder_IdInAndDeletedFalseOrderByJobOrder_IdAscCreatedDesc(jobOrderIds)
            .groupBy { it.jobOrder?.id ?: -1L }
            .filterKeys { it > 0L }
    }

    private fun loadProductProcessesByJobOrderId(jobOrderIds: List<Long>): Map<Long, ProductProcess> {
        if (jobOrderIds.isEmpty()) return emptyMap()
        return productProcessRepository
            .findByJobOrder_IdInAndDeletedIsFalse(jobOrderIds)
            .mapNotNull { pp -> pp.jobOrder?.id?.let { it to pp } }
            .groupBy { it.first }
            .mapValues { (_, entries) ->
                entries.map { it.second }.firstOrNull { isProductProcessPendingNotStarted(it) }
                    ?: entries.map { it.second }.first()
            }
            .filterValues { it != null }
            .mapValues { it.value!! }
    }

    private fun applyHide(jo: JobOrder, runAt: LocalDateTime) {
        jo.isHidden = true
        appendRemarks(jo, "[auto ${runAt.toLocalDate()}] hidden: overdue plan day, no pick submitted, process pending")
        jobOrderRepository.save(jo)
        logger.info("Job order plan-start auto hid joId={} code={}", jo.id, jo.code)
    }

    private fun applyReschedule(
        jo: JobOrder,
        productProcess: ProductProcess?,
        newPlanStart: LocalDateTime,
        runAt: LocalDateTime,
    ) {
        val pp = productProcess?.takeIf { isProductProcessPendingNotStarted(it) }
            ?: throw IllegalStateException("Product process not pending for reschedule, joId=${jo.id}")

        val newCode = jobOrderService.assignJobNo(newPlanStart)
        jo.planStart = newPlanStart
        jo.code = newCode
        appendRemarks(
            jo,
            "[auto ${runAt.toLocalDate()}] rescheduled from overdue plan day; pick started, process pending",
        )
        jobOrderRepository.save(jo)

        pp.date = newPlanStart.toLocalDate()
        productProcessRepository.save(pp)

        val sil = jo.id?.let { stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(it) }
        if (sil != null) {
            sil.lotNo = stockInLineService.assignLotNoForJo(newPlanStart.toLocalDate())
            sil.productLotNo = newCode
            stockInLineRepository.save(sil)
        }

        logger.info(
            "Job order plan-start auto rescheduled joId={} newCode={} newPlanStart={}",
            jo.id,
            newCode,
            newPlanStart,
        )
    }

    private fun appendRemarks(jo: JobOrder, snippet: String) {
        val existing = jo.remarks?.trim().orEmpty()
        jo.remarks = if (existing.isEmpty()) snippet else "$existing | $snippet"
    }
}
