package com.ffii.fpsms.modules.stock.service

import com.ffii.fpsms.modules.bag.entity.BagRepository
import com.ffii.fpsms.modules.bag.service.BagService
import com.ffii.fpsms.modules.bag.web.model.CreateBagLotLineRequest
import com.ffii.fpsms.modules.common.CodeGenerator
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecord
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchScanPickRequest
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import com.ffii.fpsms.modules.pickOrder.service.PickOrderService
import com.ffii.fpsms.modules.stock.entity.Inventory
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.StockLedger
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.StockOut
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.entity.StockOutLine
import com.ffii.fpsms.modules.stock.entity.StockOutRepository
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus
import com.ffii.fpsms.modules.stock.web.model.StockOutStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext
import org.springframework.orm.ObjectOptimisticLockingFailureException
data class WorkbenchStockOutLineSummary(
    val pickOrderId: Long,
    val created: Int,
    val reused: Int,
)
@PersistenceContext
private lateinit var entityManager: EntityManager
@Service
open class StockOutLineWorkbenchService(
    private val pickOrderRepository: PickOrderRepository,
    private val pickOrderLineRepository: PickOrderLineRepository,
    private val stockOutRepository: StockOutRepository,
    private val stockOutLIneRepository: StockOutLIneRepository,
    private val suggestPickLotRepository: SuggestPickLotRepository,
    private val inventoryLotLineRepository: InventoryLotLineRepository,
    private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService,
    private val stockLedgerRepository: StockLedgerRepository,
    private val itemUomService: ItemUomService,
    private val itemUomRespository: ItemUomRespository,
    private val bagRepository: BagRepository,
    private val bagService: BagService,
    private val pickOrderService: PickOrderService,
    private val doPickOrderRepository: DoPickOrderRepository,
    private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
    private val doPickOrderLineRepository: DoPickOrderLineRepository,
    private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
    private val deliveryOrderRepository: DeliveryOrderRepository,
) {

    @Transactional(rollbackFor = [Exception::class])
    open fun ensureStockOutLinesForPickOrderNoHold(pickOrderId: Long, userId: Long): WorkbenchStockOutLineSummary {
        val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null)
            ?: return WorkbenchStockOutLineSummary(pickOrderId = pickOrderId, created = 0, reused = 0)

        val consoCode = (pickOrder.consoCode ?: "").trim()
        val stockOutKey = if (consoCode.isNotEmpty()) consoCode else "PO-${pickOrder.id ?: 0L}"
        val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(stockOutKey)
            ?: stockOutRepository.save(
                StockOut().apply {
                    this.consoPickOrderCode = stockOutKey
                    this.type = pickOrder.type?.value ?: "do"
                    this.status = StockOutStatus.PENDING.status
                    this.handler = pickOrder.assignTo?.id ?: userId
                }
            )

        val lineIds = pickOrder.pickOrderLines.mapNotNull { it.id }
        val suggestions = if (lineIds.isEmpty()) emptyList() else suggestPickLotRepository.findAllByPickOrderLineIdIn(lineIds)

        // Avoid N+1: load existing SOL rows once and check in-memory.
        val existingSols = if (lineIds.isEmpty()) emptyList() else stockOutLIneRepository.findAllByPickOrderLineIdInAndDeletedFalse(lineIds)
        val existingKeys: Set<String> = existingSols.mapNotNull { sol ->
            val polId = sol.pickOrderLine?.id ?: return@mapNotNull null
            val illId = sol.inventoryLotLine?.id // may be null
            "$polId:${illId ?: "null"}"
        }.toSet()

        var created = 0
        var reused = 0
        suggestions.forEach { s ->
            val pol = s.pickOrderLine ?: return@forEach
            val ill = s.suggestedLotLine
            val key = "${pol.id!!}:${ill?.id ?: "null"}"
            val exists = existingKeys.contains(key)
            if (exists) {
                reused++
                return@forEach
            }

            val savedSol = stockOutLIneRepository.save(
                StockOutLine().apply {
                    this.stockOut = stockOut
                    this.pickOrderLine = pol
                    this.inventoryLotLine = ill
                    this.item = pol.item
                    this.status = StockOutLineStatus.PENDING.status
                    this.qty = 0.0
                    this.type = "NOR"
                }
            )
            suggestedPickLotWorkbenchService.linkSplToStockOutLineWhenSolCreated(savedSol)
            created++
        }

        return WorkbenchStockOutLineSummary(
            pickOrderId = pickOrderId,
            created = created,
            reused = reused
        )
    }

    /**
     * Ensures [StockOutLine] rows exist only for suggestions of the given pick order line (workbench scan-pick fast path).
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun ensureStockOutLinesForPickOrderLineNoHold(pickOrderLineId: Long, userId: Long): WorkbenchStockOutLineSummary {
        val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null)
            ?: return WorkbenchStockOutLineSummary(pickOrderId = 0L, created = 0, reused = 0)
        val pickOrder = pol.pickOrder
            ?: return WorkbenchStockOutLineSummary(pickOrderId = 0L, created = 0, reused = 0)
        val pickOrderId = pickOrder.id ?: 0L

        val consoCode = (pickOrder.consoCode ?: "").trim()
        val stockOutKey = if (consoCode.isNotEmpty()) consoCode else "PO-${pickOrder.id ?: 0L}"
        val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(stockOutKey)
            ?: stockOutRepository.save(
                StockOut().apply {
                    this.consoPickOrderCode = stockOutKey
                    this.type = pickOrder.type?.value ?: "do"
                    this.status = StockOutStatus.PENDING.status
                    this.handler = pickOrder.assignTo?.id ?: userId
                }
            )

        val suggestions = suggestPickLotRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)

        var created = 0
        var reused = 0
        suggestions.forEach { s ->
            val polEntity = s.pickOrderLine ?: return@forEach
            val ill = s.suggestedLotLine
            val exists = if (ill?.id != null) {
                stockOutLIneRepository.existsByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polEntity.id!!, ill.id!!)
            } else {
                stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(polEntity.id!!).any { it.inventoryLotLineId == null }
            }
            if (exists) {
                reused++
                return@forEach
            }

            val savedSol = stockOutLIneRepository.save(
                StockOutLine().apply {
                    this.stockOut = stockOut
                    this.pickOrderLine = polEntity
                    this.inventoryLotLine = ill
                    this.item = polEntity.item
                    this.status = StockOutLineStatus.PENDING.status
                    this.qty = 0.0
                    this.type = "NOR"
                }
            )
            suggestedPickLotWorkbenchService.linkSplToStockOutLineWhenSolCreated(savedSol)
            created++
        }

        return WorkbenchStockOutLineSummary(
            pickOrderId = pickOrderId,
            created = created,
            reused = reused
        )
    }

    @Transactional(rollbackFor = [Exception::class])
    open fun scanPick(request: WorkbenchScanPickRequest): MessageResponse {
        val lotNo = request.lotNo.trim()
        /* 
        if (lotNo.isEmpty()) {
            return MessageResponse(
                id = null, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "lotNo is required", errorPosition = null, entity = null
            )
        }
*/
        val sol = stockOutLIneRepository.findById(request.stockOutLineId).orElse(null)
            ?: return MessageResponse(
                id = request.stockOutLineId, name = "scan_pick", code = "NOT_FOUND", type = "workbench_scan_pick",
                message = "Stock out line not found", errorPosition = null, entity = null
            )

        val st = sol.status?.trim()?.lowercase() ?: ""
        if (st == "completed" || st == StockOutLineStatus.COMPLETE.status.lowercase()) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Stock out line already completed", errorPosition = null, entity = null
            )
        }
        if (st == "rejected" || st == StockOutLineStatus.REJECTED.status.lowercase()) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Stock out line is rejected", errorPosition = null, entity = null
            )
        }

        val pol = sol.pickOrderLine
            ?: return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Pick order line is missing", errorPosition = null, entity = null
            )
        val item = pol.item
            ?: return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Item is missing on pick order line", errorPosition = null, entity = null
            )
        val itemId = item.id!!
        val polId = pol.id!!

        // ✅ Workbench "zero-complete": explicit qty=0 means finish this SOL without inventory posting.
        // Used by Just Completed when user overrides qty to 0.
        val explicitQty = request.qty?.coerceAtLeast(BigDecimal.ZERO)
        if (explicitQty != null && explicitQty.compareTo(BigDecimal.ZERO) == 0) {
            sol.status = StockOutLineStatus.COMPLETE.status
            sol.pickTime = LocalDateTime.now()
            sol.handledBy = request.userId
            if (sol.startTime == null) sol.startTime = LocalDateTime.now()
            sol.endTime = LocalDateTime.now()
            stockOutLIneRepository.saveAndFlush(sol)

            // Ensure POL/PO completion is re-evaluated even when delta=0 (no inventory posting).
            try {
               
                checkWorkbenchPickOrderLineCompleted(polId)
            } catch (e: Exception) {
                println("⚠️ Workbench zero-complete POL refresh: ${e.message}")
            }
            /* 
            val consoCode = sol.pickOrderLine?.pickOrder?.consoCode
            if (!consoCode.isNullOrBlank()) {
                try {
                    pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode)
                } catch (e: Exception) {
                    println("⚠️ Workbench zero-complete pick order completion check: ${e.message}")
                }
            }
*/
            val mapped = stockOutLIneRepository.findStockOutLineInfoById(sol.id!!)
            return MessageResponse(
                id = sol.id,
                name = lotNo,
                code = "SUCCESS",
                type = "workbench_scan_pick",
                message = "Workbench SOL completed with qty=0 (no inventory posting)",
                errorPosition = null,
                entity = mapped,
            )
        }

        val scannedIll = inventoryLotLineRepository
            .findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(lotNo, itemId)
            ?: return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Lot not found for this item: $lotNo", errorPosition = null, entity = null
            )

        if (scannedIll.deleted) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Lot line is deleted", errorPosition = null, entity = null
            )
        }

        if (scannedIll.status != InventoryLotLineStatus.AVAILABLE) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Lot line is not available (status=${scannedIll.status})", errorPosition = null, entity = null
            )
        }
        val expiry = scannedIll.inventoryLot?.expiryDate
        if (expiry != null && expiry.isBefore(LocalDate.now())) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Lot is expired (expiry=$expiry)", errorPosition = null, entity = null
            )
        }
        if (scannedIll.inventoryLot?.item?.id != itemId) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Scanned lot does not match pick line item", errorPosition = null, entity = null
            )
        }

        sol.inventoryLotLine = scannedIll

        val required = pol.qty ?: BigDecimal.ZERO
        val infos = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
        val endedSumOthers = infos
            .filter { WorkbenchStockOutLinePickProgress.isCountedAsPicked(it) && it.id != sol.id }
            .fold(BigDecimal.ZERO) { acc, i -> acc + i.qty }
        val currentQtyBd = BigDecimal(sol.qty?.toString() ?: "0")
        val remainingPol = required.subtract(endedSumOthers).subtract(currentQtyBd).coerceAtLeast(BigDecimal.ZERO)

        val splForLot = suggestPickLotRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
            .firstOrNull { it.suggestedLotLine?.id == scannedIll.id }
        val chunkTarget = when {
            splForLot?.qty != null && splForLot.qty!! > BigDecimal.ZERO ->
                splForLot.qty!!.min(remainingPol.add(currentQtyBd))
            else -> remainingPol.add(currentQtyBd)
        }
        val stillNeedOnThisSol = chunkTarget.subtract(currentQtyBd).coerceAtLeast(BigDecimal.ZERO)
        if (stillNeedOnThisSol <= BigDecimal.ZERO) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "No remaining quantity to pick on this stock out line", errorPosition = null, entity = null
            )
        }

        // Align with DoWorkbenchMainService.scanPick: explicit qty may exceed this SOL chunk; cap by lot availability only.
        val requestedDelta = when {
            request.qty != null -> request.qty!!.coerceAtLeast(BigDecimal.ZERO)
            else -> stillNeedOnThisSol
        }
        if (requestedDelta < BigDecimal.ZERO) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick",
                message = "Pick quantity must be positive", errorPosition = null, entity = null
            )
        }

        val illId = scannedIll.id!!
        val freshIll = inventoryLotLineRepository.findById(illId).orElse(scannedIll)
        val availablePickable = (freshIll.inQty ?: BigDecimal.ZERO)
            .subtract(freshIll.outQty ?: BigDecimal.ZERO)
        val plannedDelta = requestedDelta.min(availablePickable)
        if (plannedDelta <= BigDecimal.ZERO) {
            return MessageResponse(
                id = sol.id, name = "scan_pick", code = "CONFLICT", type = "workbench_scan_pick",
                message = "Insufficient available quantity on lot (may have been picked by another user)",
                errorPosition = null, entity = null
            )
        }

      //  val expectedVersion = freshIll.version ?: 0
      //  val updated = inventoryLotLineRepository.incrementOutQtyIfAvailable(illId, plannedDelta, expectedVersion)
        val maxRetries = 2
var attempt = 0
var updated = 0
var finalPlannedDelta = plannedDelta
while (attempt <= maxRetries) {
    val latestIll = inventoryLotLineRepository.findById(illId).orElse(scannedIll)
    val latestAvailable = (latestIll.inQty ?: BigDecimal.ZERO)
        .subtract(latestIll.outQty ?: BigDecimal.ZERO)
    // 用最新庫存重算這次可扣數
    finalPlannedDelta = requestedDelta.min(latestAvailable)
    if (finalPlannedDelta <= BigDecimal.ZERO) {
        // 這時才回不足（不是版本衝突）
        return MessageResponse(
            id = sol.id, name = "scan_pick", code = "CONFLICT", type = "workbench_scan_pick",
            message = "Insufficient available quantity on lot (may have been picked by another user)",
            errorPosition = null, entity = null
        )
    }
    val expectedVersion = latestIll.version ?: 0
    updated = inventoryLotLineRepository.incrementOutQtyIfAvailable(illId, finalPlannedDelta, expectedVersion)
    if (updated > 0) break
    attempt++
    if (attempt <= maxRetries) {
        Thread.sleep(15L) // optional: 小 backoff，避免連續撞鎖
    }
}
if (updated == 0) {
    // 重試用盡才回衝突（不在第一次 updated==0 就回）
    return MessageResponse(
        id = sol.id, name = "scan_pick", code = "CONFLICT", type = "workbench_scan_pick",
        message = "庫存或版本已變更，請重掃或重算（Concurrent pick or stock changed）",
        errorPosition = null, entity = null
    )
}

        val newQtyBd = currentQtyBd.add(finalPlannedDelta)
        sol.qty = newQtyBd.toDouble()
        val doneThisRow = newQtyBd.compareTo(chunkTarget) >= 0
        sol.status = if (doneThisRow) StockOutLineStatus.COMPLETE.status else StockOutLineStatus.PARTIALLY_COMPLETE.status
        sol.pickTime = LocalDateTime.now()
        sol.handledBy = request.userId
        if (sol.startTime == null) sol.startTime = LocalDateTime.now()
        if (doneThisRow) sol.endTime = LocalDateTime.now()
        stockOutLIneRepository.saveAndFlush(sol)

        postWorkbenchPickSideEffects(sol.id!!, finalPlannedDelta)

        val pickOrderId = pol.pickOrder?.id
        if (pickOrderId != null) {
            suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(pickOrderId)
            ensureStockOutLinesForPickOrderNoHold(pickOrderId, request.userId)
        }

        val mapped = stockOutLIneRepository.findStockOutLineInfoById(sol.id!!)
        return MessageResponse(
            id = sol.id,
            name = scannedIll.inventoryLot?.lotNo ?: lotNo,
            code = "SUCCESS",
            type = "workbench_scan_pick",
            message = "pick successful",
            errorPosition = null,
            entity = mapped,
        )
    }

    /**
     * Workbench-only: after atomic inventory lot update — ledger, bag, pick order line / pick order / DO sync.
     */
    private fun postWorkbenchPickSideEffects(stockOutLineId: Long, deltaQty: BigDecimal) {
        if (deltaQty <= BigDecimal.ZERO) return
        val savedStockOutLine = stockOutLIneRepository.findById(stockOutLineId).orElseThrow()
        createWorkbenchPickLedger(stockOutLineId, deltaQty)
        try {
            val solItem = savedStockOutLine.item
            val inventoryLotLine = savedStockOutLine.inventoryLotLine
            val reqDeltaQty = deltaQty.toDouble()
            val statusLower = savedStockOutLine.status?.trim()?.lowercase() ?: ""
            val isCompletedOrPartiallyCompleted =
                statusLower == "completed" ||
                    statusLower == "partially_completed" ||
                    statusLower == StockOutLineStatus.PARTIALLY_COMPLETE.status.lowercase()
            if (solItem?.isBag == true &&
                inventoryLotLine != null &&
                isCompletedOrPartiallyCompleted &&
                reqDeltaQty > 0
            ) {
                val bag = bagRepository.findByItemIdAndDeletedIsFalse(solItem.id!!)
                if (bag != null) {
                    val lotNo = inventoryLotLine.inventoryLot?.lotNo
                    if (lotNo != null) {
                        bagService.createBagLotLinesByBagId(
                            CreateBagLotLineRequest(
                                bagId = bag.id!!,
                                lotId = inventoryLotLine.inventoryLot?.id ?: 0L,
                                itemId = solItem.id!!,
                                lotNo = lotNo,
                                stockQty = reqDeltaQty.toInt(),
                                date = LocalDate.now(),
                                time = LocalTime.now(),
                                stockOutLineId = savedStockOutLine.id
                            )
                        )
                    }
                }
            }
        } catch (e: Exception) {
            println("Workbench bag side effect: ${e.message}")
            e.printStackTrace()
        }
        val pol = savedStockOutLine.pickOrderLine ?: return
        val polId = pol.id ?: return

        checkWorkbenchPickOrderLineCompleted(polId)
        val pickOrder = pol.pickOrder
        val poId = pickOrder?.id
        if (poId != null) {
            val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(poId)
            val allCompleted = allLines.isNotEmpty() && allLines.all {
                it.status == PickOrderLineStatus.COMPLETED
            }
            if (allCompleted) {
                completePickOrderWithRetry(poId)
                completeDoForPickOrderWorkbench(poId)
                completeDoIfAllPickOrdersCompletedWorkbench(poId)
            }
        }
           
    }

    private fun createWorkbenchPickLedger(stockOutLineId: Long, deltaQty: BigDecimal) {
        if (deltaQty <= BigDecimal.ZERO) return
        val sol = stockOutLIneRepository.findById(stockOutLineId).orElse(null) ?: return
        val solItem = sol.item ?: return
        val inventory = itemUomService.findInventoryForItemBaseUom(solItem.id!!) ?: return
        // Fast path: use inventory onHand after update + delta to reconstruct previous balance.
        val onHandAfter = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
        val previousBalance = onHandAfter + deltaQty.toDouble()
        val newBalance = previousBalance - deltaQty.toDouble()
        val ledger = StockLedger().apply {
            this.stockOutLine = sol
            this.inventory = inventory
            this.inQty = null
            this.outQty = deltaQty.toDouble()
            this.balance = newBalance
            this.type = "NOR"
            this.itemId = solItem.id
            this.itemCode = solItem.code
            this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(solItem.id!!)?.uom?.id
                ?: inventory.uom?.id
            this.date = LocalDate.now()
        }
        stockLedgerRepository.save(ledger)
    }

    private fun resolveWorkbenchPreviousBalance(
        itemId: Long,
        inventory: Inventory,
        onHandQtyBeforeUpdate: Double?,
    ): Double {
        if (onHandQtyBeforeUpdate != null) return onHandQtyBeforeUpdate
        val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId)
        return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
    }

    private fun isWorkbenchSolEndStatus(status: String?): Boolean {
        val s = status?.trim()?.lowercase() ?: return false
        return s == "completed" ||
            s == StockOutLineStatus.COMPLETE.status.lowercase() ||
            s == "rejected" ||
            s == StockOutLineStatus.REJECTED.status.lowercase() ||
            s == "partially_completed" ||
            s == StockOutLineStatus.PARTIALLY_COMPLETE.status.lowercase()
    }


    /** Mirrors [com.ffii.fpsms.modules.stock.service.StockOutLineService.checkIsStockOutLineCompleted] for workbench-only use. */
    private fun checkWorkbenchPickOrderLineCompleted(pickOrderLineId: Long) {
        val allStockOutLines = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
        val pickOrderLine = pickOrderLineRepository.findById(pickOrderLineId).orElse(null)
        val requiredQty = pickOrderLine?.qty ?: BigDecimal.ZERO
        val unfinishedLine = allStockOutLines.filter {
            val rawStatus = it.status.trim()
            val status = rawStatus.lowercase()
            val isComplete =
                status == "completed" || status == StockOutLineStatus.COMPLETE.status.lowercase()
            val isRejected =
                status == "rejected" || status == StockOutLineStatus.REJECTED.status.lowercase()
            val isPartiallyComplete =
                status == "partially_completed" || status == StockOutLineStatus.PARTIALLY_COMPLETE.status.lowercase()
            !(isComplete || isRejected || isPartiallyComplete)
        }
        if (unfinishedLine.isEmpty()) {
            val pol = pickOrderLineRepository.findById(pickOrderLineId).orElseThrow()
            pickOrderLineRepository.save(
                pol.apply { this.status = PickOrderLineStatus.COMPLETED }
            )
        }
    }

    private fun completeDoForPickOrderWorkbench(pickOrderId: Long) {
        val dpos = doPickOrderRepository.findByPickOrderId(pickOrderId)
        if (dpos.isNotEmpty()) {
            dpos.forEach {
                it.ticketStatus = DoPickOrderStatus.completed
                it.ticketCompleteDateTime = LocalDateTime.now()
            }
            doPickOrderRepository.saveAll(dpos)
            dpos.forEach { dpo ->
                dpo.doOrderId?.let { doId ->
                    val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId)
                    if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) {
                        deliveryOrder.status = DeliveryOrderStatus.COMPLETED
                        deliveryOrderRepository.save(deliveryOrder)
                    }
                }
            }
        }
        val dporList = doPickOrderRecordRepository.findByPickOrderId(pickOrderId)
        if (dporList.isNotEmpty()) {
            dporList.forEach {
                it.ticketStatus = DoPickOrderStatus.completed
                it.ticketCompleteDateTime = LocalDateTime.now()
            }
            doPickOrderRecordRepository.saveAll(dporList)
        }
    }

    private fun completeDoIfAllPickOrdersCompletedWorkbench(pickOrderId: Long) {
        val lines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(pickOrderId)
        val doPickOrderIds =
            if (lines.isNotEmpty()) lines.mapNotNull { it.doPickOrderId }.distinct()
            else doPickOrderRepository.findByPickOrderId(pickOrderId).mapNotNull { it.id }

        doPickOrderIds.forEach { dpoId ->
            val allLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(dpoId)
            val allPickOrderIdsInDpo = if (allLines.isNotEmpty()) {
                allLines.mapNotNull { it.pickOrderId }.distinct()
            } else {
                doPickOrderRepository.findById(dpoId).orElse(null)?.pickOrderId?.let { listOf(it) } ?: emptyList()
            }
            if (allPickOrderIdsInDpo.isEmpty()) return@forEach

            val statuses = allPickOrderIdsInDpo.map { id ->
                pickOrderRepository.findById(id).orElse(null)?.status
            }
            val allCompleted = statuses.all { it == PickOrderStatus.COMPLETED }
            if (!allCompleted) return@forEach

            val dpo = doPickOrderRepository.findById(dpoId).orElse(null) ?: return@forEach
            dpo.ticketStatus = DoPickOrderStatus.completed
            dpo.ticketCompleteDateTime = LocalDateTime.now()
            doPickOrderRepository.save(dpo)

            val prefix = "DN"
            val midfix = CodeGenerator.DEFAULT_MIDFIX
            val latestCode = doPickOrderRecordRepository.findLatestDeliveryNoteCodeByPrefix("${prefix}-${midfix}")
            val deliveryNoteCode = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode)

            val dpoRecord = DoPickOrderRecord(
                recordId = dpoId,
                storeId = dpo.storeId ?: "",
                ticketNo = dpo.ticketNo ?: "",
                ticketStatus = DoPickOrderStatus.completed,
                truckId = dpo.truckId,
                pickOrderId = dpo.pickOrderId,
                truckDepartureTime = dpo.truckDepartureTime,
                shopId = dpo.shopId,
                handledBy = dpo.handledBy,
                handlerName = dpo.handlerName,
                doOrderId = dpo.doOrderId,
                pickOrderCode = dpo.pickOrderCode,
                deliveryOrderCode = dpo.deliveryOrderCode,
                deliveryNoteCode = deliveryNoteCode,
                loadingSequence = dpo.loadingSequence,
                ticketReleaseTime = dpo.ticketReleaseTime,
                ticketCompleteDateTime = LocalDateTime.now(),
                truckLanceCode = dpo.truckLanceCode,
                shopCode = dpo.shopCode,
                shopName = dpo.shopName,
                requiredDeliveryDate = dpo.requiredDeliveryDate
            )
            val savedHeader = doPickOrderRecordRepository.save(dpoRecord)

            val lineRecords = allLines.map { l ->
                DoPickOrderLineRecord().apply {
                    this.recordId = l.id
                    this.doPickOrderId = savedHeader.recordId
                    this.pickOrderId = l.pickOrderId
                    this.doOrderId = l.doOrderId
                    this.pickOrderCode = l.pickOrderCode
                    this.deliveryOrderCode = l.deliveryOrderCode
                    this.status = l.status
                }
            }
            if (lineRecords.isNotEmpty()) doPickOrderLineRecordRepository.saveAll(lineRecords)
            if (allLines.isNotEmpty()) doPickOrderLineRepository.deleteAll(allLines)
            doPickOrderRepository.delete(dpo)

            dpo.doOrderId?.let { doId ->
                val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId)
                if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) {
                    deliveryOrder.status = DeliveryOrderStatus.COMPLETED
                    deliveryOrderRepository.save(deliveryOrder)
                }
            }
        }
    }
    fun completePickOrderWithRetry(poId: Long, maxAttempts: Int = 3) {
        var last: Exception? = null
    
        repeat(maxAttempts) { attempt ->
            try {
                // 建议：每次重试前 clear，避免拿到旧实体
                entityManager.clear()
    
                val po = pickOrderRepository.findById(poId).orElse(null) ?: return
                if (po.status == PickOrderStatus.COMPLETED) return  // ✅ 幂等：已经完成就当成功
    
                po.status = PickOrderStatus.COMPLETED
                po.completeDate = LocalDateTime.now()
    
                pickOrderRepository.saveAndFlush(po)  // 这里可能抛 optimistic lock
                return
            } catch (e: ObjectOptimisticLockingFailureException) {
                last = e
                // 小退避，避免立刻再次冲突
                Thread.sleep((10L..50L).random())
            }
        }
    
        throw last ?: RuntimeException("Failed to complete pick order after retries")
    }
}
