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

import com.ffii.core.exception.BadRequestException
import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.core.utils.QrCodeUtil
import com.ffii.fpsms.modules.common.CodeGenerator
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.master.entity.QcItemRepository
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository
import com.ffii.fpsms.modules.qc.entity.QcResult
import com.ffii.fpsms.modules.qc.entity.QcResultRepository
import com.ffii.fpsms.modules.stock.entity.*
import com.ffii.fpsms.modules.stock.sql.StockSql.SQL.INVENTORY_COUNT
import net.sf.jasperreports.engine.JasperCompileManager
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Lazy
import org.springframework.core.io.ClassPathResource
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.core.utils.PdfUtils;
import com.ffii.core.utils.ZebraPrinterUtil
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.master.entity.WarehouseRepository
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.service.PrinterService
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository
import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderLineStatus
import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo
import com.ffii.fpsms.modules.stock.web.model.*
import com.ffii.fpsms.modules.stock.enums.EscalationLogStatus
import java.io.FileNotFoundException
import java.time.format.DateTimeFormatter
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import net.sf.jasperreports.engine.JasperExportManager
import net.sf.jasperreports.engine.JasperPrint
import java.io.File
import kotlin.jvm.optionals.getOrNull
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest
import com.ffii.fpsms.modules.stock.service.InventoryLotLineService
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import org.springframework.data.domain.PageRequest
import org.springframework.http.HttpStatus
import org.springframework.web.server.ResponseStatusException
import kotlin.text.toDouble
import com.ffii.fpsms.m18.model.GoodsReceiptNoteRequest
import com.ffii.fpsms.m18.model.GoodsReceiptNoteMainan
import com.ffii.fpsms.m18.model.GoodsReceiptNoteMainanValue
import com.ffii.fpsms.m18.model.GoodsReceiptNoteAnt
import com.ffii.fpsms.m18.model.GoodsReceiptNoteAntValue
import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLog
import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLogRepository
import com.ffii.fpsms.m18.M18GrnRules
import com.ffii.fpsms.m18.service.M18GoodsReceiptNoteService
import com.ffii.fpsms.m18.service.M18PurchaseOrderService
import com.ffii.fpsms.m18.utils.CommonUtils
import com.google.gson.Gson
import org.slf4j.LoggerFactory

@Serializable
data class QrContent(val itemId: Long, val stockInLineId: Long)

@Service
open class StockInLineService(
    private val jdbcDao: JdbcDao,
    private val poRepository: PurchaseOrderRepository,
    private val polRepository: PurchaseOrderLineRepository,
    private val qcItemsRepository: QcItemRepository,
    private val qcResultRepository: QcResultRepository,
    private val escalationLogService: EscalationLogService,
    private val escalationLogRepository: EscalationLogRepository,
    private val stockInService: StockInService,
    private val stockInRepository: StockInRepository,
    private val stockInLineRepository: StockInLineRepository,
    private val jobOrderRepository: JobOrderRepository,
    private val inventoryLotRepository: InventoryLotRepository,
    private val inventoryLotLineRepository: InventoryLotLineRepository,
    private val itemRepository: ItemsRepository,
    private val warehouseRepository: WarehouseRepository,
    private val itemUomRepository: ItemUomRespository,
    private val itemUomService: ItemUomService,
    private val printerService: PrinterService,
    private val stockTakeLineRepository: StockTakeLineRepository,
    private val inventoryLotLineService: InventoryLotLineService,
    private val deliveryOrderRepository: DeliveryOrderRepository,
    private val stockLedgerRepository: StockLedgerRepository,
    private val inventoryRepository: InventoryRepository,
    private val m18GoodsReceiptNoteService: M18GoodsReceiptNoteService,
    private val m18GoodsReceiptNoteLogRepository: M18GoodsReceiptNoteLogRepository,
    /** Lazy to avoid circular dependency: M18PurchaseOrderService → … → StockInLineService. */
    @Lazy private val m18PurchaseOrderService: M18PurchaseOrderService,
    /** When false (default), M18 GRN create API is not called — set true in production only. */
    @Value("\${scheduler.m18Grn.createEnabled:false}") private val m18GrnCreateEnabled: Boolean,
    /** Recent window for PO stock-in alerts (pending / receiving) in nav / list UI. */
    @Value("\${fpsms.purchase-stock-in-alert.lookback-days:7}") private val purchaseStockInAlertLookbackDays: Int,
) : AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) {

    private val logger = LoggerFactory.getLogger(StockInLineService::class.java)

    open fun getStockInLineInfo(stockInLineId: Long): StockInLineInfo {
        // Use Optional-returning repository method to avoid EmptyResultDataAccessException
        return stockInLineRepository.findStockInLineInfoById(stockInLineId).orElseThrow {
            ResponseStatusException(HttpStatus.NOT_FOUND, "stockInLineId $stockInLineId not found")
        }
    }
    open fun getReceivedStockInLineInfo(stockInLineId: Long): StockInLineInfo {
        return stockInLineRepository.findStockInLineInfoByIdAndStatusAndDeletedFalse(id = stockInLineId, status = StockInLineStatus.RECEIVED.status).orElseThrow()
    }

    open fun purchaseStockInAlertSinceDays(overrideDays: Int?): LocalDateTime {
        val d = (overrideDays ?: purchaseStockInAlertLookbackDays).coerceIn(1, 90)
        return LocalDateTime.now().minusDays(d.toLong())
    }

    /**
     * When total put-away (inventory lot line inQty, stock unit) for the POL is already at or above
     * order demand in stock units, do not count / list the line for PO stock-in reminders.
     */
    @Transactional(readOnly = true)
    open fun countPurchaseStockInAlertsSince(since: LocalDateTime): Long {
        val suppressCache = mutableMapOf<Long, Boolean>()
        var total = 0L
        var page = 0
        val pageSize = 100
        while (true) {
            val batch = stockInLineRepository.listPurchaseStockInAlertsSince(since, PageRequest.of(page, pageSize))
            if (batch.isEmpty()) break
            total += batch.count { !polPutAwaySuppressesPoStockInAlert(it.purchaseOrderLineId, suppressCache) }
            page++
            if (batch.size < pageSize) break
        }
        return total
    }

    @Transactional(readOnly = true)
    open fun listPurchaseStockInAlerts(since: LocalDateTime, limit: Int): List<PurchaseStockInAlertRow> {
        val cap = limit.coerceIn(1, 200)
        val out = mutableListOf<PurchaseStockInAlertRow>()
        val suppressCache = mutableMapOf<Long, Boolean>()
        var page = 0
        val pageSize = 50
        while (out.size < cap) {
            val batch = stockInLineRepository.listPurchaseStockInAlertsSince(since, PageRequest.of(page, pageSize))
            if (batch.isEmpty()) break
            for (row in batch) {
                if (!polPutAwaySuppressesPoStockInAlert(row.purchaseOrderLineId, suppressCache)) {
                    out.add(row)
                }
                if (out.size >= cap) break
            }
            page++
            if (batch.size < pageSize) break
        }
        return out
    }

    private fun polPutAwaySuppressesPoStockInAlert(polId: Long, cache: MutableMap<Long, Boolean>): Boolean =
        cache.getOrPut(polId) {
            val pol = polRepository.findById(polId).orElse(null) ?: return@getOrPut false
            val orderStock = polOrderStockQtyForAlert(pol)
            if (orderStock <= BigDecimal.ZERO) return@getOrPut false
            val putAway = totalPutAwayStockQtyForPol(polId)
            putAway.compareTo(orderStock) >= 0
        }

    private fun polOrderStockQtyForAlert(pol: PurchaseOrderLine): BigDecimal {
        val itemId = pol.item?.id ?: return BigDecimal.ZERO
        val qtyM18 = pol.qtyM18
        val uomM18Id = pol.uomM18?.id
        val stockQtyFromM18 =
            if (qtyM18 != null && uomM18Id != null) {
                itemUomService.convertQtyToStockQtyPrecise(itemId, uomM18Id, qtyM18)
            } else {
                BigDecimal.ZERO
            }
        return if (stockQtyFromM18 > BigDecimal.ZERO) {
            stockQtyFromM18
        } else {
            itemUomService.convertPurchaseQtyToStockQtyPrecise(itemId, pol.qty ?: BigDecimal.ZERO)
        }
    }

    private fun totalPutAwayStockQtyForPol(polId: Long): BigDecimal {
        return stockInLineRepository.findStockInLineInfoByPurchaseOrderLineIdAndDeletedFalse(polId)
            .asSequence()
            .flatMap { it.putAwayLines?.asSequence() ?: emptySequence() }
            .fold(BigDecimal.ZERO) { acc, line ->
                acc + (line.stockQty ?: BigDecimal.ZERO).coerceAtLeast(BigDecimal.ZERO)
            }
    }

/*
    open fun assignLotNo(): String {
        val prefix = "LT"
        // ✅ 每次调用都取今天的日期段
        val midfix = LocalDate.now().format(CodeGenerator.DEFAULT_FORMATTER)
    
        // ✅ 用同一个 midfix 去查最新记录
        val latestCode = inventoryLotRepository.findLatestLotNoByPrefix("$prefix-$midfix")
    
        // ✅ 并且把同一个 midfix 传给 CodeGenerator
        return CodeGenerator.generateNo(
            prefix = prefix,
            midfix = midfix,
            latestCode = latestCode
        )
    }
    */
    open fun assignLotNo(): String {
        val prefix = "LT"
        val midfix = LocalDate.now().format(CodeGenerator.DEFAULT_FORMATTER)
        val fullPrefix = "$prefix-$midfix"

        val latestFromInventory =
            inventoryLotRepository.findLatestLotNoByPrefix(fullPrefix)

        val latestFromStockInLine =
            stockInLineRepository.findLatestLotNoByPrefix(fullPrefix)

        val latestCode = listOfNotNull(latestFromInventory, latestFromStockInLine)
            .maxOrNull()

        return CodeGenerator.generateNo(
            prefix = prefix,
            midfix = midfix,
            latestCode = latestCode,
        )
    }

    /**
     * 專門給 JO 用的批號：
     * - 日期部分用 job_order.planStart (若為 null 則退回今天)
     * - 年份使用兩位數（例如 2026-03-18 -> 260318）
     * - 序號仍然與現有規則一致，且跨 inventory_lot / stock_in_line 同步遞增
     */
    open fun assignLotNoForJo(planStart: LocalDate?): String {
        val prefix = "LT"
        val date = planStart ?: LocalDate.now()
        // 兩位數年份 + MMdd，例如 2026-03-18 -> "260318"
        val midfix = date.format(DateTimeFormatter.ofPattern("yyMMdd"))
        val fullPrefix = "$prefix-$midfix"

        val latestFromInventory =
            inventoryLotRepository.findLatestLotNoByPrefix(fullPrefix)

        val latestFromStockInLine =
            stockInLineRepository.findLatestLotNoByPrefix(fullPrefix)

        val latestCode = listOfNotNull(latestFromInventory, latestFromStockInLine)
            .maxOrNull()

        return CodeGenerator.generateNo(
            prefix = prefix,
            midfix = midfix,
            latestCode = latestCode,
        )
    }
    @Throws(IOException::class)
    @Transactional
    open fun create(request: SaveStockInLineRequest): MessageResponse {
        val stockInLine = StockInLine()
        val pol = if (request.purchaseOrderLineId != null)
            request.purchaseOrderLineId?.let { polRepository.findById(it).orElseThrow() }
        else null
        val stl = if (request.stockTakeLineId != null)
            request.stockTakeLineId?.let { stockTakeLineRepository.findById(it).getOrNull() }
        else null
        val jo = if (request.jobOrderId != null)
            request.jobOrderId?.let { jobOrderRepository.findById(it).getOrNull() }
        else null

        var stockIn = if (request.stockInId != null) request.stockInId?.let { stockInRepository.findByIdAndDeletedFalse(it) }
        else if (pol != null) pol.purchaseOrder?.id?.let { stockInRepository.findByPurchaseOrderIdAndDeletedFalse(it) }
        else if (request.stockTakeId != null) request.stockTakeId?.let { stockInRepository.findByStockTakeIdAndDeletedFalse(it) }
        else null

        if (stockIn == null) {
            stockIn = stockInService.create(SaveStockInRequest(purchaseOrderId = pol?.purchaseOrder?.id)).entity as StockIn
//            stockIn = stockInService.create(SaveStockInRequest(purchaseOrderId = request.purchaseOrderId)).entity as StockIn
//        var stockIn = stockInRepository.findByPurchaseOrderIdAndDeletedFalse(request.purchaseOrderId)
        }

        val item = itemRepository.findById(request.itemId).orElseThrow()
        // If request contains valid POL
        if (pol != null) {
            val po = pol.purchaseOrder;
            pol.apply {
                status = PurchaseOrderLineStatus.RECEIVING
            }
            val savedPol = polRepository.saveAndFlush(pol)
            stockInLine.apply {
                this.purchaseOrderLine = savedPol
            }

            // update po status to receiving
            if (po != null) {
                val savedPo = updatePurchaseOrderStatus(po)
                stockInLine.apply {
                    this.purchaseOrder = savedPo
                }
            }
        }

        // If request contains valid stock take id
        if (stl != null) {
            stockInLine.apply {
                this.stockTakeLine = stl
            }
        }

        // If request contains valid job order id
        if (jo != null) {
            stockInLine.apply {
                this.jobOrder = jo
            }
        }

//        val allStockInLine = stockInLineRepository.findAllStockInLineInfoByStockInIdAndDeletedFalse(stockIn.id!!)
//        if (pol.qty!! < request.acceptedQty) {
//            throw BadRequestException()
//        }

        stockInLine.apply {
            this.item = item
            itemNo = item.code
            this.stockIn = stockIn
            // PO-origin:
            // 1) keep user-input qty in acceptedQtyM18 (M18 unit, decimal allowed)
            // 2) calculate stock acceptedQty by converting from M18 unit without round-down
            if (pol != null && item.id != null) {
                acceptedQtyM18 = request.acceptedQty
                val m18UomId = pol.uomM18?.id
                acceptedQty = if (m18UomId != null) {
                    itemUomService.convertQtyToStockQtyPrecise(
                        item.id!!,
                        m18UomId,
                        request.acceptedQty
                    )
                } else {
                    // fallback to legacy: treat request.acceptedQty as purchase unit qty
                    itemUomService.convertPurchaseQtyToStockQtyPrecise(
                        item.id!!,
                        request.acceptedQty
                    )
                }
            } else {
                // Non-PO flows: keep legacy behavior
                acceptedQty = request.acceptedQty
            }
            // Set demandQty based on source
            if (jo != null && jo?.bom != null) {
                // For job orders, demandQty comes from BOM's outputQty
                this.demandQty = jo?.bom?.outputQty

            } else if (pol != null && item.id != null) {
                val m18UomId = pol.uomM18?.id
                val qtyM18 = pol.qtyM18
                this.demandQty = if (m18UomId != null && qtyM18 != null) {
                    itemUomService.convertQtyToStockQtyPrecise(
                        item.id!!,
                        m18UomId,
                        qtyM18
                    )
                } else {
                    // fallback to legacy: treat pol.qty as purchase unit qty
                    pol.qty?.let { polQty ->
                        itemUomService.convertPurchaseQtyToStockQtyPrecise(item.id!!, polQty)
                    }
                }
            }
            dnNo = request.dnNo
            receiptDate = request.receiptDate?.atStartOfDay() ?: LocalDateTime.now()
            productLotNo = request.productLotNo
            this.type = "NOR"
            status = StockInLineStatus.PENDING.status
        }
        if (jo != null) {
            val planStartDate = jo.planStart?.toLocalDate()
            stockInLine.lotNo = assignLotNoForJo(planStartDate)
        }
        val savedSIL = saveAndFlush(stockInLine)
        if (pol != null) {
            //logger.info("[create] Stock-in line created with PO, running PO/GRN update for stockInLine id=${savedSIL.id}")
            //tryUpdatePurchaseOrderAndCreateGrnIfCompleted(savedSIL)
        }
        val lineInfo = stockInLineRepository.findStockInLineInfoByIdAndDeletedFalse(savedSIL.id!!)
        return MessageResponse(
            id = savedSIL.id,
            code = savedSIL.itemNo,
            name = savedSIL.item!!.name,
            type = "stock in line created: status = pending",
            message = "save success",
            errorPosition = null,
            entity = lineInfo
        )
    }

    @Throws(IOException::class)
    @Transactional
    fun saveInventoryLotWhenStockIn(request: SaveStockInLineRequest, stockInLine: StockInLine): InventoryLot {
        val inventoryLot = InventoryLot()
        val args = mapOf(
            "from" to LocalDate.now().atStartOfDay(),
            "to" to LocalDateTime.now(),
            "itemId" to stockInLine.item!!.id
        )
        val inventoryCount = jdbcDao.queryForInt(INVENTORY_COUNT.toString(), args)
//        val newLotNo = CodeGenerator.generateCode(prefix = "MPO", itemId = stockInLine.item!!.id!!, count = inventoryCount)
        val newLotNo = stockInLine.lotNo ?: assignLotNo()
        inventoryLot.apply {
            this.item = stockInLine.item
            this.stockInLine = stockInLine
            stockInDate = LocalDateTime.now()
            expiryDate = request.expiryDate// frontend form input
            lotNo = newLotNo
        }
        val savedInventoryLot = inventoryLotRepository.saveAndFlush(inventoryLot)
        return savedInventoryLot
    }

    @Throws(IOException::class)
    @Transactional
    fun saveInventoryLotLineWhenStockIn(request: SaveStockInLineRequest, stockInLine: StockInLine): List<InventoryLotLine> {
        val response = request.inventoryLotLines?.let { lines ->
            val saveLines = mutableListOf<InventoryLotLine>();
            lines.forEach { line ->
                val warehouse = warehouseRepository.findById(line.warehouseId!!).orElseThrow()
                val stockItemUom = itemUomRepository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(
                    itemId = request.itemId
                )
                val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId)
                // PO-origin: frontend sends qty in stock; non-PO: treat as purchase and convert to stock
                val convertedBaseQty = if (stockInLine.purchaseOrderLine != null || stockInLine.jobOrder != null) {
                    line.qty
                } else if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) {
                    (line.qty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
                } else {
                    (line.qty)
                }
                
                // ✅ Find existing inventory lot line for same warehouse and inventory lot
                val existingLotLine = stockInLine.inventoryLot?.id?.let { inventoryLotId ->
                    inventoryLotLineRepository.findByInventoryLotStockInLineIdAndWarehouseId(
                        inventoryLotStockInLineId = stockInLine.id!!,
                        warehouseId = warehouse.id!!
                    )
                }
                
                if (existingLotLine != null) {
                    // ✅ Update existing inventory lot line (add to existing qty)
                    existingLotLine.apply {
                        this.inQty = (this.inQty ?: BigDecimal.ZERO) + convertedBaseQty
                        this.status = InventoryLotLineStatus.AVAILABLE
                    }
                    saveLines.add(existingLotLine)
                } else {
                    // ✅ Create new inventory lot line (first put away for this warehouse)
                    val inventoryLotLine = InventoryLotLine()
                    inventoryLotLine.apply {
                        this.inventoryLot = stockInLine.inventoryLot
                        this.warehouse = warehouse
                        this.inQty = convertedBaseQty
                        this.status = InventoryLotLineStatus.AVAILABLE
                        this.stockUom = stockItemUom
                    }
                    saveLines.add(inventoryLotLine)
                }
            }
            inventoryLotLineRepository.saveAll(saveLines)
        }
    

        return response ?: emptyList();
//        val inventoryLotLine = InventoryLotLine()
//        val warehouse = warehouseRepository.findById(request.warehouseId!!).orElseThrow()
//        val stockItemUom = itemUomRepository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(
//            itemId = request.itemId
//        )
//        val convertedBaseQty = if (stockItemUom != null) {
//            (request.acceptQty?:request.acceptedQty) * stockItemUom.ratioN!! / stockItemUom.ratioD!!
//        } else {
//            (request.acceptQty?:request.acceptedQty)
//        }
//        inventoryLotLine.apply {
//            this.inventoryLot = stockInLine.inventoryLot
//            this.warehouse = warehouse
//            this.inQty = convertedBaseQty
//            this.status = InventoryLotLineStatus.AVAILABLE
//            this.stockUom = stockItemUom
//        }
//        val savedInventoryLotLine = inventoryLotLineRepository.saveAndFlush(inventoryLotLine)
//        return savedInventoryLotLine
    }

    @Throws(IOException::class)
    @Transactional
    fun saveQcResultWhenStockIn(request: SaveStockInLineRequest, stockInLine: StockInLine, escLogId: Long?= 0): List<QcResult>? {
        if (!request.qcResult.isNullOrEmpty()) {
            val qcResultEntries = request.qcResult!!.map {
                val qcItem = qcItemsRepository.findById(it.qcItemId).orElseThrow()
                val item = itemRepository.findById(stockInLine.item!!.id!!).orElseThrow()
                val qcResult = QcResult();

                QcResult().apply {
                    this.qcItem = qcItem
                    this.item = item
                    this.stockInLine = stockInLine
                    this.failQty = it.failQty
                    this.qcPassed = it.qcPassed
                    this.type = "qc" // default as qc for now
                    this.remarks = it.remarks
                    this.escalationLog = escalationLogService.find(escLogId).getOrNull()
                }
            }
            return qcResultRepository.saveAllAndFlush(qcResultEntries)
        }
        return null
    }

    @Throws(IOException::class)
    @Transactional
    fun saveEscalationLogWhenStockIn(request: SaveStockInLineRequest, stockInLine: StockInLine): EscalationLog? {
        if (request.escalationLog != null) {    // Save Esc log
            val escReq = request.escalationLog;

            var qcTotal = 0;
            var qcFailed = 0;
//            if (request.qcResult!!.isEmpty()) {
            if (request.status == StockInLineStatus.ESCALATED.status) {
                val escLogList = stockInLine.id?.let { escalationLogRepository.findAllByDeletedFalseAndStockInLineIdIn(listOf(it)); }
                val latestLog = escLogList?.maxBy { it.id!! };
                latestLog?.apply {
                    status = if (request.qcAccept == true) EscalationLogStatus.ACCEPTED else EscalationLogStatus.REJECTED
                }
                escalationLogService.save(latestLog);

                val qcRes = qcResultRepository.findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLine.id!!);
                qcTotal = qcRes.size;
                qcFailed = qcRes.count{ !it.qcPassed};
            } else {
                qcTotal = request.qcResult?.size ?: 5;
                qcFailed = request.qcResult?.count { !it.qcPassed } ?: 0;
            }

            escReq?.apply {
                this.stockInLineId = stockInLine.id;
                this.qcTotalCount = qcTotal;
                this.qcFailCount = qcFailed;
            }
            if (escReq != null) {
                val res = escalationLogService.saveEscalationLog(escReq)
                return res.id?.let { escalationLogRepository.findById(it).orElseThrow() };
            };
        } else {    // Update log status
            val escLogList = stockInLine.id?.let { escalationLogRepository.findAllByDeletedFalseAndStockInLineIdIn(listOf(it)); }
            val latestLog = escLogList?.maxBy { it.id!! };
            latestLog?.apply {
                status = if (request.qcAccept == true) EscalationLogStatus.ACCEPTED else EscalationLogStatus.REJECTED
            }
            escalationLogService.save(latestLog);
        }
        return null
    }

    @Throws(IOException::class)
    @Transactional
    open fun updatePurchaseOrderStatus(po : PurchaseOrder) : PurchaseOrder {
        val pols = polRepository.findAllByPurchaseOrderIdAndDeletedIsFalse(po.id!!)

        var newStatus = PurchaseOrderStatus.PENDING
        if (pols.isNotEmpty()) {
            newStatus = if (pols.any { pol-> pol.status != PurchaseOrderLineStatus.COMPLETED }) {
                PurchaseOrderStatus.RECEIVING
            } else {
                PurchaseOrderStatus.COMPLETED
            }
        }
        po.apply {
            status = newStatus
        }
        return poRepository.saveAndFlush(po)
    }

    /**
     * Calls M18 GET `/root/api/read/po?id={m18PoId}` (same as [M18PurchaseOrderService.getPurchaseOrder]) using
     * [PurchaseOrder.m18DataLog].m18Id, compares each local POL's [PurchaseOrderLine.m18Lot] to M18 `pot[].lot`
     * (matched by POL [com.ffii.fpsms.m18.entity.M18DataLog.m18Id] == M18 line id), and persists updates when different.
     * Run before building the GRN so `sourceLot` matches M18.
     */
    private fun syncPurchaseOrderLineM18LotFromM18(po: PurchaseOrder) {
        val m18PoId = po.m18DataLog?.m18Id ?: run {
            logger.debug("[GRN m18Lot sync] PO id=${po.id} code=${po.code} has no m18DataLog.m18Id, skip")
            return
        }
        val response = try {
            m18PurchaseOrderService.getPurchaseOrder(m18PoId)
        } catch (e: Exception) {
            logger.warn("[GRN m18Lot sync] read/po failed m18PoId=$m18PoId PO=${po.code}: ${e.message}")
            null
        } ?: return

        val pot = response.data?.pot ?: return
        if (pot.isEmpty()) return

        val lotByM18LineId = pot.associate { it.id to (it.lot ?: "").trim() }
        val pols = polRepository.findAllByPurchaseOrderIdAndDeletedIsFalseAndM18DataLogIsNotNull(po.id!!)
        var changed = 0
        for (pol in pols) {
            val m18LineId = pol.m18DataLog?.m18Id ?: continue
            if (!lotByM18LineId.containsKey(m18LineId)) continue
            val remoteLot = lotByM18LineId[m18LineId]!!
            val localLot = (pol.m18Lot ?: "").trim()
            if (localLot == remoteLot) continue
            logger.info("[GRN m18Lot sync] PO ${po.code} POL id=${pol.id} m18LineId=$m18LineId: local m18Lot='$localLot' -> M18 lot='$remoteLot'")
            pol.m18Lot = if (remoteLot.isEmpty()) null else remoteLot
            polRepository.saveAndFlush(pol)
            changed++
        }
        if (changed > 0) {
            logger.info("[GRN m18Lot sync] Updated m18Lot on $changed POL row(s) for PO id=${po.id} code=${po.code}")
        }
    }

    /**
     * Builds M18 Goods Receipt Note (AN) request from completed PO and its completed stock-in lines.
     */
    private fun buildGoodsReceiptNoteRequest(po: PurchaseOrder, stockInLines: List<StockInLine>): GoodsReceiptNoteRequest {
        val poCode = po.code ?: ""
        val beId = po.m18BeId?.toInt() ?: 1
        val flowTypeId = when {
            poCode.startsWith("TOA") -> 1
            poCode.startsWith("PF") -> 2
            poCode.startsWith("PP") -> 3
            else -> 1
        }
        val firstLine = stockInLines.firstOrNull()
        // Use DN number as-is (including \"DN00000\"); only trim whitespace
        val doNo = (firstLine?.dnNo ?: "").trim()
        val tDate = firstLine?.receiptDate?.toLocalDate()?.format(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
            ?: LocalDate.now().format(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
        // Group by POL first for udfpartiallyreceived: true = any POL short (accepted < ordered), false = all fully received
        val byPol = stockInLines.groupBy { it.purchaseOrderLine!!.id!! }
        val udfpartiallyreceived = byPol.any { (_, silList) ->
            val pol = silList.first().purchaseOrderLine!!
            val totalAccepted = silList.sumOf { it.acceptedQty ?: BigDecimal.ZERO }
            totalAccepted < (pol.qty ?: BigDecimal.ZERO)
        }
        val mainan = GoodsReceiptNoteMainan(
            values = listOf(
                GoodsReceiptNoteMainanValue(
                    id = null,
                    beId = beId,
                    code = null,
                    venId = (po.supplier?.m18Id ?: 0L).toInt(),
                    curId = (po.currency?.m18Id ?: 0L).toInt(),
                    rate = 1,
                    flowTypeId = flowTypeId,
                    staffId = 329,
                    virDeptId = po.shop?.m18Id?.toInt(),
                    tDate = tDate,
                    udfMTMSDNNO2 = doNo.ifBlank { "" },  // always include for log audit; "" when no DN
                    udfpartiallyreceived = udfpartiallyreceived,
                )
            )
        )
        val sourceId = po.m18DataLog?.m18Id ?: 0L
        val antValues = byPol.map { (_, silList) ->
            val sil = silList.first()
            val pol = sil.purchaseOrderLine!!
            // For PO-origin GRN, M18 ant qty must use the M18 UOM and received M18 qty.
            val totalQtyM18 = silList.sumOf {
                it.acceptedQtyM18 ?: BigDecimal.ZERO
            }
            val unitIdFromDataLog = (pol.m18DataLog?.dataLog?.get("unitId") as? Number)?.toLong()?.toInt()
            val itemName = (sil.item?.name ?: pol.item?.name).orEmpty()  // always non-null for M18 bDesc/bDesc_en
            GoodsReceiptNoteAntValue(
                sourceType = "po",
                sourceId = sourceId,
                sourceLot = pol.m18Lot ?: "",
                proId = (sil.item?.m18Id ?: pol.item?.m18Id ?: 0L).toInt(),
                locId = 155,
                unitId = unitIdFromDataLog ?: (pol.uomM18?.m18Id ?: 0L).toInt(),
                qty = totalQtyM18.toDouble(),
                up = pol.up?.toDouble() ?: 0.0,
                amt = CommonUtils.getAmt(
                    up = pol.up ?: BigDecimal.ZERO,
                    discount = pol.m18Discount ?: BigDecimal.ZERO,
                    qty = totalQtyM18
                ),
                beId = beId,
                flowTypeId = flowTypeId,
                itemDesc = itemName,
                itemDescEn = itemName,
            )
        }
        val ant = GoodsReceiptNoteAnt(values = antValues)
        return GoodsReceiptNoteRequest(mainan = mainan, ant = ant)
    }

    @Throws(IOException::class)
    @Transactional
    fun updatePurchaseOrderLineStatus(pol: PurchaseOrderLine) {
        val stockInLines = stockInLineRepository.findStockInLineInfoByPurchaseOrderLineIdAndDeletedFalse(pol.id!!)
        val qcLines = stockInLines.filter { it.status == StockInLineStatus.PENDING.status
                || it.status == StockInLineStatus.ESCALATED.status }
        val receivingLines = stockInLines.filter { it.status == StockInLineStatus.RECEIVED.status
                || it.status == StockInLineStatus.RECEIVING.status }
        // TODO: also check the qty
        if (stockInLines.isEmpty()) {   // No Stock In Line
            pol.apply { status = PurchaseOrderLineStatus.PENDING }
        } else {
            if (qcLines.isEmpty()) {    // No pending QC lines
                if (receivingLines.isEmpty()) { // No receiving lines
                    pol.apply {
                        status = PurchaseOrderLineStatus.COMPLETED
                    }
                } else {    // Only remain receiving lines
                    pol.apply {
                        status = PurchaseOrderLineStatus.RECEIVING
                    }
                }
            } else {
                // still have pending QC lines
                pol.apply {
                    status = PurchaseOrderLineStatus.RECEIVING
                }
            }
        }
        polRepository.saveAndFlush(pol)
    }

    /**
     * Processes a purchase order for M18 GRN creation. Updates PO/line status and creates GRN if applicable.
     * @param grnReceiptDate When set (scheduler paths), only completed lines on that calendar receipt day are included — separate deliveries get separate GRNs.
     */
    open fun processPurchaseOrderForGrn(stockInLine: StockInLine, grnReceiptDate: LocalDate? = null) {
        tryUpdatePurchaseOrderAndCreateGrnIfCompleted(stockInLine, grnReceiptDate = grnReceiptDate)
    }

    /**
     * Updates purchase order status from its lines and, when PO becomes COMPLETED,
     * creates M18 Goods Receipt Note. Called after saving stock-in line for both
     * RECEIVED and PENDING/ESCALATED status flows.
     */
    private fun tryUpdatePurchaseOrderAndCreateGrnIfCompleted(
        savedStockInLine: StockInLine,
        grnReceiptDate: LocalDate? = null,
    ) {
        if (savedStockInLine.purchaseOrderLine == null) return
        val pol = savedStockInLine.purchaseOrderLine ?: return
        updatePurchaseOrderLineStatus(pol)
        val savedPo = updatePurchaseOrderStatus(pol.purchaseOrder!!)
        logger.info("[updatePurchaseOrderStatus] savedPo id=${savedPo.id}, status=${savedPo.status}")
        // TODO: For test only - normally check savedPo.status == PurchaseOrderStatus.COMPLETED and use only COMPLETE lines
        try {
            // Align POL.m18Lot with M18 before GRN (sourceLot must match M18 PO line lot or AN save may fail).
            syncPurchaseOrderLineM18LotFromM18(savedPo)

            // Completed lines for GRN: either this receipt date only (multi-delivery POs) or all completed on PO (legacy).
            val linesForGrn = if (grnReceiptDate != null) {
                stockInLineRepository.findCompletedByPurchaseOrderIdAndReceiptDateAndDeletedFalseWithItemNames(
                    savedPo.id!!,
                    grnReceiptDate,
                )
            } else {
                stockInLineRepository.findCompletedByPurchaseOrderIdAndDeletedFalseWithItemNames(savedPo.id!!)
            }
            if (linesForGrn.isEmpty()) {
                logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - no stock-in lines for PO id=${savedPo.id} code=${savedPo.code}")
                return
            }
            if (grnReceiptDate != null && linesForGrn.all { sil ->
                    val id = sil.id ?: return@all false
                    m18GoodsReceiptNoteLogRepository.existsByStockInLineIdAndStatusTrue(id)
                }) {
                logger.info(
                    "[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN — all ${linesForGrn.size} line(s) for PO id=${savedPo.id} " +
                        "code=${savedPo.code} on receipt date $grnReceiptDate already have successful GRN logs"
                )
                return
            }
            if (savedPo.m18BeId == null || savedPo.supplier?.m18Id == null || savedPo.currency?.m18Id == null) {
                logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - missing M18 ids for PO id=${savedPo.id} code=${savedPo.code}. m18BeId=${savedPo.m18BeId}, supplier.m18Id=${savedPo.supplier?.m18Id}, currency.m18Id=${savedPo.currency?.m18Id}")
                return
            }
            val m18Created = savedPo.m18CreatedUId
            if (m18Created != null && m18Created in M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS) {
                logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - m18CreatedUId=$m18Created in skip list for PO id=${savedPo.id} code=${savedPo.code}")
                return
            }
            if (!m18GrnCreateEnabled) {
                logger.info(
                    "[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN API — scheduler.m18Grn.createEnabled=false " +
                        "(enable only in production) PO id=${savedPo.id} code=${savedPo.code}"
                )
                return
            }
            //temp comment this TODO will only check purchase order + dnNo duplicated
            //if (m18GoodsReceiptNoteLogRepository.existsByPurchaseOrderIdAndStatusTrue(savedPo.id!!)) {
            //    logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - already created for PO id=${savedPo.id} code=${savedPo.code} (avoids core_201 duplicate)")
            //    return
            //}
            val grnRequest = buildGoodsReceiptNoteRequest(savedPo, linesForGrn)
            val grnRequestJson = Gson().toJson(grnRequest)
            logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] M18 GRN API request (for discussion with M18) PO id=${savedPo.id} code=${savedPo.code}: $grnRequestJson")
            val grnResponse = m18GoodsReceiptNoteService.createGoodsReceiptNote(grnRequest)
            if (grnResponse == null) {
                logger.warn("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: M18 API returned null for PO id=${savedPo.id} code=${savedPo.code}. API call may have failed or timed out.")
                return
            }
            val byPol = linesForGrn.groupBy { it.purchaseOrderLine!!.id!! }
            if (grnResponse.status == true) {
                logger.info("M18 Goods Receipt Note created for PO ${savedPo.code}, goodsReceiptNote id (recordId)=${grnResponse.recordId}")
                // One log row per stock-in line (same POL can have multiple SILs / receipts); retry uses existsByStockInLineIdAndStatusTrue per line.
                byPol.forEach { (_, silList) ->
                    for (sil in silList) {
                        val pol = sil.purchaseOrderLine!!
                        val logEntry = M18GoodsReceiptNoteLog().apply {
                            m18RecordId = grnResponse.recordId
                            stockInLineId = sil.id
                            purchaseOrderLineId = pol.id
                            purchaseOrderId = savedPo.id
                            poCode = savedPo.code
                            status = true
                            message = null
                            requestJson = grnRequestJson
                        }
                        m18GoodsReceiptNoteLogRepository.save(logEntry)
                    }
                }
            } else {
                logger.warn("M18 Goods Receipt Note save returned status=false for PO ${savedPo.code}, recordId=${grnResponse?.recordId}, messages=${grnResponse?.messages}. Request sent (for M18 discussion): $grnRequestJson")
                val msg = grnResponse?.messages?.joinToString { it.msgDetail ?: it.msgCode ?: "" } ?: ""
                byPol.forEach { (_, silList) ->
                    for (sil in silList) {
                        val pol = sil.purchaseOrderLine!!
                        val logEntry = M18GoodsReceiptNoteLog().apply {
                            m18RecordId = grnResponse?.recordId ?: 0L
                            stockInLineId = sil.id
                            purchaseOrderLineId = pol.id
                            purchaseOrderId = savedPo.id
                            poCode = savedPo.code
                            status = false
                            message = msg.take(1000)
                            requestJson = grnRequestJson
                        }
                        m18GoodsReceiptNoteLogRepository.save(logEntry)
                    }
                }
            }
        } catch (e: Exception) {
            logger.error("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: M18 API call failed for PO id=${pol.purchaseOrder?.id} code=${pol.purchaseOrder?.code}: ${e.message}", e)
        }
    }

    @Throws(IOException::class)
    @Transactional
    open fun update(request: SaveStockInLineRequest): MessageResponse {
        val _t0 = System.nanoTime()
        fun _msSince(t: Long): Double = (System.nanoTime() - t) / 1_000_000.0
        fun _logStep(name: String, t: Long) {
            logger.info("[SIL.update timing] $name took ${String.format("%.3f", _msSince(t))} ms")
        }

        val _tFind = System.nanoTime()
        val stockInLine = if (request.id != null) stockInLineRepository.findById(request.id!!).orElseThrow()
        else return MessageResponse(
            id = null,
            code = null,
            name = null,
            type = "Found Null",
            message = "stock in line id is null",
            errorPosition = null,
        )
        _logStep("stockInLineRepository.findById", _tFind)
//        val allStockInLine = stockInLineRepository.findAllStockInLineInfoByStockInIdAndDeletedFalse(stockInLine!!.stockIn!!.id!!)

        if (stockInLine.expiryDate != null && request.expiryDate == null) {
            request.apply {
                expiryDate = stockInLine.expiryDate
            }
        }
        // TODO: check all status to prevent reverting progress due to multiple users access to the same po?
        // return list of stock in line, update data grid with the list

        val _tApplyFields = System.nanoTime()
        stockInLine.apply {
            this.productionDate = request.productionDate?.atStartOfDay() ?: this.productionDate
            this.productLotNo = request.productLotNo ?: this.productLotNo
            this.dnNo = request.dnNo ?: this.dnNo
//            this.dnDate = request.dnDate?.atStartOfDay() ?: this.dnDate
            // QC and PutAway should never overwrite acceptedQty (received qty).
            if (request.qcAccept != true && request.status != StockInLineStatus.RECEIVED.status) {
                val requestQty = request.acceptQty ?: request.acceptedQty
                this.acceptedQty = if (this.purchaseOrderLine != null && this.item?.id != null && requestQty != null) {
                    itemUomService.convertPurchaseQtyToStockQtyPrecise(this.item!!.id!!, requestQty)
                } else {
                    requestQty ?: this.acceptedQty
                }
            } else if (request.qcAccept == true && this.jobOrder != null) {
                // For Job Order QC, allow updating received qty (acceptedQty) based on QC accept quantity.
                // This enables stocking-in more than demand/previous received qty for JO flows only.
                this.acceptedQty = request.acceptedQty
            } else if (request.qcAccept == true && this.status == StockInLineStatus.ESCALATED.status) {
                // Case: line was escalated (QC decision 3), handler resolves with decision 1 (accept).
                // Use 揀收數量 (acceptQty) for put away instead of keeping original received qty.
                val requestQty = request.acceptQty ?: request.acceptedQty
                if (requestQty != null) {
                    this.acceptedQty = if (this.purchaseOrderLine != null && this.item?.id != null) {
                        itemUomService.convertPurchaseQtyToStockQtyPrecise(this.item!!.id!!, requestQty)
                    } else {
                        requestQty
                    }
                }
            }
            // Set demandQty based on source
            if (this.jobOrder != null && this.jobOrder?.bom != null) {
                // For job orders, demandQty comes from BOM's outputQty
                this.demandQty = this.jobOrder?.bom?.outputQty ?: this.demandQty
            } else if (this.purchaseOrderLine != null && this.item?.id != null) {
                val itemId = this.item!!.id!!
                val pol = this.purchaseOrderLine!!
                val m18UomId = pol.uomM18?.id
                val qtyM18 = pol.qtyM18
                this.demandQty = if (m18UomId != null && qtyM18 != null) {
                    itemUomService.convertQtyToStockQtyPrecise(itemId, m18UomId, qtyM18)
                } else if (pol.qty != null) {
                    // fallback to legacy fields when M18 fields are missing
                    itemUomService.convertPurchaseQtyToStockQtyPrecise(itemId, pol.qty!!)
                } else {
                    this.demandQty
                }
            }
            // Don't overwrite demandQty with acceptQty from QC form
            this.invoiceNo = request.invoiceNo
            this.receiptDate = request.receiptDate?.atStartOfDay() ?: this.receiptDate
//            this.status = request.status
            this.expiryDate = stockInLine.expiryDate ?: request.expiryDate
            this.remarks = stockInLine.remarks ?: request.remarks
        }
        _logStep("apply_fields", _tApplyFields)
        if (request.status == StockInLineStatus.RECEIVED.status) {
            val _tPutAwayTotal = System.nanoTime()
            // Putaway
            var savedInventoryLotLine: InventoryLotLine? = null

//            savedInventoryLotLine = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine)
            val _tSaveLotLines = System.nanoTime()
            val savedInventoryLotLines = saveInventoryLotLineWhenStockIn(request = request, stockInLine = stockInLine)
            _logStep("saveInventoryLotLineWhenStockIn", _tSaveLotLines)

            val _tFindAllLotLines = System.nanoTime()
            val inventoryLotLines = stockInLine.inventoryLot?.let { it.id?.let { _id -> inventoryLotLineRepository.findAllByInventoryLotId(_id) } } ?: listOf()
            _logStep("inventoryLotLineRepository.findAllByInventoryLotId", _tFindAllLotLines)

            // ✅ 每次上架都寫一筆 stock_ledger（inQty = 本次上架的庫存數量）
            val _tDeltaCompute = System.nanoTime()
            val putAwayDeltaStockQty = (request.inventoryLotLines ?: listOf()).sumOf { line ->
                val stockItemUom = itemUomRepository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(
                    itemId = request.itemId
                )
                val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId)

                val convertedBaseQty = if (stockInLine.purchaseOrderLine != null || stockInLine.jobOrder != null) {
                    // PO and Job Order: qty is already stock qty
                    line.qty
                } else if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) {
                    // Legacy: treat as purchase qty, convert to stock qty
                    (line.qty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
                } else {
                    line.qty
                }
                convertedBaseQty
            }
            _logStep("compute_putAwayDeltaStockQty(+uom lookups per line)", _tDeltaCompute)
            if (putAwayDeltaStockQty > BigDecimal.ZERO) {
                val _tLedger = System.nanoTime()
                createStockLedgerForStockIn(stockInLine, putAwayDeltaStockQty.toDouble())
                _logStep("createStockLedgerForStockIn(includes findLatestByItemId)", _tLedger)
            }

            val _tQtyCalc = System.nanoTime()
            val putAwayStockQty = inventoryLotLines.sumOf { it.inQty ?: BigDecimal.ZERO }

            // PO-origin: acceptedQty is already STOCK qty; Non-PO: keep legacy rule (acceptQty is purchase qty)
            val requiredStockQty = if (stockInLine.purchaseOrderLine != null) {
                stockInLine.acceptedQty ?: BigDecimal.ZERO
            } else {
                val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(request.itemId)
                val stockItemUom = itemUomRepository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(request.itemId)
                val ratio = if (request.stockTakeLineId == null && stockItemUom != null && purchaseItemUom != null) {
                    (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
                } else {
                    BigDecimal.ONE
                }
                (request.acceptQty ?: request.acceptedQty)?.times(ratio) ?: BigDecimal.ZERO
            }
            _logStep("compute_putAwayStockQty_and_requiredStockQty(+uom lookups)", _tQtyCalc)

            if (putAwayStockQty >= requiredStockQty) {
                val _tStatus = System.nanoTime()
                stockInLine.apply {
                    val isWipJobOrder = stockInLine.jobOrder?.bom?.description == "WIP"
                    this.status = if (isWipJobOrder) {
                        StockInLineStatus.COMPLETE.status
                    } else {
                        // For non-WIP, use original logic
                        if (putAwayStockQty.compareTo(requiredStockQty) == 0)
                            StockInLineStatus.COMPLETE.status
                        else
                            StockInLineStatus.PARTIALLY_COMPLETE.status
                    }
                    //                this.inventoryLotLine = savedInventoryLotLine
                }
                // Update JO Status
                if (stockInLine.jobOrder != null) { //TODO Improve
                    val jo = stockInLine.jobOrder
                    jo?.apply {
                        status = JobOrderStatus.COMPLETED
                    }
                    jobOrderRepository.save(jo!!)
                }
                _logStep("update_status_and_jobOrder_save", _tStatus)
            }
            _logStep("putaway_total", _tPutAwayTotal)
        } else if (request.status == StockInLineStatus.PENDING.status || request.status == StockInLineStatus.ESCALATED.status) {
            var escLogId : Long? = 0L;
            // Escalation
            if (request.status == StockInLineStatus.ESCALATED.status) {
                saveEscalationLogWhenStockIn(request, stockInLine)?.id;
            }
            // QC
            if (request.qcAccept == true) {
                // Accepted
                if (request.acceptedQty > BigDecimal(0)) {
                    var savedInventoryLot: InventoryLot? = null

//                    if (request.status == StockInLineStatus.RECEIVED.status) {
                    savedInventoryLot = saveInventoryLotWhenStockIn(request = request, stockInLine = stockInLine)
//                    }
                    //            println("checkpoint1")
                    stockInLine.apply {
                        this.status = StockInLineStatus.RECEIVED.status;
                        this.inventoryLot = stockInLine.inventoryLot ?: savedInventoryLot
                        this.lotNo = stockInLine.lotNo ?: savedInventoryLot?.lotNo
                    }
                    if (stockInLine.jobOrder != null) {
                        val jo = stockInLine.jobOrder
                        jo?.apply {
                            status = JobOrderStatus.STORING
                        }
                        jobOrderRepository.save(jo!!)
                    }
                } else {
                    return MessageResponse(
                        id = null,
                        code = null,
                        name = null,
                        type = "acceptedQty == 0",
                        message = "acceptedQty cannot be 0",
                        errorPosition = "request.acceptedQty",
                    )
                }
            } else if (request.qcAccept == false) {
                if (request.escalationLog != null) {
                    // Escalated
                    stockInLine.apply {
                        this.status = StockInLineStatus.ESCALATED.status
                    }
                    escLogId = saveEscalationLogWhenStockIn(request, stockInLine)?.id;
                } else {
                    // Rejected
                    stockInLine.apply {
                        this.status = StockInLineStatus.REJECT.status
                    }
                }
            }

            saveQcResultWhenStockIn(request, stockInLine, escLogId)
        }
        val _tSaveFlush = System.nanoTime()
        val savedStockInLine = saveAndFlush(stockInLine)
        _logStep("saveAndFlush(stockInLine)", _tSaveFlush)
        // check if all line completed
        if (savedStockInLine.purchaseOrderLine != null) {
            val pol = savedStockInLine.purchaseOrderLine
            if (pol != null) {
                val _tPol = System.nanoTime()
                updatePurchaseOrderLineStatus(pol)
                _logStep("updatePurchaseOrderLineStatus", _tPol)
                val _tPo = System.nanoTime()
                updatePurchaseOrderStatus(pol.purchaseOrder!!)
                _logStep("updatePurchaseOrderStatus", _tPo)
            }
        }

        val _tLineInfo = System.nanoTime()
        val lineInfo = stockInLineRepository.findStockInLineInfoByIdAndDeletedFalse(savedStockInLine.id!!)
        _logStep("findStockInLineInfoByIdAndDeletedFalse", _tLineInfo)

        logger.info("[SIL.update timing] TOTAL took ${String.format("%.3f", (System.nanoTime() - _t0) / 1_000_000.0)} ms")

        return MessageResponse(
            id = savedStockInLine.id,
            code = null,
            name = null,
            type = "update Success",
            message = "stock in line update status success",
            errorPosition = null,
            entity = listOf(lineInfo)
        )

    }

    @Throws(IOException::class)
    @Transactional
    open fun exportStockInLineQrcode(request: ExportQrCodeRequest): Map<String, Any> {
        val QRCODE_PDF = "qrCodeLabel/poItemPDF.jrxml"
        val resource = ClassPathResource(QRCODE_PDF)
        if (!resource.exists()) {
            println("here")
            throw FileNotFoundException("Report file not found: $QRCODE_PDF")
        }
        val inputStream = resource.inputStream
        val poLabel = JasperCompileManager.compileReport(inputStream)
        val qrCodeInfo = stockInLineRepository.findStockInLineInfoByIdInAndDeletedFalse(request.stockInLineIds).toMutableList()
        val fields = mutableListOf<MutableMap<String ,Any>>()

        for (info in qrCodeInfo) {
            val field = mutableMapOf<String, Any>()
            val qrContent = QrContent(info.itemId, info.id)
            val qrCodeContent = (Json.encodeToString(qrContent))

            val stockItemUom = itemUomRepository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(
                info.itemId
            )
            val purchaseItemUom = itemUomRepository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(info.itemId)
            // PO-origin: acceptedQty is already stock qty; non-PO: convert purchase -> stock for display
            val acceptedQty = if (info.purchaseOrderLineId != null) {
                info.acceptedQty
            } else if (stockItemUom != null && purchaseItemUom != null) {
                (info.acceptedQty) * (purchaseItemUom.ratioN!! / purchaseItemUom.ratioD!!) / (stockItemUom.ratioN!! / stockItemUom.ratioD!!)
            } else {
                (info.acceptedQty)
            }
//            field["itemId"] = info.itemId
            field["itemName"] = info.itemName ?: "N/A"
            field["itemNo"] = info.itemNo
            field["poCode"] = info.poCode ?: "N/A"
            field["itemType"] = info.itemType ?: "N/A"
            field["acceptedQty"] = "%.2f".format(acceptedQty)
            field["uom"] = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A"
            field["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
            field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
            field["lotNo"] = info.lotNo ?: "N/A"
            field["supplier"] = info.supplier ?: "N/A"
            val image = QrCodeUtil.generateQRCodeImage(qrCodeContent)
            field["qrCode"] = image
            fields.add(field)
        }
        val params: MutableMap<String, Any> = mutableMapOf(
            "poCode" to (qrCodeInfo[0].poCode ?: "N/A"), //TODO change to JO code for JO
            "isTransfer" to ""
        )
        return mapOf(
            "report" to PdfUtils.fillReport(poLabel,fields, params),
            "fileName" to (qrCodeInfo[0].poCode ?: qrCodeInfo[0].joCode ?: qrCodeInfo[0].lotNo).toString()
        );
    }

    @Transactional
    open fun printQrCode(request: PrintQrCodeForSilRequest) {
        val printer = printerService.findById(request.printerId) ?: throw NoSuchElementException("No such printer");

        val pdf = exportStockInLineQrcode(
            ExportQrCodeRequest(
                stockInLineIds = listOf(request.stockInLineId)
            )
        )

        val jasperPrint = pdf["report"] as JasperPrint

        // 1. Create a temporary file to save the PDF.
        val tempPdfFile = File.createTempFile("print_job_", ".pdf")

        try {
            // 2. Export the JasperPrint to the temporary PDF file.
            JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath)

            // 3. Call the utility function with the temporary file.
            val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty
            printer.ip?.let { ip -> printer.port?.let { port ->
                ZebraPrinterUtil.printPdfToZebra(
                    tempPdfFile,
                    ip,
                    port,
                    printQty,
                    ZebraPrinterUtil.PrintDirection.ROTATED,
                    printer.dpi
                )
            } }
        } finally {
            // 4. Ensure the temporary file is deleted after the print job is sent.
            tempPdfFile.delete()
        }
    }

    @Transactional
    open fun printQrCodeForDeliveryOrder(request: PrintQrCodeForDoRequest) {
        // Get delivery order by ID
        val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderId)
            ?: throw NoSuchElementException("Delivery order not found with ID: ${request.deliveryOrderId}")

        // Loop through delivery order lines
        deliveryOrder.deliveryOrderLines.forEach { deliveryOrderLine ->
            val itemId = deliveryOrderLine.item?.id
                ?: throw IllegalStateException("Delivery order line ${deliveryOrderLine.id} has no item")

            // Find all stock in lines matching this itemId
            val stockInLines = stockInLineRepository.findAllByItemIdAndDeletedFalse(itemId)

            // Print QR code for each stock in line
            stockInLines.forEach { stockInLine ->
                val printRequest = PrintQrCodeForSilRequest(
                    stockInLineId = stockInLine.id!!,
                    printerId = request.printerId,
                    printQty = request.printQty
                )
                printQrCode(printRequest)
            }
        }
    }
    open fun newStockInCreate(request: NewStockInCreateRequest): MessageResponse {
        val newLotNo = if (request.lotNo == null) assignLotNo() else request.lotNo
        val inventoryLotLine = inventoryLotLineRepository.findByIdAndDeletedIsFalse(
            request.inventoryLotLineId ?: throw IllegalArgumentException("Inventory lot ID not found")
        ) ?: throw IllegalArgumentException("Inventory lot line not found")
        val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse(
            inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found")
        ) ?: throw IllegalArgumentException("Inventory lot not found")

        val stockIn=StockIn().apply {
            this.code=stockTake?.code
            this.status=request.stockIntype
            //this.stockTake=stockTake
        }
        stockInRepository.save(stockIn)
        val stockInLine = StockInLine().apply {
            //this.stockTakeLine=stockTakeLine
            this.item=inventoryLot.item
            this.itemNo=request.itemNo
            this.stockIn = stockIn
            this.demandQty=request.demandQty
            this.acceptedQty=request.acceptedQty
            this.expiryDate=inventoryLot.expiryDate
            this.inventoryLot=inventoryLot
            this.inventoryLotLine=inventoryLotLine
            this.lotNo=newLotNo
            this.status = "completed"
            this.type = request.stockInLineType


        }
        stockInLineRepository.save(stockInLine)
        val updateRequest = SaveInventoryLotLineRequest(
            id = inventoryLotLine.id,
            inventoryLotId = inventoryLotLine.inventoryLot?.id,
            warehouseId = inventoryLotLine.warehouse?.id,
            stockUomId = inventoryLotLine.stockUom?.id,
            inQty = request.acceptedQty,
            outQty = inventoryLotLine.outQty,
            holdQty = inventoryLotLine.holdQty,
            status = inventoryLotLine.status?.value,
            remarks = inventoryLotLine.remarks
        )
        inventoryLotLineService.saveInventoryLotLine(updateRequest)
        return MessageResponse(
            id = stockInLine.id,
            code = null,
            name = stockInLine.item?.name,
            type = "success",
            message = "Stock in line created successfully",
            errorPosition = null
        )
    }
    @Transactional

    private fun createStockLedgerForStockIn(stockInLine: StockInLine, inQty: Double) {
        val _t0 = System.nanoTime()
        fun _msSince(t: Long): Double = (System.nanoTime() - t) / 1_000_000.0
        fun _logStep(name: String, t: Long) {
            logger.info("[SIL.ledger timing] $name took ${String.format("%.3f", _msSince(t))} ms")
        }

        val item = stockInLine.item ?: return

        val _tInv = System.nanoTime()
        val inventory = inventoryRepository.findFirstByItemIdAndDeletedIsFalseOrderByIdAsc(item.id!!) ?: return
        _logStep("inventoryRepository.findFirstByItemIdAndDeletedIsFalseOrderByIdAsc", _tInv)

        // ✅ 修复：查询最新的 stock_ledger 记录，基于前一笔 balance 计算
        val _tLatest = System.nanoTime()
        val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(item.id!!)
        _logStep("stockLedgerRepository.findLatestByItemId", _tLatest)

        // ✅ 修复：如果 latestLedger 为 null（第一次 stock in），previousBalance 应该是 0
        // 因为 inventory.onHandQty 已经被触发器更新为包含本次 stock in 的数量
        val previousBalance = latestLedger?.balance ?: 0.0

        val newBalance = previousBalance + inQty

        val _tUom = System.nanoTime()
        val stockUomId = itemUomService.findStockUnitByItemId(item.id!!)?.uom?.id ?: inventory.uom?.id
        _logStep("itemUomService.findStockUnitByItemId", _tUom)

        val stockLedger = StockLedger().apply {
            this.stockInLine = stockInLine
            this.inventory = inventory
            this.inQty = inQty
            this.outQty = null
            this.balance = newBalance
            this.type = stockInLine.type
            this.itemId = item.id
            this.itemCode = item.code
            this.uomId = stockUomId
            this.date = LocalDate.now()
        }

        val _tSave = System.nanoTime()
        stockLedgerRepository.saveAndFlush(stockLedger)
        _logStep("stockLedgerRepository.saveAndFlush", _tSave)

        logger.info("[SIL.ledger timing] TOTAL took ${String.format("%.3f", (System.nanoTime() - _t0) / 1_000_000.0)} ms")
}

    @Transactional
    open fun createStockIn(request: StockInRequest): StockInLine {
        // Step 1: Create a row of stock_in
        val prefix = "SI"
        val midfix = CodeGenerator.DEFAULT_MIDFIX
        val latestCode = stockInRepository.findLatestCodeByPrefix("$prefix-$midfix")
        val code = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode)

        val stockIn = StockIn().apply {
            this.code = code
            this.status = StockInStatus.COMPLETE.status
            this.orderDate = LocalDateTime.now()
            this.completeDate = LocalDateTime.now()
        }
        val savedStockIn = stockInRepository.save(stockIn)

        // Step 2: Create a row in stock_in_line table
        val item = itemRepository.findById(request.itemId).orElseThrow()
        val newLotNo = request.lotNo ?: assignLotNo()
        val stockInLine = StockInLine().apply {
            this.item = item
            this.itemNo = request.itemNo
            this.stockIn = savedStockIn
            this.demandQty = request.demandQty
            this.acceptedQty = request.acceptedQty
            this.receiptDate = LocalDateTime.now()
            this.productionDate = LocalDateTime.now()
            this.expiryDate = request.expiryDate
            this.status = StockInLineStatus.COMPLETE.status
            this.lotNo = newLotNo
            this.productLotNo = request.productLotNo
            this.dnNo = request.dnNo
            this.type = request.type
        }
        val savedStockInLine = saveAndFlush(stockInLine)


        // Step 3: Create a row in inventory_lot table
        val lotNo = newLotNo
        val inventoryLot = InventoryLot().apply {
            this.item = item
            this.stockInLine = savedStockInLine
            this.stockInDate = LocalDateTime.now()
            this.expiryDate = request.expiryDate
            this.lotNo = lotNo
        }
        val savedInventoryLot = inventoryLotRepository.saveAndFlush(inventoryLot)

        // Link back to StockInLine
        savedStockInLine.inventoryLot = savedInventoryLot

        // Step 4: Create a row in inventory_lot_line table
        val stockItemUom = itemUomRepository.findFirstByItemIdAndStockUnitIsTrueAndDeletedIsFalseOrderByIdAsc(
            request.itemId
        ) ?: throw IllegalArgumentException(
            "Stock UOM not found for item: id=${request.itemId}, itemNo=${request.itemNo}. " +
                    "Ensure at least one item_uom row has stockUnit=1, deleted=0 for this item."
        )

        val warehouse = if (request.warehouseId != null) {
            warehouseRepository.findById(request.warehouseId).orElseThrow()
        } else {
            throw IllegalArgumentException("warehouseId is required for creating inventoryLotLine")
        }

        val inventoryLotLine = InventoryLotLine().apply {
            this.inventoryLot = savedInventoryLot
            this.warehouse = warehouse
            this.inQty = request.acceptedQty
            this.outQty = BigDecimal.ZERO
            this.holdQty = BigDecimal.ZERO
            this.status = InventoryLotLineStatus.AVAILABLE
            this.stockUom = stockItemUom
        }
        val savedInventoryLotLine = inventoryLotLineRepository.saveAndFlush(inventoryLotLine)

        // Link back to StockInLine
        savedStockInLine.inventoryLotLine = savedInventoryLotLine

        saveAndFlush(savedStockInLine)

        // Step 5: Create Stock Ledger entry
        createStockLedgerForStockIn(savedStockInLine, request.acceptedQty.toDouble())

        return savedStockInLine
    }

    @Transactional
    open fun createStockInForExistingInventoryLotLine(
        request: StockInRequest,
        existingInventoryLotLine: InventoryLotLine
    ): StockInLine {
        // Step 1: Create StockIn
        val prefix = "SI"
        val midfix = CodeGenerator.DEFAULT_MIDFIX
        val latestCode = stockInRepository.findLatestCodeByPrefix("$prefix-$midfix")
        val code = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode)

        val stockIn = StockIn().apply {
            this.code = code
            this.status = StockInStatus.COMPLETE.status
            this.orderDate = LocalDateTime.now()
            this.completeDate = LocalDateTime.now()
        }
        val savedStockIn = stockInRepository.save(stockIn)

        // Step 2: Increase qty on the matched target lot line instead of creating a new lot line
        val updatedInventoryLotLine = existingInventoryLotLine.apply {
            this.inQty = (this.inQty ?: BigDecimal.ZERO).add(request.acceptedQty)
            this.status = inventoryLotLineService.deriveInventoryLotLineStatus(
                this.status,
                this.inQty,
                this.outQty,
                this.holdQty
            )
        }
        val savedInventoryLotLine = inventoryLotLineRepository.saveAndFlush(updatedInventoryLotLine)

        // Step 3: Create StockInLine and link to existing InventoryLotLine
        val item = itemRepository.findById(request.itemId).orElseThrow()

        val stockInLine = StockInLine().apply {
            this.item = item
            this.itemNo = request.itemNo
            this.stockIn = savedStockIn
            this.demandQty = request.demandQty
            this.acceptedQty = request.acceptedQty
            this.receiptDate = LocalDateTime.now()
            this.productionDate = LocalDateTime.now()
            this.expiryDate = request.expiryDate
            this.status = StockInLineStatus.COMPLETE.status
            this.lotNo = request.lotNo
            this.productLotNo = request.productLotNo
            this.dnNo = request.dnNo
            this.type = request.type
            // Link to existing InventoryLot and InventoryLotLine
            this.inventoryLot = savedInventoryLotLine.inventoryLot
            this.inventoryLotLine = savedInventoryLotLine
        }
        val savedStockInLine = saveAndFlush(stockInLine)

        // Step 4: Create Stock Ledger entry
        createStockLedgerForStockIn(savedStockInLine, request.acceptedQty.toDouble())

        return savedStockInLine
    }
}
