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

import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus
import com.ffii.fpsms.modules.stock.entity.*
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import com.ffii.fpsms.modules.stock.web.model.*
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.io.IOException
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderService
import com.ffii.fpsms.modules.deliveryOrder.service.DoReplenishmentService
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecord
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusByQRCodeAndLotNoRequest
import com.ffii.fpsms.modules.bag.entity.BagRepository
import com.ffii.fpsms.modules.bag.web.model.CreateBagLotLineRequest
import com.ffii.fpsms.modules.common.CodeGenerator
import org.springframework.context.annotation.Lazy
import com.ffii.fpsms.modules.bag.service.BagService
import com.ffii.fpsms.modules.pickOrder.service.PickOrderService
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository
import java.time.LocalTime
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext
import java.util.UUID
import org.springframework.http.HttpStatus
import org.springframework.web.server.ResponseStatusException
@Service
open class StockOutLineService(
    private val jdbcDao: JdbcDao,
    private val pickOrderLineRepository: PickOrderLineRepository,
    private val stockOutRepository: StockOutRepository,
    private val stockOutLineRepository: StockOutLIneRepository,
    private val itemRepository: ItemsRepository,
    private val itemUomRespository: ItemUomRespository,
    private val pickOrderRepository: PickOrderRepository,
    private val inventoryLotLineRepository: InventoryLotLineRepository,
    private val stockInLineRepository: StockInLineRepository,
    @Lazy private val suggestedPickLotService: SuggestedPickLotService,
    private val suggestPickLotRepository: SuggestPickLotRepository,
    private val inventoryLotRepository: InventoryLotRepository,
    private val doPickOrderRepository: DoPickOrderRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val bagRepository: BagRepository,
private val deliveryOrderRepository: DeliveryOrderRepository,
private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val inventoryLotLineService: InventoryLotLineService,
    private val bagService: BagService,
    private val pickOrderService: PickOrderService,
    private val stockLedgerRepository: StockLedgerRepository,
    private val inventoryRepository: InventoryRepository,
    private val pickExecutionIssueRepository: PickExecutionIssueRepository,
    private val itemUomService: ItemUomService,
    @Lazy private val doReplenishmentService: DoReplenishmentService,
    ): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository)  {

    @PersistenceContext
    private lateinit var entityManager: EntityManager

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

    @Transactional
    private fun tryCompletePickOrderLine(pickOrderLineId: Long) {
        val sols = stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
        if (sols.isEmpty()) return

        val allEnded = sols.all { isEndStatus(it.status) }
        if (!allEnded) return

        val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return
        if (pol.status != PickOrderLineStatus.COMPLETED) {
            pol.status = PickOrderLineStatus.COMPLETED
            pickOrderLineRepository.save(pol)
            doReplenishmentService.completeByPickOrderLineId(pickOrderLineId)
        }

        // Optionally bubble up to pick order completion (safe no-op if not ready)
        val consoCode = pol.pickOrder?.consoCode
        if (!consoCode.isNullOrBlank()) {
            try {
                pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode)
            } catch (e: Exception) {
                println("⚠️ Error checking pick order completion for consoCode=$consoCode: ${e.message}")
            }
        }
    }

    /** When every POL on this pick order is COMPLETED or PARTIALLY_COMPLETE, mark pick order completed and cascade DO completion. */
    private fun refreshPickOrderHeaderIfAllLinesCompleted(pickOrderId: Long) {
        val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: return
        val allLines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId)
        val allCompleted = allLines.all {
            it.status == PickOrderLineStatus.COMPLETED || it.status == PickOrderLineStatus.PARTIALLY_COMPLETE
        }
        if (allCompleted && allLines.isNotEmpty()) {
            pickOrder.status = PickOrderStatus.COMPLETED
            pickOrderRepository.save(pickOrder)
            completeDoForPickOrder(pickOrderId)
            completeDoIfAllPickOrdersCompleted(pickOrderId)
        }
    }

    @Throws(IOException::class)
    @Transactional
    open fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> {
        return stockOutLineRepository.findAllByStockOutIdAndDeletedFalse(stockOutId)
    }
    @Throws(IOException::class)
    @Transactional
    open fun deleteById(id: Long): String {
        val deleteStockOutline = stockOutLineRepository.findById(id).orElseThrow().apply {
            deleted = true
        }
        val updateItem = stockOutLineRepository.save(deleteStockOutline)
        val response = "mark deleted item: ${updateItem.id}"
        return response
    }
    @Throws(IOException::class)
    @Transactional
    open fun deleteByStockOutId(id: Long): String {
        val deleteStockOutline = stockOutLineRepository.findAllByStockOutIdAndDeletedFalse(id).map {sol ->
            sol.apply {
                deleted = true
            }
        }
        val updateItemIds = stockOutLineRepository.saveAllAndFlush(deleteStockOutline).map { it.id }
        val response = "mark deleted items: $updateItemIds"
        return response
    }

    @Transactional
    open fun getAllStockOutLineByPickOrderLineId(pickOrderLineId: Long): List<StockOutLineInfo> {
        return stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
    }

    @Transactional
    open fun create(request: CreateStockOutLineRequest): MessageResponse {
        // pick flow step 1
        println("=== DEBUG: create StockOutLine ===")
        println("Request consoCode: ${request.consoCode}")
        println("Request pickOrderLineId: ${request.pickOrderLineId}")
        println("Request inventoryLotLineId: ${request.inventoryLotLineId}")
        println("Request qty: ${request.qty}")
//        println(request.pickOrderLineId)
val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
            request.pickOrderLineId, 
            request.inventoryLotLineId
        )
        
        if (existingStockOutLine.isNotEmpty()) {
            //  如果已存在，返回 null 表示不需要创建
            return MessageResponse(
                id = null,
                name = "Stock out line already exists",
                code = "EXISTS",
                type = "stock_out_line",
                message = "Stock out line already exists for this pick order line and inventory lot line",
                errorPosition = null,
                entity = null,
            )
        }
        val allStockOuts = stockOutRepository.findAll()
        println("=== DEBUG: All StockOut records ===")
        allStockOuts.forEach { stockOut ->
            println("StockOut ID: ${stockOut.id}, consoPickOrderCode: ${stockOut.consoPickOrderCode}")
        }
        val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(request.consoCode)
    ?: throw NoSuchElementException("StockOut not found for consoCode: ${request.consoCode}")
        println("Found stockOut: ${stockOut.id} with consoCode: ${stockOut.consoPickOrderCode}")
        val pickOrderLine = pickOrderLineRepository.saveAndFlush(
            pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow()
                .apply {
                    this.status = PickOrderLineStatus.PICKING
                }
        )
        val item = itemRepository.findById(pickOrderLine.item!!.id!!).orElseThrow()
        val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()
        
        val stockOutLine = StockOutLine()
            .apply {
                this.item = item
                this.qty = request.qty
                this.stockOut = stockOut
                this.inventoryLotLine = inventoryLotLine
                this.pickOrderLine = pickOrderLine
                this.status = StockOutLineStatus.PENDING.status
                this.type = "NOR"
            }
        val savedStockOutLine = saveAndFlush(stockOutLine)
        createStockLedgerForStockOut(savedStockOutLine)
        val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)
//        println("triggering")
        return MessageResponse(
            id = savedStockOutLine.id,
            name = savedStockOutLine.inventoryLotLine?.inventoryLot?.lotNo?: "",
            code = savedStockOutLine.stockOut?.consoPickOrderCode?: "",
            type = savedStockOutLine.status,
            message = "success",
            errorPosition = null,
            entity = mappedSavedStockOutLine,
        )
    }
    @Transactional
fun handleQc(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List<StockOutLine?> {
    var newStockOutLine: StockOutLine? = null
    if (request.qty < stockOutLine.qty!!) {
        newStockOutLine = StockOutLine().apply {
            this.pickOrderLine = stockOutLine.pickOrderLine
            this.stockOut = stockOutLine.stockOut
            this.item = stockOutLine.item
            this.qty = stockOutLine.qty!! - request.qty
            this.status = StockOutLineStatus.DETERMINE1.status // escalated
        }
    }
    val inventoryLotLine = if (request.inventoryLotLineId != null)
        inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()
    else null
    stockOutLine.apply {
        this.inventoryLotLine = inventoryLotLine ?: stockOutLine.inventoryLotLine
        this.qty = request.qty
        this.status = StockOutLineStatus.COMPLETE.status // complete
    }
    // update inventory lot line
    val zero = BigDecimal.ZERO
    val one = BigDecimal.ONE
    val targetLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId!!).orElseThrow()
    val salesUnit = inventoryLotLine?.inventoryLot?.item?.id?.let {_itemId -> itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(_itemId) }
    val ratio = 1.0 //(salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one).toDouble()
    val targetLotLineEntry = targetLotLine.apply {

        this.outQty = (this.outQty?: BigDecimal.ZERO) + (request.qty.div(ratio)).toBigDecimal()
        this.holdQty = this.holdQty!!.minus((request.qty.div(ratio)).toBigDecimal())
    }
    inventoryLotLineRepository.save(targetLotLineEntry)
    // update inventory
    val inventory = inventoryRepository.findByItemId(request.itemId).orElseThrow()
    val inventoryEntry = inventory.apply {
        this.onHandQty = this.onHandQty!!.minus((request.qty.div(ratio)).toBigDecimal())
        this.onHoldQty = this.onHoldQty!!.minus((request.qty.div(ratio)).toBigDecimal())
    }
    inventoryRepository.save(inventoryEntry)
    return listOf(stockOutLine, newStockOutLine)
}
    @Transactional
open fun createWithoutConso(request: CreateStockOutLineWithoutConsoRequest): MessageResponse {
    try {
        //  Get stockOutId from pickOrderLineId with detailed error
        val stockOutId = getStockOutIdFromPickOrderLine(request.pickOrderLineId)
        println("Found stockOutId: $stockOutId for pickOrderLineId: ${request.pickOrderLineId}")
        
        val stockOut = stockOutRepository.findById(stockOutId).orElseThrow {
            IllegalArgumentException("StockOut not found with ID: $stockOutId")
        }
        println("Found stockOut: ${stockOut.id} with consoCode: ${stockOut.consoPickOrderCode}")
        
        val pickOrderLine = pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow {
            IllegalArgumentException("PickOrderLine not found with ID: ${request.pickOrderLineId}")
        }
        println("Found pickOrderLine: ${pickOrderLine.id}")
        
        val updatedPickOrderLine = pickOrderLineRepository.saveAndFlush(
            pickOrderLine.apply {
                this.status = PickOrderLineStatus.PICKING
            }
        )
        println("Updated pickOrderLine status to: ${updatedPickOrderLine.status}")
        
        val item = itemRepository.findById(updatedPickOrderLine.item!!.id!!).orElseThrow {
            IllegalArgumentException("Item not found with ID: ${updatedPickOrderLine.item!!.id}")
        }
        println("Found item: ${item.id} - ${item.code}")
        
        val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow {
            IllegalArgumentException("InventoryLotLine not found with ID: ${request.inventoryLotLineId}")
        }
        println("Found inventoryLotLine: ${inventoryLotLine.id}")
       
        val stockOutLine = StockOutLine()
            .apply {
                this.item = item
                this.qty = request.qty
                this.stockOut = stockOut
                this.inventoryLotLine = inventoryLotLine
                this.pickOrderLine = updatedPickOrderLine
                this.status = StockOutLineStatus.PENDING.status
                this.type = "NOR"
            }
        println("Created stockOutLine with qty: ${request.qty}")
        
        val savedStockOutLine = saveAndFlush(stockOutLine)
        createStockLedgerForStockOut(savedStockOutLine)
        println("Saved stockOutLine with ID: ${savedStockOutLine.id}")
        
        val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)
        println("Mapped stockOutLine info: $mappedSavedStockOutLine")
        
        return MessageResponse(
            id = savedStockOutLine.id,
            name = savedStockOutLine.inventoryLotLine?.inventoryLot?.lotNo?: "",
            code = savedStockOutLine.stockOut?.consoPickOrderCode,
            type = savedStockOutLine.status,
            message = "success",
            errorPosition = null,
            entity = mappedSavedStockOutLine,
        )
    } catch (e: Exception) {
        println("Error in createWithoutConso: ${e.message}")
        e.printStackTrace()
        throw e
    }
}

//  Update helper method with detailed error messages
private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
    println("Getting stockOutId for pickOrderLineId: $pickOrderLineId")
    
    //  Fixed: Use poId instead of pick_order_id
    val sql = """
        SELECT so.id as stockOutId, so.consoPickOrderCode, po.consoCode
        FROM stock_out so
        JOIN pick_order po ON po.consoCode = so.consoPickOrderCode
        JOIN pick_order_line pol ON pol.poId = po.id  --  Fixed: Use poId
        WHERE pol.id = :pickOrderLineId
    """.trimIndent()
    
    val result = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to pickOrderLineId))
   // println("SQL result: $result")
    
    if (result.isEmpty()) {
        throw IllegalArgumentException("No StockOut found for pickOrderLineId: $pickOrderLineId. Check if pick order line exists and has associated stock out.")
    }
    
    val stockOutId = result[0]["stockOutId"] as? Long
    val consoPickOrderCode = result[0]["consoPickOrderCode"] as? String
    val consoCode = result[0]["consoCode"] as? String
    
    //println("Found stockOutId: $stockOutId, consoPickOrderCode: $consoPickOrderCode, consoCode: $consoCode")
    
    if (stockOutId == null) {
        throw IllegalArgumentException("StockOut ID is null for pickOrderLineId: $pickOrderLineId. ConsoCode: $consoCode, ConsoPickOrderCode: $consoPickOrderCode")
    }
    
    return stockOutId
}
    @Transactional
    fun handleLotChangeApprovalOrReject(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List<StockOutLine?> {
        /**
         * @param request.qty
         *  qty that requires lot change
         */
        var newStockOutLine: StockOutLine? = null

        if (request.qty < stockOutLine.qty!!) {
            newStockOutLine = StockOutLine().apply {
                this.pickOrderLine = stockOutLine.pickOrderLine
                this.stockOut = stockOutLine.stockOut
                this.item = stockOutLine.item
                this.qty = stockOutLine.qty!! - request.qty
                this.status = StockOutLineStatus.REJECTED.status // original status
            }
        }
        stockOutLine.apply {
            this.qty = request.qty
            this.status = StockOutLineStatus.LOT_CHANGE_APPROVAL.status // require new lot no
        }
        return listOf(stockOutLine, newStockOutLine)
    }

//    @Transactional
//    fun assignNewLotLine(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List<StockOutLine?> {
//        val newInventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()
//        return listOf(stockOutLine.apply {
//            this.inventoryLotLine = newInventoryLotLine
//        })
//    }
    @Transactional
    fun checkIsStockOutLineCompleted(pickOrderLineId: Long, quiet: Boolean = false) {
        val allStockOutLines = stockOutLineRepository
            .findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
        


        // 计算当前行的需求数量
        val pickOrderLine = pickOrderLineRepository.findById(pickOrderLineId).orElse(null)
        val requiredQty = pickOrderLine?.qty ?: BigDecimal.ZERO

        // 1) 计算所有拣货的累计数量（包括 completed / partially_completed / COMPLETE / PARTIALLY_COMPLETE）
        val totalPickedQty = allStockOutLines
            .filter { sol ->
                val solStatus = sol.status?.trim()?.lowercase()
                solStatus == "completed" ||
                        solStatus == "partially_completed" ||
                        solStatus == StockOutLineStatus.COMPLETE.status.lowercase() ||
                        solStatus == StockOutLineStatus.PARTIALLY_COMPLETE.status.lowercase()
            }
            .fold(BigDecimal.ZERO) { acc, sol ->
                val qtyValue = sol.qty ?: 0.0
                acc + BigDecimal(qtyValue.toString())
            }

        // 2) 计算该行所有相关 issue 的数量（miss / bad / expiry 等）
        val totalIssueQty = try {
            val issues = pickExecutionIssueRepository.findByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
            issues.fold(BigDecimal.ZERO) { acc, issue ->
                acc + (issue.issueQty ?: BigDecimal.ZERO)
            }
        } catch (e: Exception) {
            if (!quiet) {
                println(" Error fetching issues for pickOrderLineId $pickOrderLineId: ${e.message}")
            }
            BigDecimal.ZERO
        }

       // println("  totalPickedQty = $totalPickedQty, totalIssueQty = $totalIssueQty, requiredQty = $requiredQty")

        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 (!quiet) {
            println("Unfinished lines: ${unfinishedLine.size}")
            if (unfinishedLine.isNotEmpty()) {
                unfinishedLine.forEach { sol ->
                    println("  - StockOutLine ${sol.id}: status=${sol.status}, qty=${sol.qty}")
                }
            }
        }

        if (unfinishedLine.isEmpty()) {
            // set pick order line status to complete
            val pol = pickOrderLineRepository.findById(pickOrderLineId).orElseThrow()
            val previousStatus = pol.status
            pickOrderLineRepository.save(
                pol.apply {
                    this.status = PickOrderLineStatus.COMPLETED
                }
            )
            if (!quiet) {
                println("✅ Updated pick order line $pickOrderLineId from $previousStatus to COMPLETED")
            }
        } else if (!quiet) {
            println("⏳ Pick order line $pickOrderLineId not completed yet - has ${unfinishedLine.size} unfinished stock out lines")
        }
    }

    /** Batch pick: same qty rules as [InventoryLotLineService.updateInventoryLotLineQuantities] pick, without double findById / extra service layers. */
    private fun applyPickToInventoryLotLineInBatch(ill: InventoryLotLine, submitQty: BigDecimal) {
        val zero = BigDecimal.ZERO
        val newHold = (ill.holdQty ?: zero).minus(submitQty)
        val newOut = (ill.outQty ?: zero).plus(submitQty)
        if (newHold < zero || newOut < zero) {
            throw IllegalArgumentException("Invalid pick quantities for lotLine ${ill.id}: holdQty=$newHold, outQty=$newOut")
        }
        val prevStatus = ill.status
        ill.holdQty = newHold
        ill.outQty = newOut
        ill.status = inventoryLotLineService.deriveInventoryLotLineStatus(prevStatus, ill.inQty, ill.outQty, ill.holdQty)
        inventoryLotLineRepository.save(ill)
    }
    private fun completeDoIfAllPickOrdersCompleted(pickOrderId: Long) {
        // 1) 先用 line 关联找 do_pick_order_id
        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
    
            // 2) 检查这个 do_pick_order 下所有 pick orders 是否都 COMPLETED
            val statuses = allPickOrderIdsInDpo.map { id ->
                pickOrderRepository.findById(id).orElse(null)?.status
            }
            val allCompleted = statuses.all { it == PickOrderStatus.COMPLETED }
    
            if (!allCompleted) return@forEach
    
            // 3) 全部完成 → 更新 do_pick_order 为 completed，并可复制到 record 表（可选）
            val dpo = doPickOrderRepository.findById(dpoId).orElse(null) ?: return@forEach

            dpo.ticketStatus = DoPickOrderStatus.completed
            dpo.ticketCompleteDateTime = LocalDateTime.now()
            doPickOrderRepository.save(dpo)

            // 4) 可选：复制 header 到 record 表，复制 lines 到 line_record，再删除原有行/头（与你在 PickOrderService.completeStockOut 的做法保持一致）
            val savedHeader = saveDoPickOrderRecordWithDnRetry { deliveryNoteCode ->
                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 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)
    
            // 若你流程要求，把原行/头删掉（或保留 completed 状态）
            if (allLines.isNotEmpty()) doPickOrderLineRepository.deleteAll(allLines)
            doPickOrderRepository.delete(dpo)
    
            // 5) 可选：同步 delivery_order → COMPLETED
            dpo.doOrderId?.let { doId ->
                val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId)
                if (deliveryOrder != null && deliveryOrder.status != DeliveryOrderStatus.COMPLETED) {
                    deliveryOrder.status = DeliveryOrderStatus.COMPLETED
                    deliveryOrderRepository.save(deliveryOrder)
                }
            }
        }
    }
    @Transactional
    open fun update(request: UpdateStockOutLineRequest): MessageResponse {
        val stockOutLine = stockOutLineRepository.findById(request.id).orElseThrow()
        var stockOutLineEntries: List<StockOutLine?> = listOf()
        // pick flow step 2
        if (request.status == StockOutLineStatus.COMPLETE.status) { // doing qc
            //println("doing sth")
            if (request.qty <= 0) {
                return MessageResponse(
                    id = null,
                    code = null,
                    name = null,
                    type = "qty == 0",
                    message = "qty cannot be 0",
                    errorPosition = "request.qty",
                )
            }
            stockOutLineEntries = handleQc(stockOutLine, request)
        }
        if (request.status == StockOutLineStatus.LOT_CHANGE_APPROVAL.status) {
            if (request.qty < 0) {
                return MessageResponse(
                    id = null,
                    code = null,
                    name = null,
                    type = "qty < 0",
                    message = "qty cannot be smaller than 0",
                    errorPosition = "request.qty",
                )
            }
            stockOutLineEntries = handleLotChangeApprovalOrReject(stockOutLine, request)
        }

        val savedEntries = stockOutLineRepository.saveAllAndFlush(stockOutLineEntries.filterNotNull())
        val lineInfoList = stockOutLineRepository.findAllByIdIn(savedEntries.map { it.id!! })

        checkIsStockOutLineCompleted(pickOrderLineId = stockOutLine.pickOrderLine!!.id!!)

        return MessageResponse(
            id = 0,
            name = "id: ${lineInfoList.map { it.id }.joinToString(separator = ",")}",
            code = stockOutLine.stockOut!!.consoPickOrderCode,
            type = lineInfoList.joinToString(separator = ",") { it.status },
            message = "success",
            errorPosition = null,
            entity = lineInfoList,
        )
    }

    private fun saveDoPickOrderRecordWithDnRetry(
        maxAttempts: Int = 3,
        buildRecord: (String) -> DoPickOrderRecord
    ): DoPickOrderRecord {
        val prefix = "DN"
        repeat(maxAttempts) { attempt ->
            val midfix = LocalDate.now().format(CodeGenerator.DEFAULT_FORMATTER)
            val latestCode = doPickOrderRecordRepository.findLatestDeliveryNoteCodeByPrefix("$prefix-$midfix")
            val deliveryNoteCode = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode)
            val dpoRecord = buildRecord(deliveryNoteCode)
            try {
                return doPickOrderRecordRepository.save(dpoRecord)
            } catch (e: DataIntegrityViolationException) {
                if (attempt == maxAttempts - 1) {
                    throw e
                }
            }
        }
        throw IllegalStateException("Failed to generate unique delivery note code after $maxAttempts attempts")
    }
    private fun completeDoForPickOrder(pickOrderId: Long) {
        // 1) 原表 do_pick_order → completed
        val dpos = doPickOrderRepository.findByPickOrderId(pickOrderId)
        if (dpos.isNotEmpty()) {
            dpos.forEach {
                it.ticketStatus = com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus.completed
                it.ticketCompleteDateTime = java.time.LocalDateTime.now()
            }
            doPickOrderRepository.saveAll(dpos)

            // 2) 同步相关 delivery_order 状态
            dpos.forEach { dpo ->
                dpo.doOrderId?.let { doId ->
                    val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId)
                    if (deliveryOrder != null &&
                        deliveryOrder.status != com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus.COMPLETED
                    ) {
                        deliveryOrder.status = com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus.COMPLETED
                        deliveryOrderRepository.save(deliveryOrder)
                    }
                }
            }
        }

        // 3) 记录表 do_pick_order_record → completed（可选）
        val dporList = doPickOrderRecordRepository.findByPickOrderId(pickOrderId)
        if (dporList.isNotEmpty()) {
            dporList.forEach {
                it.ticketStatus = com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus.completed
                it.ticketCompleteDateTime = java.time.LocalDateTime.now()
            }
            doPickOrderRecordRepository.saveAll(dporList)
        }
    }
    open fun updateStatus(request: UpdateStockOutLineStatusRequest): MessageResponse {
        try {
            // 1. 查当前 stockOutLine（用于日志）
            val stockOutLine = stockOutLineRepository.findById(request.id).orElseThrow {
                IllegalArgumentException("StockOutLine not found with ID: ${request.id}")
            }

            println("Updating StockOutLine ID: ${request.id}")
            println("Current status: ${stockOutLine.status}")
            println("New status: ${request.status}")
            val deferAggregate = request.deferAggregatePickOrderEffects == true
            val savedStockOutLine = applyStockOutLineDelta(
                stockOutLine = stockOutLine,
                deltaQty = BigDecimal((request.qty ?: 0.0).toString()),
                newStatus = request.status,
                skipInventoryWrite = request.skipInventoryWrite == true,
                skipLedgerWrite = request.skipLedgerWrite == true,
                skipTryCompletePickOrderLine = deferAggregate,
                deferPersistenceFlush = deferAggregate
            )
            println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}")
            if (!deferAggregate) {
                try {
                    val item = savedStockOutLine.item
                    val inventoryLotLine = savedStockOutLine.inventoryLotLine
                    val reqDeltaQty = request.qty ?: 0.0

                    // 只在状态为 completed 或 partially_completed，且数量增加时创建 BagLotLine
                    val isCompletedOrPartiallyCompleted = request.status == "completed" ||
                        request.status == "partially_completed" ||
                        request.status == "PARTIALLY_COMPLETE"

                    if (item?.isBag == true &&
                        inventoryLotLine != null &&
                        isCompletedOrPartiallyCompleted &&
                        reqDeltaQty > 0
                    ) {

                        println("  Item isBag=true, creating BagLotLine...")

                        val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!)

                        if (bag != null) {
                            val lotNo = inventoryLotLine.inventoryLot?.lotNo
                            if (lotNo != null) {
                                val createBagLotLineRequest = CreateBagLotLineRequest(
                                    bagId = bag.id!!,
                                    lotId = inventoryLotLine.inventoryLot?.id ?: 0L,
                                    itemId = item.id!!,
                                    lotNo = lotNo,
                                    stockQty = reqDeltaQty.toInt(),
                                    date = LocalDate.now(),
                                    time = LocalTime.now(),
                                    stockOutLineId = savedStockOutLine.id
                                )

                                bagService.createBagLotLinesByBagId(createBagLotLineRequest)
                                println("  ✓ BagLotLine created successfully for item ${item.code}")
                            } else {
                                println("     Warning: lotNo is null, skipping BagLotLine creation")
                            }
                        } else {
                            println("     Warning: Bag not found for itemId ${item.id}, skipping BagLotLine creation")
                        }
                    }
                } catch (e: Exception) {
                    println("     Error creating BagLotLine: ${e.message}")
                    e.printStackTrace()
                    // 不中断主流程，只记录错误
                }
            }
            // 3. 如果被拒绝，触发特殊处理
            if (request.status == "rejected" || request.status == "REJECTED") {
                println("=== TRIGGERING LOT REJECTION LOGIC ===")
                handleLotRejectionFromStockOutLine(savedStockOutLine)
            }
    
            // 4–5. 自动刷 pickOrderLine / pickOrder 状态（批次提交在结尾统一处理）
            if (!deferAggregate) {
                val pickOrderLine = savedStockOutLine.pickOrderLine
                if (pickOrderLine != null) {
                    checkIsStockOutLineCompleted(pickOrderLine.id)
                    pickOrderLine.pickOrder?.id?.let { refreshPickOrderHeaderIfAllLinesCompleted(it) }
                }
            }

            val mappedSavedStockOutLine =
                if (deferAggregate) null
                else stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)
            return MessageResponse(
                id = savedStockOutLine.id,
                name = savedStockOutLine.inventoryLotLine?.inventoryLot?.lotNo?: "",
                code = savedStockOutLine.stockOut?.consoPickOrderCode?: "",
                type = savedStockOutLine.status,
                message = "Stock out line status updated successfully",
                errorPosition = null,
                entity = mappedSavedStockOutLine,
            )
        } catch (e: Exception) {
            println("Error updating stock out line status: ${e.message}")
            e.printStackTrace()
            throw e
        }
    }
   //  ADD THIS: Handle lot rejection when stock out line is rejected
   private fun handleLotRejectionFromStockOutLine(stockOutLine: StockOutLine) {
    try {
        println("=== HANDLING LOT REJECTION FROM STOCK OUT LINE ===")
        println("StockOutLine ID: ${stockOutLine.id}")
        println("StockOutLine Status: ${stockOutLine.status}")
        
        val inventoryLotLine = stockOutLine.inventoryLotLine
        if (inventoryLotLine != null) {
            println("InventoryLotLine ID: ${inventoryLotLine.id}")
            println("Current lot status: ${inventoryLotLine.status}")
            println("Current holdQty: ${inventoryLotLine.holdQty}")
            
            // Step 2: Update inventory lot line
            inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE
            inventoryLotLine.holdQty = BigDecimal.ZERO
            
            println("Setting lot status to UNAVAILABLE and holdQty to 0")
            val savedLotLine = inventoryLotLineRepository.save(inventoryLotLine)
            println("Saved lot line: ${savedLotLine.id}, status: ${savedLotLine.status}, holdQty: ${savedLotLine.holdQty}")
            
            // Step 3: Update inventory table
            println("Updating inventory table...")
            updateInventoryTableAfterLotRejection(inventoryLotLine)
            
            // Step 4: Trigger resuggest
            val pickOrderLine = stockOutLine.pickOrderLine
            if (pickOrderLine?.pickOrder?.id != null) {
                println("Triggering resuggest for pick order ID: ${pickOrderLine.pickOrder!!.id}")
                suggestedPickLotService.resuggestPickOrder(pickOrderLine.pickOrder!!.id!!)
                println("Resuggest completed")
            } else {
                println("No pick order found for resuggest")
            }
        } else {
            println("No inventory lot line found in stock out line")
        }
    } catch (e: Exception) {
        println("Error handling lot rejection from stock out line: ${e.message}")
        e.printStackTrace()
    }
}

//  ADD THIS: Update inventory table after lot rejection
private fun updateInventoryTableAfterLotRejection(inventoryLotLine: InventoryLotLine) {
    try {
        println("=== UPDATING INVENTORY TABLE ===")
        val itemId = inventoryLotLine.inventoryLot?.item?.id
        println("Item ID: $itemId")
        
        if (itemId != null) {
            // Calculate onHoldQty (sum of holdQty from available lots only)
            val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
                .sumOf { it.holdQty ?: BigDecimal.ZERO }
            
            // Calculate unavailableQty (sum of inQty from unavailable lots only)
            val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId,InventoryLotLineStatus.UNAVAILABLE)
            .sumOf { 
                val inQty = it.inQty ?: BigDecimal.ZERO
                val outQty = it.outQty ?: BigDecimal.ZERO
                val remainingQty = inQty.minus(outQty)
                remainingQty
            }
            
            println("Calculated onHoldQty: $onHoldQty")
            println("Calculated unavailableQty: $unavailableQty")
            
            // Update the inventory table
            val inventory = inventoryRepository.findByItemId(itemId).orElse(null)
            if (inventory != null) {
                println("Found inventory record: ${inventory.id}")
                println("Current inventory - onHoldQty: ${inventory.onHoldQty}, unavailableQty: ${inventory.unavailableQty}")
                
                inventory.onHoldQty = onHoldQty
                inventory.unavailableQty = unavailableQty
                val savedInventory = inventoryRepository.save(inventory)
                
                println("Updated inventory - onHoldQty: ${savedInventory.onHoldQty}, unavailableQty: ${savedInventory.unavailableQty}")
            } else {
                println("No inventory record found for item $itemId")
            }
        }
    } catch (e: Exception) {
        println("Error updating inventory table after lot rejection: ${e.message}")
        e.printStackTrace()
    }
}

@Transactional(rollbackFor = [Exception::class])
open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
    val startTime = System.currentTimeMillis()
    println(" Start time: ${java.time.LocalDateTime.now()}")
    if (request.lines.isEmpty()) {
        return MessageResponse(
            id = null,
            name = "No lines",
            code = "EMPTY",
            type = "batch_submit",
            message = "No scanned lines",
            errorPosition = null
        )
    }

    val errors = mutableListOf<String>()
    val processedIds = mutableListOf<Long>()

    request.lines.forEach { line:QrPickSubmitLineRequest ->
        val lineStartTime = System.currentTimeMillis()
        try {
            // 1) noLot 情况：等价于前端 handleSubmitAllScanned 里 noLot 分支
            if (line.noLot) {
                val req = UpdateStockOutLineStatusRequest(
                    id = line.stockOutLineId,
                    status = "completed",
                    qty = 0.0
                )
                updateStatus(req)   // 直接复用已有逻辑（会自动更新行/订单状态）
                processedIds += line.stockOutLineId
                // 如果需要，这里可以再调记录 issue 的 service，而不是让前端调
                return@forEach
            }

            // 2) 正常有 lot：照抄你前端 handleSubmitAllScanned 的计算逻辑
            val required = line.requiredQty ?: BigDecimal.ZERO
            val currentActual = line.actualPickQty ?: BigDecimal.ZERO
            val submitQty = if (line.requiredQty != null) line.requiredQty else line.requiredQty
            val submitQtyNotNull = submitQty ?: BigDecimal.ZERO
            val cumulative = currentActual + submitQtyNotNull

            var newStatus = "partially_completed"
            if (cumulative >= required) {
                newStatus = "completed"
            }

            // 注意：updateStatus 是「在原 qty 上 + qty」，所以这里传的是「本次增量」而不是 cumulative
            val updateReq = UpdateStockOutLineStatusRequest(
                id = line.stockOutLineId,
                status = newStatus,
                qty = submitQtyNotNull.toDouble()
            )
            updateStatus(updateReq)   // 内部已经会做 pickOrderLine / pickOrder / DO 完成检查

            // 3) 扣库存：等价于前端 updateInventoryLotLineQuantities
            if (submitQtyNotNull > BigDecimal.ZERO && line.inventoryLotLineId != null) {
                val lotLine = inventoryLotLineRepository.findById(line.inventoryLotLineId).orElseThrow()
                val zero = BigDecimal.ZERO
                val one = BigDecimal.ONE
                val salesUnit = lotLine.inventoryLot?.item?.id?.let { itemId ->
                    itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(itemId)
                }
                val ratio = 1.0 //(salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one).toDouble()
                val baseQty = (submitQtyNotNull.toDouble() / ratio).toBigDecimal()

                // 更新 lot line
                lotLine.apply {
                    this.outQty = (this.outQty ?: zero) + baseQty
                    this.holdQty = (this.holdQty ?: zero) - baseQty
                }
                inventoryLotLineRepository.save(lotLine)

                // 更新 inventory
                val inv = inventoryRepository.findByItemId(lotLine.inventoryLot?.item?.id!!).orElseThrow()
                inv.apply {
                    this.onHandQty = (this.onHandQty ?: zero) - baseQty
                    this.onHoldQty = (this.onHoldQty ?: zero) - baseQty
                }
                inventoryRepository.save(inv)
            }

            processedIds += line.stockOutLineId
            val lineTime = System.currentTimeMillis() - lineStartTime
            // 4) 如果你还想保留「按 consoCode 再单独 check-complete」的接口，也可以在这里再调一次：
            // if (newStatus == "completed" && line.pickOrderConsoCode != null) {
            //     pickOrderService.checkAndCompletePickOrderByConsoCode(line.pickOrderConsoCode)
            // }

        } catch (e: Exception) {
            errors += "stockOutLineId=${line.stockOutLineId}: ${e.message}"
        }
    }

    val msg = if (errors.isEmpty()) {
        "Batch submit success (${processedIds.size} lines)."
    } else {
        "Batch submit partial success (${processedIds.size} lines), errors: ${errors.joinToString("; ")}"
    }
    val totalTime = System.currentTimeMillis() - startTime
    println(" Processed: ${processedIds.size}/${request.lines.size} items")
    println(" Total time: ${totalTime}ms (${totalTime / 1000.0}s)")
    println(" Average time per item: ${if (processedIds.isNotEmpty()) totalTime / processedIds.size else 0}ms")
    println(" End time: ${java.time.LocalDateTime.now()}")
    return MessageResponse(
        id = null,
        name = "batch_submit",
        code = if (errors.isEmpty()) "SUCCESS" else "PARTIAL_SUCCESS",
        type = "batch_submit",
        message = msg,
        errorPosition = null,
        entity = mapOf(
            "processedIds" to processedIds,
            "errors" to errors
        )
    )
}

@Transactional
open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineStatusByQRCodeAndLotNoRequest): MessageResponse {
    val totalStartTime = System.currentTimeMillis()
    val startTime = System.currentTimeMillis()
    try {
        println("===  QR SCAN REQUEST RECEIVED ===")
        println("⏰ Request received at: ${java.time.LocalDateTime.now()}")
        println(" Request details:")
        println("   - stockOutLineId: ${request.stockOutLineId}")
        println("   - pickOrderLineId: ${request.pickOrderLineId}")
        println("   - inventoryLotNo: ${request.inventoryLotNo}")
        println("   - itemId: ${request.itemId}")
        println("   - status: ${request.status}")

        // Step 1: Find StockOutLine
        val findStockOutLineStart = System.currentTimeMillis()
        val stockOutLine = stockOutLineRepository.findById(request.stockOutLineId).orElseThrow {
            println("  StockOutLine not found with ID: ${request.stockOutLineId}")
            IllegalArgumentException("StockOutLine not found with ID: ${request.stockOutLineId}")
        }
        val findStockOutLineTime = System.currentTimeMillis() - findStockOutLineStart
        println("⏱️ [STEP 1] Find StockOutLine: ${findStockOutLineTime}ms")
        
        println(" Found StockOutLine:")
        println("   - ID: ${stockOutLine.id}")
        println("   - Current status: ${stockOutLine.status}")
        println("   - Qty: ${stockOutLine.qty}")
        println("   - PickOrderLine ID: ${stockOutLine.pickOrderLine?.id}")
        println("   - Item ID: ${stockOutLine.item?.id}")
        println("   - InventoryLotLine ID: ${stockOutLine.inventoryLotLine?.id}")
        
        // Step 2: Get InventoryLotLine
        val getInventoryLotLineStart = System.currentTimeMillis()
        // If StockOutLine has no lot (noLot row), resolve InventoryLotLine by scanned lotNo + itemId and bind it
        var inventoryLotLine = stockOutLine.inventoryLotLine
        if (inventoryLotLine == null) {
            // Prefer stockInLineId from QR for deterministic binding
            val resolved = if (request.stockInLineId != null && request.stockInLineId > 0) {
                println(" Resolving InventoryLotLine by stockInLineId=${request.stockInLineId} ...")
                val sil = stockInLineRepository.findById(request.stockInLineId).orElse(null)
                val ill = sil?.inventoryLotLine
                if (ill == null) {
                    println(" StockInLine ${request.stockInLineId} has no associated InventoryLotLine")
                    null
                } else {
                    // item consistency guard
                    val illItemId = ill.inventoryLot?.item?.id
                    if (illItemId != null && illItemId != request.itemId) {
                        println(" InventoryLotLine item mismatch for stockInLineId=${request.stockInLineId}: $illItemId != ${request.itemId}")
                        null
                    } else {
                        ill
                    }
                }
            } else {
                println(" StockOutLine has no associated InventoryLotLine, resolving by lotNo+itemId...")
                inventoryLotLineRepository
                    .findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(
                        request.inventoryLotNo,
                        request.itemId
                    )
            }
            if (resolved == null) {
                println(" Cannot resolve InventoryLotLine by lotNo=${request.inventoryLotNo}, itemId=${request.itemId}")
                return MessageResponse(
                    id = null,
                    name = "No inventory lot line",
                    code = "NO_INVENTORY_LOT_LINE",
                    type = "error",
                    message = "Cannot resolve InventoryLotLine (stockInLineId=${request.stockInLineId ?: "null"}, lotNo=${request.inventoryLotNo}, itemId=${request.itemId})",
                    errorPosition = null
                )
            }
            val targetUnavailable = resolved.status == InventoryLotLineStatus.UNAVAILABLE
            if (targetUnavailable) {
                println(" noLot bind target is UNAVAILABLE (id=${resolved.id}), will bind only without status update")
            }
            // Bind the lot line to this stockOutLine so subsequent operations can proceed
            stockOutLine.inventoryLotLine = resolved
            stockOutLine.item = stockOutLine.item ?: resolved.inventoryLot?.item
            inventoryLotLine = resolved

            // Also update SuggestedPickLot to point to the resolved lot line (so UI/holdQty logic matches DO confirmLotSubstitution)
            val spl = suggestPickLotRepository.findFirstByStockOutLineId(stockOutLine.id!!)
            if (spl != null) {
                spl.suggestedLotLine = resolved
                suggestPickLotRepository.saveAndFlush(spl)
            }
        }
        val getInventoryLotLineTime = System.currentTimeMillis() - getInventoryLotLineStart
        println("⏱️ [STEP 2] Get InventoryLotLine: ${getInventoryLotLineTime}ms")
        
        // inventoryLotLine is guaranteed non-null here
        
        // Step 3: Get InventoryLot
        val getInventoryLotStart = System.currentTimeMillis()
        val inventoryLot = inventoryLotLine.inventoryLot
        val getInventoryLotTime = System.currentTimeMillis() - getInventoryLotStart
        println("⏱️ [STEP 3] Get InventoryLot: ${getInventoryLotTime}ms")
        
        if (inventoryLot == null) {
            println(" InventoryLotLine has no associated InventoryLot")
            return MessageResponse(
                id = null,
                name = "No inventory lot",
                code = "NO_INVENTORY_LOT",
                type = "error",
                message = "InventoryLotLine ${inventoryLotLine.id} has no associated InventoryLot",
                errorPosition = null
            )
        }
        
        println("  Checking inventory lot:")
        println("   - Found inventory lot:")
        println("     - Lot No: ${inventoryLot.lotNo}")
        println("     - Item ID: ${inventoryLot.item?.id}")
        println("     - InventoryLot ID: ${inventoryLot.id}")

        // Step 4: Validation
        val validationStart = System.currentTimeMillis()
        //   修复：比较逻辑
        val lotNoMatch = inventoryLot.lotNo == request.inventoryLotNo
        val itemIdMatch = inventoryLot.item?.id == request.itemId
        val validationTime = System.currentTimeMillis() - validationStart
        println("⏱️ [STEP 4] Validation: ${validationTime}ms")
        
        println("    Matching results:")
        println("   - Lot No match: $lotNoMatch (Expected: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo})")
        println("   - Item ID match: $itemIdMatch (Expected: ${inventoryLot.item?.id}, Got: ${request.itemId})")

        if (lotNoMatch && itemIdMatch) {
            val targetUnavailable = inventoryLotLine.status == InventoryLotLineStatus.UNAVAILABLE
            if (targetUnavailable) {
                println(" Target lot is UNAVAILABLE. Bind kept, skip stockOutLine status update.")
                val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
                val mappedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)
                return MessageResponse(
                    id = savedStockOutLine.id,
                    name = inventoryLot.lotNo,
                    code = "BOUND_UNAVAILABLE",
                    type = savedStockOutLine.status,
                    message = "Bound to unavailable lot without stock out line status update",
                    errorPosition = null,
                    entity = mappedStockOutLine
                )
            }
           
            println(" MATCH SUCCESS: Lot and Item both match!")
            
            // Step 5: Update StockOutLine
            val updateStart = System.currentTimeMillis()
            stockOutLine.status = request.status
            if (request.status == "checked") {
                stockOutLine.startTime = LocalDateTime.now()
            }
            if (request.status == "completed") {
                // ✅ 修复：如果 startTime 为 null，也设置它（处理直接完成的情况）
                if (stockOutLine.startTime == null) {
                    stockOutLine.startTime = LocalDateTime.now()
                }
                stockOutLine.endTime = LocalDateTime.now()
            }
            val updateTime = System.currentTimeMillis() - updateStart
            println("⏱️ [STEP 5] Update entity: ${updateTime}ms")
            
            // Step 6: Save to database
            val saveStart = System.currentTimeMillis()
            val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
            val saveTime = System.currentTimeMillis() - saveStart
            println("⏱️ [STEP 6] Save to DB: ${saveTime}ms")
            
            println("  Status updated successfully:")
            println("   - New status: ${savedStockOutLine.status}")
            println("   - StockOutLine ID: ${savedStockOutLine.id}")
            
            // Step 7: Fetch mapped info
            val fetchMappedStart = System.currentTimeMillis()
            val mappedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!)
            val fetchMappedTime = System.currentTimeMillis() - fetchMappedStart
            println("⏱️ [STEP 7] Fetch mapped info: ${fetchMappedTime}ms")
            
            val totalTime = System.currentTimeMillis() - totalStartTime
            println("===   QR SCAN COMPLETED SUCCESSFULLY ===")
            println("⏱️ [TOTAL BACKEND TIME] ${totalTime}ms (${totalTime / 1000.0}s)")
            println("📊 Breakdown: findStockOutLine=${findStockOutLineTime}ms, getInventoryLotLine=${getInventoryLotLineTime}ms, getInventoryLot=${getInventoryLotTime}ms, validation=${validationTime}ms, update=${updateTime}ms, save=${saveTime}ms, fetchMapped=${fetchMappedTime}ms")
            println("⏰ Completed at: ${java.time.LocalDateTime.now()}")
            
            return MessageResponse(
                id = savedStockOutLine.id,
                name = inventoryLot.lotNo,
                code = "checked", //   修复：返回操作结果，而不是 consoPickOrderCode
                type = request.status,
                message = "Stock out line status updated successfully",
                errorPosition = null,
                entity = mappedStockOutLine
            )
        } else if (!lotNoMatch && itemIdMatch) {
            // Item 匹配但 lotNo 不匹配
            val totalTime = System.currentTimeMillis() - totalStartTime
            println("⏱️ [TOTAL BACKEND TIME] ${totalTime}ms (LOT MISMATCH)")
            println("  LOT NUMBER MISMATCH:")
            println("   - Expected lotNo: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo}")
            println("   - Item ID matches: ${inventoryLot.item?.id} == ${request.itemId}")
            
            return MessageResponse(
                id = null,
                name = "Item match, lot number mismatch",
                code = "LOT_NUMBER_MISMATCH",
                type = "lot_not_found",
                message = "Item match, lot number mismatch. Expected: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo}",
                errorPosition = null
            )
        } else {
            // Item 不匹配
            val totalTime = System.currentTimeMillis() - totalStartTime
            println("⏱️ [TOTAL BACKEND TIME] ${totalTime}ms (ITEM MISMATCH)")
            println("  ITEM MISMATCH:")
            println("   - Expected itemId: ${inventoryLot.item?.id}, Got: ${request.itemId}")
            println("   - Expected lotNo: ${inventoryLot.lotNo}, Got: ${request.inventoryLotNo}")
            
            return MessageResponse(
                id = null,
                name = "Item mismatch",
                code = "ITEM_MISMATCH",
                type = "item_not_found",
                message = "Item mismatch. Expected: ${inventoryLot.item?.id}, Got: ${request.itemId}",
                errorPosition = null
            )
        }

    } catch (e: Exception) {
        val totalTime = System.currentTimeMillis() - totalStartTime
        println("⏱️ [TOTAL BACKEND TIME] ${totalTime}ms (ERROR)")
        println("  ERROR updating stock out line status by QR: ${e.message}")
        println("  Exception type: ${e.javaClass.simpleName}")
        println("  Stack trace:")
        e.printStackTrace()
        return MessageResponse(
            id = null,
            name = "Error",
            code = "ERROR",
            type = "error",
            message = "Error updating stock out line: ${e.message}",
            errorPosition = null
        )
    }
}

@Transactional(rollbackFor = [Exception::class])
open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
    val startTime = System.currentTimeMillis()
    val traceId = "BATCH-${UUID.randomUUID().toString().substring(0, 8)}"
    println("=== BATCH SUBMIT START ===")
    println("[$traceId] Batch submit request received")
    println("Start time: ${java.time.LocalDateTime.now()}")
    println("Request lines count: ${request.lines.size}")

    if (request.lines.isEmpty()) {
        return MessageResponse(
            id = null,
            name = "No lines",
            code = "EMPTY",
            type = "batch_submit",
            message = "No scanned lines",
            errorPosition = null
        )
    }

    val errors = mutableListOf<String>()
    val processedIds = mutableListOf<Long>()

    try {
        // 1) Bulk load lot lines（下方會用 lotLines 取批次/袋裝等）
        // 注意：勿呼叫 inventoryRepository.findByItemId —— 若歷史資料同一 item 有多筆 inventory，
        // Spring Data 的 Optional 單筆查詢會拋 NonUniqueResultException（「unique result: 2」）。
        // 先前預載的 inventories 從未使用（save 已註解），已移除以避免批量提交無故失敗。
        val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId }
        println("Loading ${lotLineIds.size} lot lines...")
        val lotLinesById: MutableMap<Long, InventoryLotLine> = if (lotLineIds.isNotEmpty()) {
            inventoryLotLineRepository.findAllById(lotLineIds).associateByTo(mutableMapOf()) { it.id!! }
        } else {
            mutableMapOf()
        }

        // 2) Bulk load all stock out lines（批次內就地更新）
        val stockOutLineIds = request.lines.map { it.stockOutLineId }
        println("Loading ${stockOutLineIds.size} stock out lines...")
        val stockOutLinesById =
            stockOutLineRepository.findAllById(stockOutLineIds).associateByTo(mutableMapOf()) { it.id!! }

        // 3) Process each request line（直接 applyStockOutLineDelta + 內聯扣庫存，避免 updateStatus 與雙重查詢）
        request.lines.forEach { line: QrPickSubmitLineRequest ->
            val lineTrace = "$traceId|SOL=${line.stockOutLineId}"
            try {
                val solEntity = stockOutLinesById[line.stockOutLineId]
                    ?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found")

                if (line.noLot) {
                    val updated = applyStockOutLineDelta(
                        stockOutLine = solEntity,
                        deltaQty = BigDecimal.ZERO,
                        newStatus = "completed",
                        skipInventoryWrite = true,
                        skipLedgerWrite = true,
                        skipTryCompletePickOrderLine = true,
                        deferPersistenceFlush = true
                    )
                    stockOutLinesById[line.stockOutLineId] = updated
                    processedIds += line.stockOutLineId
                    return@forEach
                }

                val currentStatus = solEntity.status?.trim()?.lowercase()
                if (currentStatus == "completed" || currentStatus == "complete") {
                    return@forEach
                }

                val currentActual = (solEntity.qty ?: 0.0).toBigDecimal()
                val targetActual = line.actualPickQty ?: BigDecimal.ZERO
                val required = line.requiredQty ?: BigDecimal.ZERO
                val submitQty = targetActual - currentActual
                val newStatus = line.stockOutLineStatus
                    ?: if (targetActual >= required) "completed" else "partially_completed"

                if (submitQty <= BigDecimal.ZERO) {
                    val updated = applyStockOutLineDelta(
                        stockOutLine = solEntity,
                        deltaQty = BigDecimal.ZERO,
                        newStatus = newStatus,
                        skipInventoryWrite = true,
                        skipLedgerWrite = true,
                        skipTryCompletePickOrderLine = true,
                        deferPersistenceFlush = true
                    )
                    stockOutLinesById[line.stockOutLineId] = updated
                    return@forEach
                }

                val savedSol = applyStockOutLineDelta(
                    stockOutLine = solEntity,
                    deltaQty = submitQty,
                    newStatus = newStatus,
                    skipInventoryWrite = true,
                    skipLedgerWrite = true,
                    skipTryCompletePickOrderLine = true,
                    deferPersistenceFlush = true
                )
                stockOutLinesById[line.stockOutLineId] = savedSol

                val actualInventoryLotLineId =
                    line.inventoryLotLineId ?: savedSol.inventoryLotLine?.id

                if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) {
                    val ill = lotLinesById[actualInventoryLotLineId]
                        ?: inventoryLotLineRepository.findById(actualInventoryLotLineId).orElseThrow().also {
                            lotLinesById[actualInventoryLotLineId] = it
                        }
                    val item = savedSol.item
                    val inventoryBeforeUpdate = item?.id?.let { itemId ->
                        itemUomService.findInventoryForItemBaseUom(itemId)
                    }
                    val onHandQtyBeforeUpdate =
                        (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
                    applyPickToInventoryLotLineInBatch(ill, submitQty)
                    createStockLedgerForPickDelta(
                        stockOutLine = savedSol,
                        deltaQty = submitQty,
                        onHandQtyBeforeUpdate = onHandQtyBeforeUpdate,
                        traceTag = lineTrace,
                        flushAfterSave = false
                    )
                } else if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId == null) {
                    val item = savedSol.item
                    val inventoryBeforeUpdate = item?.id?.let { itemId ->
                        itemUomService.findInventoryForItemBaseUom(itemId)
                    }
                    val onHandQtyBeforeUpdate =
                        (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble()
                    createStockLedgerForPickDelta(
                        stockOutLine = savedSol,
                        deltaQty = submitQty,
                        onHandQtyBeforeUpdate = onHandQtyBeforeUpdate,
                        traceTag = lineTrace,
                        flushAfterSave = false
                    )
                }

                try {
                    val item = savedSol.item
                    val inventoryLotLine = line.inventoryLotLineId?.let { lid -> lotLinesById[lid] }
                    if (item?.isBag == true && inventoryLotLine != null && submitQty > BigDecimal.ZERO) {
                        val bag = bagRepository.findByItemIdAndDeletedIsFalse(item.id!!)
                        if (bag != null) {
                            val lotNo = inventoryLotLine.inventoryLot?.lotNo
                            if (lotNo != null) {
                                val createBagLotLineRequest = CreateBagLotLineRequest(
                                    bagId = bag.id!!,
                                    lotId = inventoryLotLine.inventoryLot?.id ?: 0L,
                                    itemId = item.id!!,
                                    stockOutLineId = savedSol.id,
                                    lotNo = lotNo,
                                    stockQty = submitQty.toInt(),
                                    date = LocalDate.now(),
                                    time = LocalTime.now()
                                )
                                bagService.createBagLotLinesByBagId(createBagLotLineRequest)
                            }
                        }
                    }
                } catch (e: Exception) {
                    println("     Error creating BagLotLine: ${e.message}")
                    e.printStackTrace()
                }
                processedIds += line.stockOutLineId
            } catch (e: Exception) {
                println("[$lineTrace] Error processing line: ${e.message}")
                e.printStackTrace()
                errors += "stockOutLineId=${line.stockOutLineId}: ${e.message}"
            }
        }

        entityManager.flush()

        // 批次內已 defer：此處只處理本批有碰到的 POL，再刷新所屬 pick order 表頭，最後每個 conso 只掃一次
        val polIdsTouched = request.lines.mapNotNull { line ->
            stockOutLinesById[line.stockOutLineId]?.pickOrderLine?.id
        }.distinct()

        println("=== Checking ${polIdsTouched.size} pick order lines touched by batch ===")
        polIdsTouched.forEach { pickOrderLineId ->
            try {
                checkIsStockOutLineCompleted(pickOrderLineId, quiet = true)
            } catch (e: Exception) {
                println("Error checking pick order line $pickOrderLineId: ${e.message}")
            }
        }

        val pickOrderIdsTouched = request.lines.mapNotNull { line ->
            stockOutLinesById[line.stockOutLineId]?.pickOrderLine?.pickOrder?.id
        }.distinct()

        pickOrderIdsTouched.forEach { pickOrderId ->
            try {
                refreshPickOrderHeaderIfAllLinesCompleted(pickOrderId)
            } catch (e: Exception) {
                println("Error refreshing pick order header for pickOrderId=$pickOrderId: ${e.message}")
            }
        }

        val affectedConsoCodes = pickOrderIdsTouched.mapNotNull { pickOrderId ->
            pickOrderRepository.findById(pickOrderId).orElse(null)?.consoCode
        }
            .filter { !it.isNullOrBlank() }
            .distinct()

        println("=== Checking completion by consoCode for ${affectedConsoCodes.size} affected consoCodes after batch submit ===")
        affectedConsoCodes.forEach { consoCode ->
            try {
                val result = pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode)
                println("   -> checkAndCompletePickOrderByConsoCode($consoCode) result: code=${result.code}, message=${result.message}")
            } catch (e: Exception) {
                println("Error checking completion for consoCode=$consoCode: ${e.message}")
                e.printStackTrace()
            }
        }
        val msg = if (errors.isEmpty()) {
            "Batch submit success (${processedIds.size} lines)."
        } else {
            "Batch submit partial success (${processedIds.size} lines), errors: ${errors.joinToString("; ")}"
        }

        val totalTime = System.currentTimeMillis() - startTime
        println("Processed: ${processedIds.size}/${request.lines.size} items")
        println("Total time: ${totalTime}ms (${totalTime / 1000.0}s)")
        println("Average time per item: ${if (processedIds.isNotEmpty()) totalTime / processedIds.size else 0}ms")
        println("End time: ${java.time.LocalDateTime.now()}")
        println("=== BATCH SUBMIT END ===")

        return MessageResponse(
            id = null,
            name = "batch_submit",
            code = if (errors.isEmpty()) "SUCCESS" else "PARTIAL_SUCCESS",
            type = "batch_submit",
            message = msg,
            errorPosition = null,
            entity = mapOf(
                "processedIds" to processedIds,
                "errors" to errors
            )
        )
    } catch (e: Exception) {
        println("=== BATCH SUBMIT ERROR ===")
        println("Error: ${e.message}")
        e.printStackTrace()
        return MessageResponse(
            id = null,
            name = "batch_submit",
            code = "ERROR",
            type = "batch_submit",
            message = "Error: ${e.message}",
            errorPosition = null,
            entity = mapOf(
                "processedIds" to processedIds,
                "errors" to listOf(e.message ?: "Unknown error")
            )
        )
    }
}
    @Transactional
    private fun createStockLedgerForStockOut(stockOutLine: StockOutLine) {
        val item = stockOutLine.item ?: return
        val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) ?: return

        val outQty = stockOutLine.qty?.toDouble() ?: 0.0
        // Use latest ledger balance (same pattern as createStockLedgerForStockIn) so balance is correct when multiple actions run in one transaction
        val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(item.id!!)
        val previousBalance = latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
        val newBalance = previousBalance - outQty

        val stockLedger = StockLedger().apply {
            this.stockOutLine = stockOutLine
            this.inventory = inventory
            this.inQty = null
            this.outQty = outQty
            this.balance = newBalance
            this.type = stockOutLine.type
            this.itemId = item.id
            this.itemCode = item.code
            this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
                ?: inventory.uom?.id
            this.date = LocalDate.now()
        }

        stockLedgerRepository.saveAndFlush(stockLedger)
    }

    /** Used for error messages when validating out-only flows (adjustment vs stock transfer). */
    private enum class OutOnlyLotLineContext {
        ADJUSTMENT,
        STOCK_TRANSFER,
    }

    /**
     * Validates qty for flows that only increase [InventoryLotLine.outQty] (hold unchanged):
     * qty > 0 and qty <= inQty - outQty - holdQty.
     */
    private fun validateOutOnlyIncrease(
        inventoryLotLine: InventoryLotLine,
        qtyBd: BigDecimal,
        context: OutOnlyLotLineContext,
    ) {
        if (qtyBd <= BigDecimal.ZERO) {
            val msg = when (context) {
                OutOnlyLotLineContext.ADJUSTMENT -> "Adjustment qty must be > 0"
                OutOnlyLotLineContext.STOCK_TRANSFER -> "Stock transfer out qty must be > 0"
            }
            throw ResponseStatusException(HttpStatus.BAD_REQUEST, msg)
        }
        val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO
        val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
        val holdQty = inventoryLotLine.holdQty ?: BigDecimal.ZERO
        val availableQty = inQty.subtract(outQty).subtract(holdQty)
        if (qtyBd > availableQty) {
            val prefix = when (context) {
                OutOnlyLotLineContext.ADJUSTMENT -> "Adjustment qty exceeds availableQty"
                OutOnlyLotLineContext.STOCK_TRANSFER -> "Stock transfer qty exceeds availableQty"
            }
            throw ResponseStatusException(
                HttpStatus.BAD_REQUEST,
                "$prefix (availableQty=$availableQty, requested=$qtyBd). " +
                    "Rule: requested <= inQty - outQty - holdQty."
            )
        }
    }

    /** Applies validated out-only delta: increases [outQty] only, [holdQty] unchanged. */
    private fun applyOutOnlyIncreaseToLotLine(inventoryLotLine: InventoryLotLine, qtyBd: BigDecimal) {
        val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
        inventoryLotLine.apply {
            this.outQty = outQty.add(qtyBd)
            this.status = inventoryLotLineService.deriveInventoryLotLineStatus(
                this.status,
                this.inQty,
                this.outQty,
                this.holdQty
            )
        }
    }

    /**
     * After the lot line has been saved: create [StockOut], [StockOutLine], and stock ledger.
     * Shared by pick/QR [createStockOut], [createStockOutForAdjustment], and [createStockOutForStockTransfer].
     * Not used by [createStockOutForRemoval] (different line semantics).
     */
    private fun completeStockOutAfterLotLineSave(
        updatedInventoryLotLine: InventoryLotLine,
        stockOutType: String,
        qty: Double,
    ): StockOutLine {
        val itemId = updatedInventoryLotLine.inventoryLot?.item?.id
            ?: throw IllegalArgumentException("InventoryLotLine must have an associated item")

        val currentUser = SecurityUtils.getUser().orElseThrow()
        val stockOut = StockOut().apply {
            this.type = stockOutType
            this.completeDate = LocalDateTime.now()
            this.handler = currentUser.id
            this.status = StockOutStatus.COMPLETE.status
        }
        val savedStockOut = stockOutRepository.save(stockOut)

        val item = itemRepository.findById(itemId).orElseThrow()
        val stockOutLine = StockOutLine().apply {
            this.item = item
            this.qty = qty
            this.stockOut = savedStockOut
            this.inventoryLotLine = updatedInventoryLotLine
            this.status = StockOutLineStatus.COMPLETE.status
            this.pickTime = LocalDateTime.now()
            this.handledBy = currentUser.id
            this.type = stockOutType
        }
        val savedStockOutLine = saveAndFlush(stockOutLine)
        createStockLedgerForStockOut(savedStockOutLine)
        return savedStockOutLine
    }

    /**
     * Pick / QR scan outbound: increases [outQty] and releases [holdQty] up to qty (same behaviour as before TRF was split out).
     * For adjustment or stock-transfer source outbound, use [createStockOutForAdjustment] or [createStockOutForStockTransfer].
     */
    open fun createStockOut(request: StockOutRequest): StockOutLine {
        val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()
        val qtyBd = BigDecimal.valueOf(request.qty)
        val oldHold = inventoryLotLine.holdQty ?: BigDecimal.ZERO

        // Increase outQty and release hold (pick / QR); inventory aggregates via trigger inventory_lot_line_AFTER_UPDATE
        val updatedInventoryLotLine = inventoryLotLine.apply {
            val currentOutQty = this.outQty ?: BigDecimal.ZERO
            this.outQty = currentOutQty + qtyBd
            this.holdQty = oldHold.subtract(qtyBd).coerceAtLeast(BigDecimal.ZERO)
            this.status = inventoryLotLineService.deriveInventoryLotLineStatus(
                this.status,
                this.inQty,
                this.outQty,
                this.holdQty
            )
        }
        inventoryLotLineRepository.save(updatedInventoryLotLine)
        // Do not manually adjust inventory.onHoldQty here — trigger syncs from lot line changes.

        return completeStockOutAfterLotLineSave(updatedInventoryLotLine, request.type, request.qty)
    }

    /**
     * Stock transfer (TRF) source outbound: [outQty] only, [holdQty] unchanged, qty capped by available on the line.
     * Independent of [createStockOut] (pick/QR); shares out-only validation/apply with [createStockOutForAdjustment].
     */
    open fun createStockOutForStockTransfer(inventoryLotLineId: Long, qty: Double): StockOutLine {
        val inventoryLotLine = inventoryLotLineRepository.findById(inventoryLotLineId).orElseThrow()
        val qtyBd = BigDecimal.valueOf(qty)
        validateOutOnlyIncrease(inventoryLotLine, qtyBd, OutOnlyLotLineContext.STOCK_TRANSFER)
        applyOutOnlyIncreaseToLotLine(inventoryLotLine, qtyBd)
        inventoryLotLineRepository.save(inventoryLotLine)
        // inventory aggregates: DB trigger inventory_lot_line_AFTER_UPDATE

        return completeStockOutAfterLotLineSave(inventoryLotLine, "TRF", qty)
    }

    /**
     * Stock adjustment outbound: only increases [outQty]; must not change [holdQty].
     * Rule: qty <= inQty - outQty - holdQty (avoids invalid on-hand vs on-hold and trigger inconsistencies).
     */
    open fun createStockOutForAdjustment(request: StockOutRequest): StockOutLine {
        val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()
        val qtyBd = BigDecimal.valueOf(request.qty)
        validateOutOnlyIncrease(inventoryLotLine, qtyBd, OutOnlyLotLineContext.ADJUSTMENT)
        applyOutOnlyIncreaseToLotLine(inventoryLotLine, qtyBd)
        inventoryLotLineRepository.save(inventoryLotLine)
        // inventory aggregates: DB trigger inventory_lot_line_AFTER_UPDATE

        return completeStockOutAfterLotLineSave(inventoryLotLine, request.type, request.qty)
    }

    /**
     * Removal flow (stock adjustment "remove lot line"):
     * - Physically reduce on-hand to zero by setting outQty = inQty
     * - Clear holdQty = 0 to avoid invalid state (onHand < onHold) and negative available calculations
     *
     * A stock-out document/ledger is still created for audit trail.
     */
    open fun createStockOutForRemoval(inventoryLotLineId: Long, type: String = "ADJ"): StockOutLine {
        val inventoryLotLine = inventoryLotLineRepository.findById(inventoryLotLineId).orElseThrow()
        val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO
        val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO
        val deltaOut = inQty.subtract(outQty).coerceAtLeast(BigDecimal.ZERO)

        // Step 1: Set outQty=inQty and clear holdQty
        val updatedInventoryLotLine = inventoryLotLine.apply {
            this.outQty = inQty
            this.holdQty = BigDecimal.ZERO
            this.status = inventoryLotLineService.deriveInventoryLotLineStatus(
                this.status,
                this.inQty,
                this.outQty,
                this.holdQty
            )
        }
        inventoryLotLineRepository.save(updatedInventoryLotLine)
        // inventory aggregates: handled by DB trigger `inventory_lot_line_AFTER_UPDATE`

        val itemId = updatedInventoryLotLine.inventoryLot?.item?.id
            ?: throw IllegalArgumentException("InventoryLotLine must have an associated item")

        // Step 2: Create stock_out header
        val currentUser = SecurityUtils.getUser().orElseThrow()
        val stockOut = StockOut().apply {
            this.type = type
            this.completeDate = LocalDateTime.now()
            this.handler = currentUser.id
            this.status = StockOutStatus.COMPLETE.status
        }
        val savedStockOut = stockOutRepository.save(stockOut)

        // Step 3: Create stock_out_line (qty = physical delta)
        val item = itemRepository.findById(itemId).orElseThrow()
        val stockOutLine = StockOutLine().apply {
            this.item = item
            this.qty = deltaOut.toDouble()
            this.stockOut = savedStockOut
            this.inventoryLotLine = updatedInventoryLotLine
            this.status = StockOutLineStatus.COMPLETE.status
            this.pickTime = LocalDateTime.now()
            this.handledBy = currentUser.id
            this.type = type
        }
        val savedStockOutLine = saveAndFlush(stockOutLine)

        // Step 4: ledger
        createStockLedgerForStockOut(savedStockOutLine)
        return savedStockOutLine
    }


    private fun createStockLedgerForPickDelta(
        stockOutLine: StockOutLine,
        deltaQty: BigDecimal,
        onHandQtyBeforeUpdate: Double? = null,
        traceTag: String? = null,
        flushAfterSave: Boolean = true
    ) {
        val solId = stockOutLine.id
        val tracePrefix = traceTag?.let { "[$it] " } ?: "[SOL=$solId] "
        if (deltaQty <= BigDecimal.ZERO) {
            if (flushAfterSave) {
                println("${tracePrefix}Skip ledger creation: deltaQty <= 0 (deltaQty=$deltaQty)")
            }
            return
        }

        val item = stockOutLine.item
        if (item == null) {
            if (flushAfterSave) {
                println("${tracePrefix}Skip ledger creation: stockOutLine.item is null")
            }
            return
        }

        val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!)
        if (inventory == null) {
            if (flushAfterSave) {
                println("${tracePrefix}Skip ledger creation: inventory not found by itemId=${item.id}")
            }
            return
        }

        val previousBalance = resolvePreviousBalance(
            itemId = item.id!!,
            inventory = inventory,
            onHandQtyBeforeUpdate = onHandQtyBeforeUpdate
        )

        val newBalance = previousBalance - deltaQty.toDouble()

        if (flushAfterSave) {
            println("${tracePrefix}Creating ledger: previousBalance=$previousBalance, deltaQty=$deltaQty, newBalance=$newBalance, inventoryId=${inventory.id}")
            if (onHandQtyBeforeUpdate != null) {
                println("${tracePrefix}Using onHandQtyBeforeUpdate: $onHandQtyBeforeUpdate")
            }
        }

        val ledger = StockLedger().apply {
            this.stockOutLine = stockOutLine
            this.inventory = inventory
            this.inQty = null
            this.outQty = deltaQty.toDouble()
            this.balance = newBalance
            this.type = "NOR"
            this.itemId = item.id
            this.itemCode = item.code
            this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
                ?: inventory.uom?.id
            this.date = LocalDate.now()
        }

        if (flushAfterSave) {
            stockLedgerRepository.saveAndFlush(ledger)
            //println("${tracePrefix}Ledger created successfully for stockOutLineId=$solId")
        } else {
            stockLedgerRepository.save(ledger)
        }
    }

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

    @Transactional(rollbackFor = [Exception::class])
open fun batchScan(request: com.ffii.fpsms.modules.stock.web.model.BatchScanRequest): MessageResponse {
    val startTime = System.currentTimeMillis()
    println("=== BATCH SCAN START ===")
    println("Start time: ${java.time.LocalDateTime.now()}")
    println("Request lines count: ${request.lines.size}")

    if (request.lines.isEmpty()) {
        return MessageResponse(
            id = null,
            name = "No lines",
            code = "EMPTY",
            type = "batch_scan",
            message = "No lines to scan",
            errorPosition = null
        )
    }

    val errors = mutableListOf<String>()
    val processedIds = mutableListOf<Long>()
    val createdIds = mutableListOf<Long>()

    try {
        // 1) Bulk load all pick order lines
        val pickOrderLineIds = request.lines.map { it.pickOrderLineId }.distinct()
        //println("Loading ${pickOrderLineIds.size} pick order lines...")
        val pickOrderLines = pickOrderLineRepository.findAllById(pickOrderLineIds).associateBy { it.id }

        // 2) Bulk load all inventory lot lines (if any)
        val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId }.distinct()
       // println("Loading ${lotLineIds.size} inventory lot lines...")
        val inventoryLotLines = if (lotLineIds.isNotEmpty()) {
            inventoryLotLineRepository.findAllById(lotLineIds).associateBy { it.id }
        } else {
            emptyMap()
        }

        val itemIds = request.lines.map { it.itemId }.distinct()
        println("Loading ${itemIds.size} items...")
        val items = itemRepository.findAllById(itemIds).associateBy { it.id }

        // ✅ 简化：直接使用 stockOutLineId（如果提供）来获取 StockOut
        // 批量加载所有提供的 stockOutLineId 对应的 StockOutLine 和 StockOut
        val providedStockOutLineIds = request.lines.mapNotNull { it.stockOutLineId }.distinct()
        println("Loading ${providedStockOutLineIds.size} stock out lines by ID...")
        val stockOutLinesById = if (providedStockOutLineIds.isNotEmpty()) {
            stockOutLineRepository.findAllById(providedStockOutLineIds)
                .associateBy { it.id }
        } else {
            emptyMap()
        }
        
        // 从 StockOutLine 中提取 StockOut
        val stockOutsById = stockOutLinesById.values
            .mapNotNull { it.stockOut?.id?.let { id -> id to it.stockOut } }
            .toMap()
        
        println("Loaded ${stockOutsById.size} stock outs from provided stockOutLineIds")

        // ✅ 对于没有 stockOutLineId 的情况，使用 consoCode 或 pickOrderLineId 查找
        val consoCodes = request.lines
            .filter { it.stockOutLineId == null }
            .map { it.pickOrderConsoCode }
            .distinct()
            .filter { it.isNotBlank() }
        
        println("Loading ${consoCodes.size} stock outs by consoCode (for lines without stockOutLineId)...")
        val stockOutsByConsoCode = consoCodes.mapNotNull { consoCode ->
            stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null)?.let { consoCode to it }
        }.toMap()

        // ✅ 对于既没有 stockOutLineId 也没有 consoCode 的情况，通过 pickOrderLineId 获取
        val pickOrderLineIdsNeedingStockOut = request.lines
            .filter { line -> 
                line.stockOutLineId == null && 
                (line.pickOrderConsoCode.isBlank() || !stockOutsByConsoCode.containsKey(line.pickOrderConsoCode))
            }
            .map { it.pickOrderLineId }
            .distinct()
        
        val stockOutsByPickOrderLineId = mutableMapOf<Long, StockOut>()
        if (pickOrderLineIdsNeedingStockOut.isNotEmpty()) {
            println("Batch loading ${pickOrderLineIdsNeedingStockOut.size} stock outs by pickOrderLineId...")
            
            val sql = """
                SELECT pol.id as pickOrderLineId, so.id as stockOutId
                FROM stock_out so
                JOIN pick_order po ON po.consoCode = so.consoPickOrderCode
                JOIN pick_order_line pol ON pol.poId = po.id
                WHERE pol.id IN (:pickOrderLineIds)
            """.trimIndent()
            
            val results = jdbcDao.queryForList(
                sql, 
                mapOf("pickOrderLineIds" to pickOrderLineIdsNeedingStockOut)
            )
            
            results.forEach { row ->
                val pickOrderLineId = row["pickOrderLineId"] as? Long
                val stockOutId = row["stockOutId"] as? Long
                
                if (pickOrderLineId != null && stockOutId != null) {
                    val stockOut = stockOutRepository.findById(stockOutId).orElse(null)
                    if (stockOut != null) {
                        stockOutsByPickOrderLineId[pickOrderLineId] = stockOut
                    }
                }
            }
        }
        
        println("Loaded ${stockOutsById.size} stock outs by ID, ${stockOutsByConsoCode.size} by consoCode, ${stockOutsByPickOrderLineId.size} by pickOrderLineId")

        // 5) Check existing stock out lines for each pick order line and lot
        val existingStockOutLines = mutableMapOf<Pair<Long, Long?>, StockOutLine>()
        request.lines.forEach { line ->
            // ✅ 如果已有 stockOutLineId，直接使用它
            if (line.stockOutLineId != null) {
                val existing = stockOutLinesById[line.stockOutLineId]
                if (existing != null) {
                    existingStockOutLines[Pair(line.pickOrderLineId, line.inventoryLotLineId)] = existing
                }
            } else if (line.inventoryLotLineId != null) {
                // 如果没有 stockOutLineId，通过 pickOrderLineId 和 inventoryLotLineId 查询
                val existing = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
                    line.pickOrderLineId,
                    line.inventoryLotLineId!!
                )
                if (existing.isNotEmpty()) {
                    existingStockOutLines[Pair(line.pickOrderLineId, line.inventoryLotLineId)] = existing.first()
                }
            }
        }
        println("Found ${existingStockOutLines.size} existing stock out lines")

        // 6) Process each line
        request.lines.forEach { line ->
            try {
                println("Processing line: pickOrderLineId=${line.pickOrderLineId}, lotNo=${line.lotNo}, stockOutLineId=${line.stockOutLineId}")

                val pickOrderLine = pickOrderLines[line.pickOrderLineId]
                    ?: throw IllegalStateException("PickOrderLine ${line.pickOrderLineId} not found")

                val item = items[line.itemId]
                    ?: throw IllegalStateException("Item ${line.itemId} not found")

                // ✅ 优先使用 stockOutLineId 获取 StockOut
                val stockOut = when {
                    line.stockOutLineId != null -> {
                        // 从已加载的 stockOutLines 中获取
                        val stockOutLine = stockOutLinesById[line.stockOutLineId]
                        stockOutLine?.stockOut ?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found or has no StockOut")
                    }
                    line.pickOrderConsoCode.isNotBlank() && stockOutsByConsoCode.containsKey(line.pickOrderConsoCode) -> {
                        stockOutsByConsoCode[line.pickOrderConsoCode]!!
                    }
                    else -> {
                        stockOutsByPickOrderLineId[line.pickOrderLineId]
                            ?: throw IllegalStateException("StockOut not found for pickOrderLineId: ${line.pickOrderLineId} (consoCode: '${line.pickOrderConsoCode}', stockOutLineId: ${line.stockOutLineId})")
                    }
                }

                // Check if stock out line already exists
                val existingKey = Pair(line.pickOrderLineId, line.inventoryLotLineId)
                val existingStockOutLine = existingStockOutLines[existingKey]

                if (existingStockOutLine != null) {
                    // Update existing stock out line to 'checked'
                    println("  Updating existing stock out line ${existingStockOutLine.id}")
                    existingStockOutLine.status = StockOutLineStatus.CHECKED.status
                    existingStockOutLine.startTime = LocalDateTime.now()
                    stockOutLineRepository.saveAndFlush(existingStockOutLine)
                    processedIds += existingStockOutLine.id!!
                    println("  ✓ Updated stock out line ${existingStockOutLine.id}")
                } else {
                    // Create new stock out line
                    println("  Creating new stock out line")
                    
                    val inventoryLotLine = line.inventoryLotLineId?.let { inventoryLotLines[it] }
                        ?: throw IllegalStateException("InventoryLotLine ${line.inventoryLotLineId} not found")

                    // Update pick order line status to PICKING
                    pickOrderLine.status = PickOrderLineStatus.PICKING
                    pickOrderLineRepository.saveAndFlush(pickOrderLine)

                    val stockOutLine = StockOutLine().apply {
                        this.item = item
                        this.qty = 0.0
                        this.stockOut = stockOut
                        this.inventoryLotLine = inventoryLotLine
                        this.pickOrderLine = pickOrderLine
                        this.status = StockOutLineStatus.CHECKED.status
                        this.type = "NOR"
                        this.startTime = LocalDateTime.now()
                    }

                    val savedStockOutLine = saveAndFlush(stockOutLine)
                    createStockLedgerForStockOut(savedStockOutLine)
                    createdIds += savedStockOutLine.id!!
                    processedIds += savedStockOutLine.id!!
                    println("  ✓ Created stock out line ${savedStockOutLine.id}")
                }
            } catch (e: Exception) {
                println("  ✗ Error processing line pickOrderLineId=${line.pickOrderLineId}: ${e.message}")
                e.printStackTrace()
                errors += "pickOrderLineId=${line.pickOrderLineId}, lotNo=${line.lotNo}: ${e.message}"
            }
        }

        val msg = if (errors.isEmpty()) {
            "Batch scan success (${processedIds.size} lines processed, ${createdIds.size} created)."
        } else {
            "Batch scan partial success (${processedIds.size} lines processed, ${createdIds.size} created), errors: ${errors.joinToString("; ")}"
        }

        val totalTime = System.currentTimeMillis() - startTime
        println("Processed: ${processedIds.size}/${request.lines.size} items")
        println("Created: ${createdIds.size} new stock out lines")
        println("Total time: ${totalTime}ms (${totalTime / 1000.0}s)")
        println("Average time per item: ${if (processedIds.isNotEmpty()) totalTime / processedIds.size else 0}ms")
        println("End time: ${java.time.LocalDateTime.now()}")
        println("=== BATCH SCAN END ===")

        return MessageResponse(
            id = null,
            name = "batch_scan",
            code = if (errors.isEmpty()) "SUCCESS" else "PARTIAL_SUCCESS",
            type = "batch_scan",
            message = msg,
            errorPosition = null,
            entity = mapOf(
                "processedIds" to processedIds,
                "createdIds" to createdIds,
                "errors" to errors
            )
        )
    } catch (e: Exception) {
        println("=== BATCH SCAN ERROR ===")
        println("Error: ${e.message}")
        e.printStackTrace()
        return MessageResponse(
            id = null,
            name = "batch_scan",
            code = "ERROR",
            type = "batch_scan",
            message = "Error: ${e.message}",
            errorPosition = null,
            entity = mapOf(
                "processedIds" to processedIds,
                "createdIds" to createdIds,
                "errors" to listOf(e.message ?: "Unknown error")
            )
        )
    }
}
@Transactional
fun applyStockOutLineDelta(
    stockOutLine: StockOutLine,
    deltaQty: BigDecimal,
    newStatus: String?,
    typeOverride: String? = null,
    skipInventoryWrite: Boolean = false,
    skipLedgerWrite: Boolean = false,
    operator: String? = null,
    eventTime: LocalDateTime = LocalDateTime.now(),
    skipTryCompletePickOrderLine: Boolean = false,
    deferPersistenceFlush: Boolean = false
): StockOutLine {
    require(deltaQty >= BigDecimal.ZERO) { "deltaQty cannot be negative" }

    val sol = stockOutLine

    // 1) update stock_out_line qty/status/time
    val currentQty = BigDecimal(sol.qty?.toString() ?: "0")
    val newQty = currentQty + deltaQty
    sol.qty = newQty.toDouble()

    if (!newStatus.isNullOrBlank()) {
        sol.status = newStatus
    }
    val statusLower = sol.status?.trim()?.lowercase() ?: ""
    val isPickEnd = statusLower == "completed" || statusLower == "partially_completed"

    if (statusLower == "checked" && sol.startTime == null) {
        sol.startTime = eventTime
    }
    if (statusLower == "completed") {
        if (sol.startTime == null) sol.startTime = eventTime
        sol.endTime = eventTime
    }

    if (!operator.isNullOrBlank()) {
        sol.modifiedBy = operator
    }
    val savedSol =
        if (deferPersistenceFlush) stockOutLineRepository.save(sol)
        else stockOutLineRepository.saveAndFlush(sol)

    // Nothing to post if no delta
    if (deltaQty == BigDecimal.ZERO || !isPickEnd) {
        return savedSol
    }

    val postingType = (typeOverride ?: savedSol.type ?: "").trim().lowercase()
    val isIssuePosting = postingType == "miss" || postingType == "bad" || postingType == "expiry"

    // 2) inventory_lot_line + inventory
    if (!skipInventoryWrite) {
        val lotLine = savedSol.inventoryLotLine
        if (lotLine != null) {
            if (isIssuePosting) {
                val latestLotLine = inventoryLotLineRepository.findById(lotLine.id!!).orElse(null)
                if (latestLotLine != null) {
                    val prevStatus = latestLotLine.status
                    val currentHoldQty = latestLotLine.holdQty ?: BigDecimal.ZERO
                    val currentOutQty = latestLotLine.outQty ?: BigDecimal.ZERO
                    latestLotLine.holdQty = currentHoldQty.subtract(deltaQty).coerceAtLeast(BigDecimal.ZERO)
                    latestLotLine.outQty = currentOutQty.add(deltaQty)
                    latestLotLine.status = inventoryLotLineService.deriveInventoryLotLineStatus(
                        prevStatus,
                        latestLotLine.inQty,
                        latestLotLine.outQty,
                        latestLotLine.holdQty
                    )
                    latestLotLine.modified = eventTime
                    if (!operator.isNullOrBlank()) {
                        latestLotLine.modifiedBy = operator
                    }
                    if (deferPersistenceFlush) inventoryLotLineRepository.save(latestLotLine)
                    else inventoryLotLineRepository.saveAndFlush(latestLotLine)
                }
            } else {
                val lotUpdateResult = inventoryLotLineService.updateInventoryLotLineQuantities(
                    UpdateInventoryLotLineQuantitiesRequest(
                        inventoryLotLineId = lotLine.id!!,
                        qty = deltaQty,
                        operation = "pick"
                    )
                )
                if (lotUpdateResult.code != "SUCCESS") {
                    throw IllegalStateException(
                        "Failed to update inventory lot line ${lotLine.id} with pick operation: ${lotUpdateResult.message}"
                    )
                }
            }
        }

        val itemId = savedSol.item?.id
        if (itemId != null) {
            val inv = itemUomService.findInventoryForItemBaseUom(itemId)
            if (inv != null) {
                val zero = BigDecimal.ZERO
                inv.onHandQty = (inv.onHandQty ?: zero).minus(deltaQty)
                if (!isIssuePosting) {
                    inv.onHoldQty = (inv.onHoldQty ?: zero).minus(deltaQty)
                }
                inventoryRepository.save(inv)
            }
        }
    }

    // 3) stock_ledger (same ledger shape as createStockLedgerForStockOut / completeStockOutAfterLotLineSave)
    if (!skipLedgerWrite) {
        val item = savedSol.item ?: return savedSol
        val inventory = itemUomService.findInventoryForItemBaseUom(item.id!!) ?: return savedSol

        val previousBalance = resolvePreviousBalance(
            itemId = item.id!!,
            inventory = inventory
        )
        val outQty = deltaQty.toDouble()
        val newBalance = previousBalance - outQty

        val ledger = StockLedger().apply {
            this.stockOutLine = savedSol
            this.inventory = inventory
            this.inQty = null
            this.outQty = outQty
            this.balance = newBalance
            this.type = typeOverride ?: savedSol.type ?: "NOR"
            this.itemId = item.id
            this.itemCode = item.code
            this.uomId =
                itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
                    ?: inventory.uom?.id
            this.date = eventTime.toLocalDate()
            if (!operator.isNullOrBlank()) {
                this.createdBy = operator
                this.modifiedBy = operator
            }
        }
        if (deferPersistenceFlush) stockLedgerRepository.save(ledger)
        else stockLedgerRepository.saveAndFlush(ledger)
    }

    // 4) existing side-effects keep same behavior (batch submit defers to end of newBatchSubmit)
    if (!skipTryCompletePickOrderLine && isEndStatus(savedSol.status)) {
        savedSol.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) }
    }

    return savedSol
}
@Transactional(rollbackFor = [Exception::class])
open fun createStockOutBatch(request: BatchStockOutRequest): BatchStockOutResult {
    if (request.lines.isEmpty()) return BatchStockOutResult(null, 0, 0)

    val currentUserId = request.handler ?: SecurityUtils.getUser().orElseThrow().id ?: 0L
    val lotLineIds = request.lines.map { it.inventoryLotLineId }.distinct()

    // 1) 一次 preload lotLine + item + warehouse
    val lotLines = inventoryLotLineRepository
        .findAllByIdInAndDeletedFalseWithRefs(lotLineIds)

    val lotLineMap = lotLines.associateBy { it.id!! }

    // 2) 构造 header（每批一个，避免每笔新建）
    val stockOutHeader = StockOut().apply {
        this.type = request.type
        this.completeDate = LocalDateTime.now()
        this.handler = currentUserId
        this.status = StockOutStatus.COMPLETE.status
    }
    val savedHeader = stockOutRepository.save(stockOutHeader)

    val now = LocalDateTime.now()
    val today = LocalDate.now()

    val lotLinesToUpdate = mutableListOf<InventoryLotLine>()
    val stockOutLinesToInsert = mutableListOf<StockOutLine>()
    val ledgersToInsert = mutableListOf<StockLedger>()
    var skipped = 0

    // 3) 内存校验 + 组装对象（不在循环里 findById / saveAndFlush）
    request.lines.forEach { lineReq ->
        val lotLine = lotLineMap[lineReq.inventoryLotLineId]
        if (lotLine == null) {
            skipped++
            return@forEach
        }

        val qtyBd = BigDecimal.valueOf(lineReq.qty)
        if (qtyBd <= BigDecimal.ZERO) {
            skipped++
            return@forEach
        }

        // 可用量校验（按你现有规则）
        val inQty = lotLine.inQty ?: BigDecimal.ZERO
        val outQty = lotLine.outQty ?: BigDecimal.ZERO
        val holdQty = lotLine.holdQty ?: BigDecimal.ZERO
        val available = inQty.subtract(outQty) // Expiry 通常吃剩余量，可视业务改成 -holdQty
        if (qtyBd > available) {
            skipped++
            return@forEach
        }

        // 更新 lot line
        lotLine.outQty = outQty.add(qtyBd)
        lotLine.holdQty = holdQty.subtract(qtyBd).coerceAtLeast(BigDecimal.ZERO)
        lotLine.status = inventoryLotLineService.deriveInventoryLotLineStatus(
            lotLine.status, lotLine.inQty, lotLine.outQty, lotLine.holdQty
        )
        lotLinesToUpdate += lotLine

        val item = lotLine.inventoryLot?.item ?: run {
            skipped++
            return@forEach
        }

        // 组装 stock_out_line
        val sol = StockOutLine().apply {
            this.item = item
            this.qty = lineReq.qty
            this.stockOut = savedHeader
            this.inventoryLotLine = lotLine
            this.status = StockOutLineStatus.COMPLETE.status
            this.pickTime = now
            this.handledBy = currentUserId
            this.type = request.type
        }
        stockOutLinesToInsert += sol
    }

    // 4) 批量写（关键）
    inventoryLotLineRepository.saveAll(lotLinesToUpdate)
    val savedLines = stockOutLineRepository.saveAll(stockOutLinesToInsert)

    // 5) 批量组装 ledger（避免每笔 createStockLedgerForStockOut）
    // Keep per-item running balance so multiple lines in the same batch
    // chain from the previous ledger balance instead of reusing inventory.onHandQty.
    val runningLedgerBalanceByItemId = mutableMapOf<Long, Double>()
    val inventoryByItemId = mutableMapOf<Long, Inventory>()
    savedLines.sortedBy { it.id ?: Long.MAX_VALUE }.forEach { sol ->
        val item = sol.item ?: return@forEach
        val itemId = item.id ?: return@forEach
        val inv = inventoryByItemId[itemId]
            ?: itemUomService.findInventoryForItemBaseUom(itemId)?.also { inventoryByItemId[itemId] = it }
            ?: return@forEach
        val delta = BigDecimal.valueOf(sol.qty ?: 0.0)
        val prevBalance = runningLedgerBalanceByItemId[itemId]
            ?: run {
                val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemId)
                latestLedger?.balance ?: (inv.onHandQty ?: BigDecimal.ZERO).toDouble()
            }
        val newBalance = prevBalance - delta.toDouble()

        ledgersToInsert += StockLedger().apply {
            this.stockOutLine = sol
            this.inventory = inv
            this.inQty = null
            this.outQty = delta.toDouble()
            this.balance = newBalance
            this.type = request.type
            this.itemId = itemId
            this.itemCode = item.code
            this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(itemId)?.uom?.id
                ?: inv.uom?.id
            this.date = today
        }
        runningLedgerBalanceByItemId[itemId] = newBalance
    }
    stockLedgerRepository.saveAll(ledgersToInsert)

    return BatchStockOutResult(
        stockOutId = savedHeader.id,
        processedCount = savedLines.size,
        skippedCount = skipped
    )
}
}