package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.core.utils.PdfUtils
import org.springframework.context.annotation.Lazy
import com.ffii.core.utils.ZebraPrinterUtil
import com.ffii.fpsms.m18.entity.M18DataLogRepository
import com.ffii.fpsms.m18.service.M18DataLogService
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrder
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderInfo
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderResponse
import com.ffii.fpsms.modules.deliveryOrder.web.models.SaveDeliveryOrderStatusRequest
import com.ffii.fpsms.modules.master.service.CurrencyService
import com.ffii.fpsms.modules.master.service.ShopService
import com.ffii.fpsms.modules.user.entity.UserRepository
import com.ffii.fpsms.modules.user.service.UserService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.io.IOException
import kotlin.jvm.optionals.getOrDefault
import kotlin.jvm.optionals.getOrNull
import com.ffii.fpsms.modules.deliveryOrder.web.models.DoDetailResponse
import com.ffii.fpsms.modules.deliveryOrder.web.models.DoDetailLineResponse
import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoRequest
import com.ffii.fpsms.modules.pickOrder.web.models.SavePickOrderRequest
import com.ffii.fpsms.modules.pickOrder.web.models.SavePickOrderLineRequest
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderType
import com.ffii.fpsms.modules.pickOrder.service.PickOrderService
import java.time.LocalDate
import java.math.BigDecimal
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import java.time.LocalDateTime
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrder
import com.ffii.fpsms.modules.deliveryOrder.entity.DoReplenishment
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.ExportDeliveryNoteRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDeliveryNoteRequest
import com.ffii.fpsms.modules.master.service.PrinterService
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.pickOrder.entity.TruckRepository
import java.time.LocalTime
import com.ffii.fpsms.modules.pickOrder.entity.Truck
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.StockOutRepository
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.entity.StockOut
import com.ffii.fpsms.modules.stock.entity.StockOutLine
import com.ffii.fpsms.modules.stock.web.model.StockOutStatus
import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus
import com.ffii.fpsms.modules.stock.web.model.SuggestedPickLotForPolRequest
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import net.sf.jasperreports.engine.JasperCompileManager
import net.sf.jasperreports.engine.JasperExportManager
import net.sf.jasperreports.engine.JasperPrint
import org.springframework.core.io.ClassPathResource
import java.io.File
import java.io.FileNotFoundException
import com.ffii.core.support.JdbcDao;
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.web.models.ExportDNLabelsRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.ExportDNLabelsReprintRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.DnCartonLabelSheetRow
import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDNLabelsRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintDNLabelsReprintRequest
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository
import com.ffii.fpsms.modules.stock.service.InventoryLotService
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService  //  添加这行
import com.ffii.fpsms.modules.deliveryOrder.web.models.*
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository //  添加
import com.ffii.core.utils.CanonPrinterUtil
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import kotlin.collections.emptyMap
import com.ffii.fpsms.modules.master.print.A4PrintDriverRegistry
import com.ffii.core.response.RecordsRes
import net.sf.jasperreports.engine.JasperReport
import org.slf4j.LoggerFactory
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderInfoLite
import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderInfoLiteDto
import com.ffii.fpsms.modules.deliveryOrder.entity.models.DeliveryOrderLineInfo
import com.ffii.fpsms.modules.master.entity.Items
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import java.util.Locale
import org.slf4j.Logger
@Service
open class DeliveryOrderService(
    private val deliveryOrderRepository: DeliveryOrderRepository,
    private val shopService: ShopService,
    private val currencyService: CurrencyService,
    private val m18DataLogService: M18DataLogService,
    private val m18DataLogRepository: M18DataLogRepository,
    private val userService: UserService,
    private val userRepository: UserRepository,
    private val pickOrderService: PickOrderService,
    @Lazy private val doPickOrderService: DoPickOrderService,
    private val truckRepository: TruckRepository,
    private val pickOrderRepository: PickOrderRepository,
    private val suggestedPickLotService: SuggestedPickLotService,
    private val inventoryLotLineRepository: InventoryLotLineRepository,
    private val stockOutRepository: StockOutRepository,
    private val stockOutLineRepository: StockOutLIneRepository,
    private val pickOrderLineRepository: PickOrderLineRepository,
    private val printerService: PrinterService,
    private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
    private val purchaseOrderRepository: PurchaseOrderRepository,
    private val inventoryLotService: InventoryLotService,
    private val suggestedPickLotRepository: SuggestPickLotRepository,
    private val inventoryLotRepository: InventoryLotRepository,
    private val jdbcDao: JdbcDao,
    private val pickExecutionIssueRepository: PickExecutionIssueRepository,
    private val doPickOrderRepository: DoPickOrderRepository,
    private val doPickOrderLineRepository: DoPickOrderLineRepository,
    private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
    private val itemsRepository: ItemsRepository,
    private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
    @Lazy private val doReplenishmentService: DoReplenishmentService,
) {
    /**
     * 樓層篩選：2F／4F／ALL 由 [DoFloorSupplierSettingsService] 讀 `settings`。
     * 車線-X 仍依 DO supplier 所屬樓層出現在對應 tab。
     */
    private fun allowedSupplierCodesForFloor(floor: String?): List<String> =
        doFloorSupplierSettingsService.allowedSupplierCodesForFloor(floor)

    private fun loadDoFloorSupplierLists(): Pair<List<String>, List<String>> =
        doFloorSupplierSettingsService.loadDoFloorSupplierLists()

    private fun preferredStoreFloorForSupplier(
        supplierCode: String?,
        suppliers2F: List<String>,
        suppliers4F: List<String>,
    ): String = doFloorSupplierSettingsService.preferredStoreFloorForSupplier(supplierCode, suppliers2F, suppliers4F)

    private fun mapDeliveryOrderLinesForPickOrderRelease(
        deliveryOrder: DeliveryOrder,
    ): List<SavePickOrderLineRequest> {
        return deliveryOrder.deliveryOrderLines
            .filter { it.deleted != true }
            .filter { it.item?.isFee != true }
            .map {
                SavePickOrderLineRequest(
                    itemId = it.item?.id,
                    qty = it.qty ?: BigDecimal.ZERO,
                    uomId = it.uom?.id,
                )
            }
    }

    open fun searchDoLiteByPage(
    code: String?,
    shopName: String?,
    status: String?,
    estimatedArrivalDate: LocalDateTime?,
    pageNum: Int?,
    pageSize: Int?,
    truckLanceCode: String?,
    floor: String? = null,
    isExtra: Boolean? = null,
): RecordsRes<DeliveryOrderInfoLiteDto> {

        val page = (pageNum ?: 1) - 1
        val size = pageSize ?: 10
        val statusEnum = status?.let { s -> DeliveryOrderStatus.entries.find { it.value == s } }

        val etaStart = estimatedArrivalDate
        val etaEnd = estimatedArrivalDate?.plusDays(1)

        val searchTruckLanceCode = truckLanceCode?.ifBlank { null }?.lowercase()

        if (searchTruckLanceCode != null) {
            println("DEBUG: Filtering by truckLanceCode: $searchTruckLanceCode, floor=$floor")

            val allowedForFloor = allowedSupplierCodesForFloor(floor)
            val allResult = deliveryOrderRepository.searchDoLitePageWithSupplierCodes(
                code = code?.ifBlank { null },
                shopName = shopName?.ifBlank { null },
                status = statusEnum,
                etaStart = etaStart,
                etaEnd = etaEnd,
                isExtra = isExtra,
                allowedSupplierCodes = allowedForFloor,
                pageable = PageRequest.of(0, 100_000),
            )

            println("DEBUG: Total records from DB (supplier+floor filter): ${allResult.totalElements}")

            val deliveryOrderIds = allResult.content.mapNotNull { it.id }
            val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds)
                .associateBy { it.id }

            val preFilteredContent = allResult.content
            val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()

            // ✅ 优化3: 收集所有需要查询的 shopId 和日期组合（只处理预过滤后的记录）
            val shopIdAndDatePairs = preFilteredContent.mapNotNull { info ->
                val deliveryOrder = deliveryOrdersMap[info.id]
                val shopId = deliveryOrder?.shop?.id
                val estimatedArrivalDate = info.estimatedArrivalDate
                if (shopId != null && estimatedArrivalDate != null) {
                    val targetDate = estimatedArrivalDate.toLocalDate()
                    val dayAbbr = getDayOfWeekAbbr(targetDate)
                    val supplierCode = deliveryOrder.supplier?.code
                    val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
                    Triple(shopId, preferredFloor, dayAbbr)
                } else {
                    null
                }
            }.distinct()

            println("DEBUG: Unique shopId/floor/day combinations: ${shopIdAndDatePairs.size}")

            // ✅ 优化4: 批量查询所有需要的 Truck
            val truckCache = mutableMapOf<Triple<Long, String, String>, Truck?>()
            shopIdAndDatePairs.forEach { (shopId, preferredFloor, dayAbbr) ->
                truckCache[Triple(shopId, preferredFloor, dayAbbr)] =
                    resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)
            }

            println("DEBUG: Cached ${truckCache.size} truck lookups")

            // 处理预过滤后的记录：计算 truckLanceCode 并过滤
            val processedRecords = preFilteredContent.map { info ->
                val deliveryOrder = deliveryOrdersMap[info.id]
                val supplierCode = deliveryOrder?.supplier?.code
                val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
                val shop = deliveryOrder?.shop
                val shopId = shop?.id
                val estimatedArrivalDate = info.estimatedArrivalDate

                val calculatedTruckLanceCode =
                    if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) {
                        val targetDate = estimatedArrivalDate.toLocalDate()
                        val dayAbbr = getDayOfWeekAbbr(targetDate)

                        // 从缓存中获取 Truck
                        val matchedTruck = truckCache[Triple(shopId, preferredFloor, dayAbbr)]
                        matchedTruck?.truckLanceCode
                    } else {
                        null
                    }

                DeliveryOrderInfoLiteDto(
                    id = info.id,
                    code = info.code,
                    orderDate = info.orderDate,
                    estimatedArrivalDate = info.estimatedArrivalDate,
                    status = info.status,
                    shopName = info.shopName,
                    supplierName = info.supplierName,
                    shopAddress = info.shopAddress,
                    truckLanceCode = calculatedTruckLanceCode,
                    isExtra = deliveryOrdersMap[info.id]?.isExtra ?: info.isExtra,
                )
            }.filter { dto ->
                val dtoTruckLanceCode = dto.truckLanceCode?.lowercase() ?: ""
                dtoTruckLanceCode.contains(searchTruckLanceCode)
            }

            // 计算准确的总数（过滤后的总数）
            val totalCount = processedRecords.size
            println("DEBUG: Final filtered records count: $totalCount")

            // 对处理后的记录进行分页
            val startIndex = page * size
            val endIndex = minOf(startIndex + size, processedRecords.size)

            val paginatedRecords = if (startIndex < processedRecords.size) {
                processedRecords.subList(startIndex, endIndex)
            } else {
                emptyList()
            }
 
            return RecordsRes(paginatedRecords, totalCount)
        } else {
            // 未提供 truckLanceCode：在 DB 層依允許的供應商分頁，避免先取 10 筆再過濾導致每頁顯示少於 pageSize
            val allowedSupplierCodes = allowedSupplierCodesForFloor(floor)
            val result = deliveryOrderRepository.searchDoLitePageWithSupplierCodes(
                code = code?.ifBlank { null },
                shopName = shopName?.ifBlank { null },
                status = statusEnum,
                etaStart = etaStart,
                etaEnd = etaEnd,
                isExtra = isExtra,
                allowedSupplierCodes = allowedSupplierCodes,
                pageable = PageRequest.of(page.coerceAtLeast(0), size),
            )

            val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
            val records = result.content.map { info ->
                val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(info.id)
                val supplierCode = deliveryOrder?.supplier?.code
                val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
                val shop = deliveryOrder?.shop
                val shopId = shop?.id
                val estimatedArrivalDate = info.estimatedArrivalDate

                val calculatedTruckLanceCode =
                    if (deliveryOrder != null && shopId != null && estimatedArrivalDate != null) {
                        val targetDate = estimatedArrivalDate.toLocalDate()
                        val dayAbbr = getDayOfWeekAbbr(targetDate)
                        resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode
                    } else {
                        null
                    }

                DeliveryOrderInfoLiteDto(
                    id = info.id,
                    code = info.code,
                    orderDate = info.orderDate,
                    estimatedArrivalDate = info.estimatedArrivalDate,
                    status = info.status,
                    shopName = info.shopName,
                    supplierName = info.supplierName,
                    shopAddress = info.shopAddress,
                    truckLanceCode = calculatedTruckLanceCode,
                    isExtra = deliveryOrder?.isExtra ?: info.isExtra,
                )
            }

            return RecordsRes(records, result.totalElements.toInt())
        }


    }

    /**
     * DO 輕量搜索 v2：車線關鍵字正規化（`車線-X`／`x`／`車線-` 前綴含未指派）、
     * 以允許供應商 + 分批掃描取代單次 100000 筆載入；無車線條件時等同 [searchDoLiteByPage] 無車線分支。
     */
    open fun searchDoLiteByPageV2(
        code: String?,
        shopName: String?,
        status: String?,
        estimatedArrivalDate: LocalDateTime?,
        pageNum: Int?,
        pageSize: Int?,
        truckLanceCode: String?,
        floor: String? = null,
        isExtra: Boolean? = null,
    ): RecordsRes<DeliveryOrderInfoLiteDto> {
        val mode = TruckLaneSearchSpec.parse(truckLanceCode)
        if (mode is TruckLaneSearchSpec.Mode.NoFilter) {
            return searchDoLiteByPage(
                code,
                shopName,
                status,
                estimatedArrivalDate,
                pageNum,
                pageSize,
                null,
                floor,
                isExtra,
            )
        }
        val pageIdx = (pageNum ?: 1).coerceAtLeast(1) - 1
        val size = (pageSize ?: 10).coerceAtLeast(1).coerceAtMost(10_000)
        val statusEnum = status?.let { s -> DeliveryOrderStatus.entries.find { it.value == s } }
        val etaStart = estimatedArrivalDate
        val etaEnd = estimatedArrivalDate?.plusDays(1)

        val lanePredicate: (String?) -> Boolean = { lane -> TruckLaneSearchSpec.matches(mode, lane) }
        val matched = scanDoLiteSupplierFilteredWithLanePredicate(
            code = code?.ifBlank { null },
            shopName = shopName?.ifBlank { null },
            statusEnum = statusEnum,
            etaStart = etaStart,
            etaEnd = etaEnd,
            isExtra = isExtra,
            allowedSupplierCodes = allowedSupplierCodesForFloor(floor),
            lanePredicate = lanePredicate,
        )
        val total = matched.size
        val from = pageIdx * size
        val pageRecords =
            if (from >= total) emptyList()
            else matched.subList(from, minOf(from + size, total))
        return RecordsRes(pageRecords, total)
    }

    /**
     * 僅回傳依店鋪／ETA 從 truck 排程**推算後** `truckLanceCode` 為 null 或空白的送貨單（畫面上對應「車線-X」）。
     * 與 [searchDoLiteByPage] 帶一般車線關鍵字分開，避免 `車線-X` 在 truck 表無 shopId 時走舊邏輯漏單。
     */
    open fun searchDoLiteUnassignedTruckByPage(
        code: String?,
        shopName: String?,
        status: String?,
        estimatedArrivalDate: LocalDateTime?,
        pageNum: Int?,
        pageSize: Int?,
        floor: String? = null,
        isExtra: Boolean? = null,
    ): RecordsRes<DeliveryOrderInfoLiteDto> {
        val page = (pageNum ?: 1) - 1
        val size = pageSize ?: 10
        val statusEnum = status?.let { s -> DeliveryOrderStatus.entries.find { it.value == s } }
        val etaStart = estimatedArrivalDate
        val etaEnd = estimatedArrivalDate?.plusDays(1)
        val allowedSupplierCodes = allowedSupplierCodesForFloor(floor)

        val allResult = deliveryOrderRepository.searchDoLitePageWithSupplierCodes(
            code = code?.ifBlank { null },
            shopName = shopName?.ifBlank { null },
            status = statusEnum,
            etaStart = etaStart,
            etaEnd = etaEnd,
            isExtra = isExtra,
            allowedSupplierCodes = allowedSupplierCodes,
            pageable = PageRequest.of(0, 100_000),
        )

        val deliveryOrderIds = allResult.content.mapNotNull { it.id }
        val deliveryOrdersMap = deliveryOrderRepository.findAllById(deliveryOrderIds).associateBy { it.id }
        val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()

        val processedRecords = allResult.content.map { info ->
            val deliveryOrder = deliveryOrdersMap[info.id]
            val supplierCode = deliveryOrder?.supplier?.code
            val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
            val shop = deliveryOrder?.shop
            val shopId = shop?.id
            val infoEta = info.estimatedArrivalDate

            val calculatedTruckLanceCode =
                if (deliveryOrder != null && shopId != null && infoEta != null) {
                    val targetDate = infoEta.toLocalDate()
                    val dayAbbr = getDayOfWeekAbbr(targetDate)
                    resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)?.truckLanceCode
                } else {
                    null
                }

            DeliveryOrderInfoLiteDto(
                id = info.id,
                code = info.code,
                orderDate = info.orderDate,
                estimatedArrivalDate = info.estimatedArrivalDate,
                status = info.status,
                shopName = info.shopName,
                supplierName = info.supplierName,
                shopAddress = info.shopAddress,
                truckLanceCode = calculatedTruckLanceCode,
                isExtra = deliveryOrdersMap[info.id]?.isExtra ?: info.isExtra,
            )
        }.filter { dto -> TruckLaneSearchSpec.isUnassignedResolvedLane(dto.truckLanceCode) }

        val totalCount = processedRecords.size
        val startIndex = page * size
        val endIndex = minOf(startIndex + size, processedRecords.size)
        val paginatedRecords = if (startIndex < processedRecords.size) {
            processedRecords.subList(startIndex, endIndex)
        } else {
            emptyList()
        }
        return RecordsRes(paginatedRecords, totalCount)
    }

        open fun findByM18DataLogId(m18DataLogId: Long): DeliveryOrder? {
            return deliveryOrderRepository.findTopByM18DataLogIdOrderByModifiedDesc(m18DataLogId)
        }

        open fun getDoList(): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findDeliveryOrderInfoByDeletedIsFalse();
        }

        /*
    open fun getDetailedDo(id: Long) : DeliveryOrder? {
        return deliveryOrderRepository.findByIdAndDeletedIsFalse(id);
    }
    */
        private fun computeAvailableStockQtyForItem(itemId: Long): BigDecimal {
            val availableLots = inventoryLotLineRepository
                .findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE)
            return availableLots.sumOf { lot ->
                (lot.inQty ?: BigDecimal.ZERO)
                    .minus(lot.outQty ?: BigDecimal.ZERO)
                    .minus(lot.holdQty ?: BigDecimal.ZERO)
            }
        }

        /**
         * Picker display name from [DeliveryOrderPickOrder.handlerName] (workbench source of truth).
         * Linked via [pick_order.doId] or [do_pick_order_line.do_order_id]; falls back to [User.name]
         * from [DeliveryOrderPickOrder.handledBy] when [handlerName] was not persisted.
         */
        private fun resolveHandlerNameForDeliveryOrder(doId: Long): String? {
            val sql = """
                SELECT DISTINCT TRIM(handler_name) AS handler_name
                FROM (
                    SELECT COALESCE(
                        NULLIF(TRIM(dop.handlerName), ''),
                        NULLIF(TRIM(u.name), '')
                    ) AS handler_name
                    FROM fpsmsdb.delivery_order_pick_order dop
                    INNER JOIN fpsmsdb.pick_order po
                      ON po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0
                    LEFT JOIN fpsmsdb.user u
                      ON u.id = dop.handledBy AND u.deleted = 0
                    WHERE dop.deleted = 0
                      AND po.doId = :doId

                    UNION

                    SELECT COALESCE(
                        NULLIF(TRIM(dop.handlerName), ''),
                        NULLIF(TRIM(u.name), '')
                    ) AS handler_name
                    FROM fpsmsdb.delivery_order_pick_order dop
                    INNER JOIN fpsmsdb.pick_order po
                      ON po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0
                    INNER JOIN fpsmsdb.do_pick_order_line dpol
                      ON dpol.pick_order_id = po.id AND dpol.deleted = 0
                    LEFT JOIN fpsmsdb.user u
                      ON u.id = dop.handledBy AND u.deleted = 0
                    WHERE dop.deleted = 0
                      AND dpol.do_order_id = :doId
                ) resolved
                WHERE handler_name IS NOT NULL AND TRIM(handler_name) <> ''
                ORDER BY handler_name
            """.trimIndent()

            val names = jdbcDao.queryForList(sql, mapOf("doId" to doId))
                .mapNotNull { row ->
                    val key = row.keys.find { it.equals("handler_name", true) } ?: return@mapNotNull null
                    row[key]?.toString()?.trim()?.takeIf { it.isNotEmpty() }
                }
                .distinct()
                .sorted()

            return names.joinToString(",").ifBlank { null }
        }

        private fun resolveTruckLaneCodeForDeliveryOrder(doId: Long): String? {
            doPickOrderRepository.findByDoOrderIdAndDeletedFalse(doId)
                .mapNotNull { it.truckLanceCode?.trim()?.takeIf { code -> code.isNotEmpty() } }
                .firstOrNull()
                ?.let { return it }

            doPickOrderRecordRepository.findByDoOrderIdAndDeletedFalse(doId)
                .mapNotNull { it.truckLanceCode?.trim()?.takeIf { code -> code.isNotEmpty() } }
                .firstOrNull()
                ?.let { return it }

            val sql = """
                SELECT DISTINCT TRIM(truck_lance_code) AS truck_lance_code
                FROM (
                    SELECT NULLIF(TRIM(dop.truckLanceCode), '') AS truck_lance_code
                    FROM fpsmsdb.delivery_order_pick_order dop
                    INNER JOIN fpsmsdb.pick_order po
                      ON po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0
                    WHERE dop.deleted = 0
                      AND po.doId = :doId

                    UNION

                    SELECT NULLIF(TRIM(dop.truckLanceCode), '') AS truck_lance_code
                    FROM fpsmsdb.delivery_order_pick_order dop
                    INNER JOIN fpsmsdb.pick_order po
                      ON po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0
                    INNER JOIN fpsmsdb.do_pick_order_line dpol
                      ON dpol.pick_order_id = po.id AND dpol.deleted = 0
                    WHERE dop.deleted = 0
                      AND dpol.do_order_id = :doId
                ) resolved
                WHERE truck_lance_code IS NOT NULL AND TRIM(truck_lance_code) <> ''
                ORDER BY truck_lance_code
                LIMIT 1
            """.trimIndent()

            return jdbcDao.queryForList(sql, mapOf("doId" to doId))
                .firstOrNull()
                ?.let { row ->
                    val key = row.keys.find { it.equals("truck_lance_code", true) } ?: return@let null
                    row[key]?.toString()?.trim()?.takeIf { it.isNotEmpty() }
                }
        }

        open fun getDetailedDo(id: Long): DoDetailResponse? {
            val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(id) ?: return null
            val handlerName = try {
                resolveHandlerNameForDeliveryOrder(id)
            } catch (ex: Exception) {
                log.warn("Failed to resolve handler name for delivery order {}: {}", id, ex.message)
                null
            }
            val truckLaneCode = try {
                resolveTruckLaneCodeForDeliveryOrder(id)
            } catch (ex: Exception) {
                log.warn("Failed to resolve truck lane for delivery order {}: {}", id, ex.message)
                null
            }

            val itemIds = deliveryOrder.deliveryOrderLines.filter { it.deleted != true }.mapNotNull { it.item?.id }.distinct()
            val stockQtyByItemId = itemIds.associateWith { itemId ->
                computeAvailableStockQtyForItem(itemId)
            }
            val fallbackQtyByItemId = deliveryOrder.deliveryOrderLines.filter { it.deleted != true }.mapNotNull { line ->
                val itemId = line.item?.id ?: return@mapNotNull null
                itemId to (line.qty ?: BigDecimal.ZERO)
            }.toMap()
            val actualShippedQtyByItemId = try {
                doReplenishmentService.resolveActualShippedQtyForDeliveryOrder(id, fallbackQtyByItemId)
            } catch (ex: Exception) {
                log.warn("Failed to resolve actual shipped qty for delivery order {}: {}", id, ex.message)
                fallbackQtyByItemId
            }

            return DoDetailResponse(
                id = deliveryOrder.id!!,
                code = deliveryOrder.code ?: "",
                supplierCode = deliveryOrder.supplier?.code,
                shopCode = deliveryOrder.shop?.code,
                shopName = deliveryOrder.shop?.name,
                currencyCode = deliveryOrder.currency?.code,
                orderDate = deliveryOrder.orderDate,
                estimatedArrivalDate = deliveryOrder.estimatedArrivalDate,
                completeDate = deliveryOrder.completeDate,
                status = deliveryOrder.status?.value,
                isExtra = deliveryOrder.isExtra,
                handlerName = handlerName,
                truckLaneCode = truckLaneCode,
                deliveryOrderLines = deliveryOrder.deliveryOrderLines.filter { it.deleted != true }.map { line ->
                    val itemId = line.item?.id
                    val stockQty = itemId?.let { stockQtyByItemId[it] } ?: BigDecimal.ZERO
                    val requiredQty = line.qty ?: BigDecimal.ZERO
                    val availableStatus =
                        if (stockQty >= requiredQty) "available" else "insufficient"

                    DoDetailLineResponse(
                        id = line.id!!,
                        itemNo = line.itemNo,
                        qty = line.qty,
                        actualShippedQty = itemId?.let { actualShippedQtyByItemId[it] },
                        price = line.price,
                        status = line.status?.value,
                        itemName = line.item?.name,
                        uomCode = line.uom?.code,
                        uom = line.uom?.udfudesc,
                        shortUom = line.uom?.udfShortDesc,
                        stockQty = stockQty,
                        availableStatus = availableStatus,
                    )
                }
            )
        }

        open fun searchByCode(code: String?): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndDeletedIsFalse(code);
        }

        open fun searchByShopName(shopName: String): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByShopNameContainsAndDeletedIsFalse(shopName);
        }

        open fun searchByStatus(status: DeliveryOrderStatus): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByStatusAndDeletedIsFalse(status);
        }

        open fun searchBetweenOrderDate(start: LocalDateTime?, end: LocalDateTime?): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findByOrderDateBetweenAndDeletedIsFalse(start, end);
        }


        open fun getFullList(): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllBy();
        }

        open fun searchAll(
            code: String?,
            shopName: String?,
            status: DeliveryOrderStatus?,
            orderStartDate: LocalDateTime?,
            orderEndDate: LocalDateTime?,
            estArrStartDate: LocalDateTime?,
            estArrEndDate: LocalDateTime?
        ): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndStatusAndOrderDateBetweenAndEstimatedArrivalDateBetweenAndDeletedIsFalse(
                code,
                shopName,
                status,
                orderStartDate,
                orderEndDate,
                estArrStartDate,
                estArrEndDate
            );
        }

        open fun searchWithoutStatus(
            code: String?,
            shopName: String?,
            orderStartDate: LocalDateTime?,
            orderEndDate: LocalDateTime?,
            estArrStartDate: LocalDateTime?,
            estArrEndDate: LocalDateTime?
        ): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndOrderDateBetweenAndEstimatedArrivalDateBetweenAndDeletedIsFalse(
                code,
                shopName,
                orderStartDate,
                orderEndDate,
                estArrStartDate,
                estArrEndDate
            );
        }

        open fun searchWithoutEstArrDate(
            code: String?,
            shopName: String?,
            status: DeliveryOrderStatus?,
            orderStartDate: LocalDateTime?,
            orderEndDate: LocalDateTime?
        ): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndStatusAndOrderDateBetweenAndDeletedIsFalse(
                code,
                shopName,
                status,
                orderStartDate,
                orderEndDate
            );
        }

        /*
    open fun searchDoForList(
        code: String?,
        shopName: String?,
        status: String?,              // 前端传过来的字符串（pending/receiving/completed 或空）
        orderFrom: LocalDateTime?,
        orderTo: LocalDateTime?,
        etaFrom: LocalDateTime?,
        etaTo: LocalDateTime?,

    ): Page<DoSearchRow> {

        val page = deliveryOrderRepository.search(
            code = if (code.isNullOrBlank()) null else code,
            shopName = if (shopName.isNullOrBlank()) null else shopName,
            status = if (status.isNullOrBlank()) null else status,
            orderFrom = orderFrom,
            orderTo = orderTo,
            etaFrom = etaFrom,
            etaTo = etaTo,
            pageable = pageable
        )

        return page.map { d ->
            DoSearchRow(
                id = d.id!!,
                code = d.code,
                shopName = d.shop?.name,
                supplierName = d.supplier?.name,
                orderDate = d.orderDate,
                estimatedArrivalDate = d.estimatedArrivalDate,
                status = d.status?.value,
                deliveryOrderLineCount = d.deliveryOrderLines.size  // 只要数量
            )
        }
    }
    */
        open fun searchWithoutEstArrDateAndStatus(
            code: String?,
            shopName: String?,
            orderStartDate: LocalDateTime?,
            orderEndDate: LocalDateTime?
        ): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndOrderDateBetweenAndDeletedIsFalse(
                code,
                shopName,
                orderStartDate,
                orderEndDate
            );
        }

        open fun searchWithoutOrderDate(
            code: String?,
            shopName: String?,
            status: DeliveryOrderStatus?,
            estArrStartDate: LocalDateTime?,
            estArrEndDate: LocalDateTime?
        ): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndStatusAndEstimatedArrivalDateBetweenAndDeletedIsFalse(
                code,
                shopName,
                status,
                estArrStartDate,
                estArrEndDate
            );
        }

        open fun searchWithoutOrderDateAndStatus(
            code: String?,
            shopName: String?,
            estArrStartDate: LocalDateTime?,
            estArrEndDate: LocalDateTime?
        ): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndEstimatedArrivalDateBetweenAndDeletedIsFalse(
                code,
                shopName,
                estArrStartDate,
                estArrEndDate
            );
        }

        open fun searchWithoutDate(
            code: String?,
            shopName: String?,
            status: DeliveryOrderStatus?
        ): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndStatusAndDeletedIsFalse(
                code,
                shopName,
                status
            );
        }

        open fun searchCodeAndShopName(code: String?, shopName: String?): List<DeliveryOrderInfo> {
            return deliveryOrderRepository.findAllByCodeContainsAndShopNameContainsAndDeletedIsFalse(code, shopName);
        }

        open fun getWarehouseOrderByItemId(itemId: Long): Int? {
            val inventoryLots = inventoryLotService.findByItemId(itemId)
            if (inventoryLots.isNotEmpty()) {
                val inventoryLotId = inventoryLots.first().id?.toInt()
                return inventoryLotId?.let { lotId ->
                    // 查询 warehouse 的 order 字段
                    val sql = """
                    SELECT w.`order` as warehouseOrder
                    FROM fpsmsdb.inventory_lot_line ill
                    JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
                    WHERE ill.inventoryLotId = :lotId
                    AND ill.deleted = false
                    ORDER BY w.`order` ASC
                    LIMIT 1
                """.trimIndent()
                    val result = jdbcDao.queryForList(sql, mapOf("lotId" to lotId))
                    if (result.isNotEmpty()) {
                        val orderString = result.first()["warehouseOrder"] as? String
                        // Extract the last 3 digits from format like "2F-001"
                        orderString?.let { order ->
                            // Try to extract numeric part after the last "-"
                            val parts = order.split("-")
                            if (parts.isNotEmpty()) {
                                val lastPart = parts.last().trim()
                                // Take last 3 characters if they are all digits
                                if (lastPart.length >= 3) {
                                    val lastThreeDigits = lastPart.takeLast(3)
                                    if (lastThreeDigits.all { it.isDigit() }) {
                                        return lastThreeDigits.toIntOrNull()
                                    }
                                }
                                // Fallback: try to parse the last part as Int
                                lastPart.toIntOrNull()
                            } else {
                                // Fallback: try to extract any 3-digit sequence from the end
                                val lastThreeDigits = order.takeLast(3)
                                if (lastThreeDigits.all { it.isDigit() }) {
                                    lastThreeDigits.toIntOrNull()
                                } else {
                                    null
                                }
                            }
                        }
                    } else null
                }
            }
            return null
        }

        //  新增方法2：获取 warehouse 的 code 字段（用于显示路由）
        open fun getWarehouseCodeByItemId(itemId: Long): String? {
            val inventoryLots = inventoryLotService.findByItemId(itemId)
            if (inventoryLots.isNotEmpty()) {
                val inventoryLotId = inventoryLots.first().id?.toInt()
                return inventoryLotId?.let { lotId ->
                    val sql = """
                    SELECT w.code as warehouseCode
                    FROM fpsmsdb.inventory_lot_line ill
                    JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
                    WHERE ill.inventoryLotId = :lotId
                    AND ill.deleted = false
                    ORDER BY w.`order` ASC
                    LIMIT 1
                """.trimIndent()
                    val result = jdbcDao.queryForList(sql, mapOf("lotId" to lotId))
                    if (result.isNotEmpty()) {
                        result.first()["warehouseCode"] as String?
                    } else null
                }
            }
            return null
        }

    open fun updateDeliveryOrderStatus(request: SaveDeliveryOrderStatusRequest): SaveDeliveryOrderResponse {
        val deliveryOrder = checkNotNull(
            request.id?.let { deliveryOrderRepository.findById(it).getOrNull() }
        ) { "ID must be set" }

        val status = checkNotNull(
            request.status?.let { status -> DeliveryOrderStatus.entries.find { it.value == status } }
        ) { "Status must be set" }

        deliveryOrder.apply {
            this.status = status
        }

        val savedDeliveryOrder = deliveryOrderRepository.save(deliveryOrder).let {
            SaveDeliveryOrderResponse(
                id = it.id,
                code = it.code,
                supplierCode = it.supplier?.code,
                shopCode = it.shop?.code,
                currencyCode = it.currency?.code,
                orderDate = it.orderDate,
                estimatedArrivalDate = it.estimatedArrivalDate,
                completeDate = it.completeDate,
                status = it.status?.value
            )
        }

        return savedDeliveryOrder
    }

    @Throws(IOException::class)
    @Transactional
    open fun saveDeliveryOrder(request: SaveDeliveryOrderRequest): SaveDeliveryOrderResponse {
        val deliveryOrder =
            request.id?.let { deliveryOrderRepository.findById(it).getOrDefault(DeliveryOrder()) } ?: DeliveryOrder()
        val supplier = request.m18SupplierId?.let { shopService.findVendorByM18Id(it) }
            ?: request.supplierId?.let { shopService.findById(it) }
        val shop = request.m18ShopId?.let { shopService.findShopByM18Id(it) }
            ?: request.shopId?.let { shopService.findById(it) }
        val currency = request.m18CurrencyId?.let { currencyService.findByM18Id(it) }
            ?: request.currencyId?.let { currencyService.findById(it) }
        val status = request.status?.let { status -> DeliveryOrderStatus.entries.find { it.value == status } }
        val m18DataLog = request.m18DataLogId?.let { m18DataLogRepository.findById(it).getOrNull() }
        val handler = request.handlerId?.let { userRepository.findById(it).getOrNull() }

        //Need check duplicate?
        deliveryOrder.apply {
            code = request.code
            this.supplier = supplier
            this.shop = shop
            this.currency = currency
            orderDate = request.orderDate
            estimatedArrivalDate = request.estimatedArrivalDate
            completeDate = request.completeDate
            this.status = status
            this.m18DataLog = m18DataLog
            this.handler = handler
            m18BeId = request.m18BeId
            this.deleted = request.deleted
            isExtra = request.isExtra ?: false
        }

        val savedDeliveryOrder = deliveryOrderRepository.saveAndFlush(deliveryOrder).let {
            SaveDeliveryOrderResponse(
                id = it.id,
                code = it.code,
                supplierCode = it.supplier?.code,
                shopCode = it.shop?.code,
                currencyCode = it.currency?.code,
                orderDate = it.orderDate,
                estimatedArrivalDate = it.estimatedArrivalDate,
                completeDate = it.completeDate,
                status = it.status?.value
            )
        }

        return savedDeliveryOrder
    }

    @Transactional(rollbackFor = [Exception::class])
    open fun releaseDeliveryOrder(request: ReleaseDoRequest): MessageResponse {
        println(" DEBUG: Starting releaseDeliveryOrder for DO ID: ${request.id}, User ID: ${request.userId}")

        val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id)
            ?: throw NoSuchElementException("Delivery Order not found")

        println(" DEBUG: Found delivery order - ID: ${deliveryOrder.id}, Shop: ${deliveryOrder.shop?.code}, Status: ${deliveryOrder.status}")

        deliveryOrder.apply {
            status = DeliveryOrderStatus.PENDING
        }
        deliveryOrderRepository.save(deliveryOrder)

        val pols = mapDeliveryOrderLinesForPickOrderRelease(deliveryOrder)
        val po = SavePickOrderRequest(
            doId = deliveryOrder.id,
            type = PickOrderType.DELIVERY_ORDER,
            targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(),
            pickOrderLine = pols
        )

        val createdPickOrder = pickOrderService.create(po)
        println("    DEBUG: Created pick order - ID: ${createdPickOrder.id}")

        val consoCode = pickOrderService.assignConsoCode()
        val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null)

        if (pickOrderEntity != null) {
            pickOrderEntity.consoCode = consoCode
            pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED
            pickOrderRepository.saveAndFlush(pickOrderEntity)
            println(" DEBUG: Assigned consoCode $consoCode to pick order ${createdPickOrder.id}")

            //  Debug: Check pick order lines
            println(" DEBUG: Pick order has ${pickOrderEntity.pickOrderLines?.size ?: 0} pick order lines")
            pickOrderEntity.pickOrderLines?.forEach { line ->
                println("    DEBUG: Pick order line - Item ID: ${line.item?.id}, Qty: ${line.qty}")
            }


            val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!)
            val suggestions = suggestedPickLotService.suggestionForPickOrderLines(
                SuggestedPickLotForPolRequest(pickOrderLines = lines)
            )

            val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList)
            println("    DEBUG: Saved ${saveSuggestedPickLots.size} suggested pick lots")
            val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null }
            if (insufficientCount > 0) {
                println("⚠️ WARNING: $insufficientCount items have insufficient stock (issues auto-created)")
            }

            //  Hold inventory quantities
            val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(
                saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id }
            )

            saveSuggestedPickLots.forEach { lot ->
                if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) {
                    val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine)
                    if (lineIndex >= 0) {
                        inventoryLotLines[lineIndex].holdQty =
                            (inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO)
                    }
                }
            }
            inventoryLotLineRepository.saveAll(inventoryLotLines)

            //  Create stock out record and pre-create stock out lines
            val stockOut = StockOut().apply {
                this.type = "do"
                this.consoPickOrderCode = consoCode
                this.status = StockOutStatus.PENDING.status
                this.handler = request.userId
            }
            val savedStockOut = stockOutRepository.saveAndFlush(stockOut)

            //  Pre-create stock out lines for suggested lots
            saveSuggestedPickLots.forEach { lot ->
                val polId = lot.pickOrderLine?.id
                val illId = lot.suggestedLotLine?.id
                if (polId != null && illId != null) {
                    val existingLines =
                        stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId)
                    if (existingLines.isEmpty()) {
                        val pickOrderLine = pickOrderLineRepository.findById(polId).orElse(null)
                        val inventoryLotLine = inventoryLotLineRepository.findById(illId).orElse(null)

                        if (pickOrderLine != null && inventoryLotLine != null) {
                            val line = StockOutLine().apply {
                                this.stockOut = savedStockOut
                                this.pickOrderLine = pickOrderLine
                                this.inventoryLotLine = inventoryLotLine
                                this.item = pickOrderLine.item
                                this.status = StockOutLineStatus.PENDING.status
                                this.qty = 0.0
                                this.type = "NOR"
                            }
                            stockOutLineRepository.save(line)
                        }
                    }
                }
            }
        }

        //  CREATE do_pick_order_record entries
        val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
        val datePrefix = targetDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))

        println("    DEBUG: Target date: $targetDate, Date prefix: $datePrefix")
        
        //  新逻辑：根据 supplier code 决定楼层（清單來自 settings）
        val supplierCode = deliveryOrder.supplier?.code
        val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
        val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)

        println("    DEBUG: Supplier code: $supplierCode, Preferred floor: $preferredFloor")

        //  查找 truck
        val truck = deliveryOrder.shop?.id?.let { shopId ->
            println("    DEBUG: Looking for truck with shop ID: $shopId")
            val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
            println("    DEBUG: Found ${trucks.size} trucks for shop $shopId")

            val preferredStoreId = when (preferredFloor) {
                "2F" -> "2F"
                "4F" -> "4F"
                "3F" -> "3F"
                else -> "2F"
            }
            // 优先选择匹配的 truck
            val selectedTruck = if (trucks.size > 1) {
                trucks.find { it.storeId == preferredStoreId }
                    ?: trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
            } else {
                trucks.firstOrNull()
            }

            println("    DEBUG: Selected truck: ID=${selectedTruck?.id}, StoreId=${selectedTruck?.storeId}")
            selectedTruck
        }

        //  检查 truck 和 preferredFloor 是否匹配
        val truckStoreId = truck?.storeId
        val expectedStoreId = when (preferredFloor) {
            "2F" -> "2F"
            "4F" -> "4F"
            "3F" -> "3F"
            else -> "2F"
        }

        val defaultTruckId = 5577L
        val effectiveTruck = truck ?: truckRepository.findById(defaultTruckId).orElse(null)
        if (effectiveTruck == null) {
            val errorMsg = "No matching truck for preferredFloor ($preferredFloor) and default truck $defaultTruckId not found. Skipping DO ${deliveryOrder.id}."
            println("⚠️ $errorMsg")
            throw IllegalStateException(errorMsg)
        }

        println(" DEBUG: Truck matches preferred floor - Truck Store: ${effectiveTruck.storeId}, Preferred: $preferredFloor")

        val usedDefaultTruck = (truck == null)
        val storeId: String? = if (usedDefaultTruck) null else "$preferredFloor/F"
        val loadingSequence = effectiveTruck.loadingSequence ?: 999

        println("    DEBUG: Creating DoPickOrder - Floor: $preferredFloor, Store: $storeId, Truck: ${effectiveTruck.id}")

        val doPickOrder = DoPickOrder(
            storeId = storeId,
            ticketNo = "TEMP-${System.currentTimeMillis()}",
            ticketStatus = DoPickOrderStatus.pending,
            truckId = effectiveTruck.id,
            pickOrderId = createdPickOrder.id,
            doOrderId = deliveryOrder.id,
            ticketReleaseTime = null,
            shopId = deliveryOrder.shop?.id,
            handlerName = null,
            handledBy = null,
            ticketCompleteDateTime = null,
            truckDepartureTime = effectiveTruck.departureTime,
            truckLanceCode = effectiveTruck.truckLanceCode,
            shopCode = deliveryOrder.shop?.code,
            shopName = deliveryOrder.shop?.name,
            requiredDeliveryDate = targetDate,
            createdBy = null,
            modifiedBy = null,
            pickOrderCode = createdPickOrder.code,
            deliveryOrderCode = deliveryOrder.code,
            loadingSequence = loadingSequence,
            releaseType = null
        )

        val savedDoPickOrder = doPickOrderService.save(doPickOrder)
        println("    DEBUG: Saved DoPickOrder - ID: ${savedDoPickOrder.id}")

        return MessageResponse(
            id = deliveryOrder.id,
            code = deliveryOrder.code,
            name = deliveryOrder.shop?.name,
            type = null,
            message = null,
            errorPosition = null,
            entity = mapOf("status" to deliveryOrder.status?.value)
        )
    }

    open fun getLotNumbersForPickOrderByItemId(itemId: Long, pickOrderId: Long): String {
        try {
            val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrderId)
            val pickOrderLineIds = pickOrderLines.mapNotNull { it.id }
            val suggestedPickLots = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds)

            val lotNumbers = suggestedPickLots
                .filter { it.pickOrderLine?.item?.id == itemId }
                .mapNotNull { it.suggestedLotLine?.inventoryLot?.lotNo }
                .distinct()

            return lotNumbers.joinToString(", ")
        } catch (e: Exception) {
            println("Error getting lot numbers for item $itemId in pick order $pickOrderId: ${e.message}")
            return ""
        }
    }


    // Delivery Note
    @Throws(IOException::class)
    @Transactional
    open fun exportDeliveryNote(request: ExportDeliveryNoteRequest): Map<String, Any> {
        val tAll0 = System.nanoTime()
        val tJ0 = System.nanoTime()
        val deliveryNote = deliveryNoteJasperTemplate
        val jasperGetMs = (System.nanoTime() - tJ0) / 1_000_000.0

        val fields = mutableListOf<MutableMap<String, Any>>()
        val params = mutableMapOf<String, Any>()

        val tFill0 = System.nanoTime()
        val result = if (request.isDraft) {
            exportDeliveryNoteFromDraft(request, deliveryNote, fields, params)
        } else {
            exportDeliveryNoteFromRecord(request, deliveryNote, fields, params)
        }
        val fillDataMs = (System.nanoTime() - tFill0) / 1_000_000.0
        val totalMs = (System.nanoTime() - tAll0) / 1_000_000.0
        log.info(
            "DeliveryNote export timing: jasperGetMs={} fillDataMs={} totalMs={} doPickOrderId={} isDraft={}",
            jasperGetMs,
            fillDataMs,
            totalMs,
            request.doPickOrderId,
            request.isDraft,
        )
        return result
    }

    // DRAFT
    private fun exportDeliveryNoteFromDraft(
        request: ExportDeliveryNoteRequest,
        deliveryNote: net.sf.jasperreports.engine.JasperReport,
        fields: MutableList<MutableMap<String, Any>>,
        params: MutableMap<String, Any>
    ): Map<String, Any> {
        val doPickOrder = doPickOrderRepository.findById(request.doPickOrderId).orElseThrow {
            NoSuchElementException("DoPickOrder not found with ID: ${request.doPickOrderId}")
        }

        val pickOrderIds = if (doPickOrder.pickOrderId != null) {
            // SINGLE DELIVERY ORDER
            listOf(doPickOrder.pickOrderId!!)
        } else {
            // MERGED DELIVERY ORDERS (DOPICKORDERLINE)
            val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!)
            val orderIds = doPickOrderLines.mapNotNull { it.pickOrderId }.distinct()

            if (orderIds.isEmpty()) {
                throw IllegalStateException("DoPickOrder ${request.doPickOrderId} has no associated pick orders")
            }
            orderIds
        }

        val deliveryOrderIds = if (doPickOrder.doOrderId != null) {
            // SINGLE DELIVERY ORDER
            listOf(doPickOrder.doOrderId!!)
        } else {
            // MERGED DELIVERY ORDERS (DOPICKORDERLINE)
            val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!)
            val orderIds = doPickOrderLines.mapNotNull { it.doOrderId }.distinct()

            if (orderIds.isEmpty()) {
                throw IllegalStateException("DoPickOrder ${request.doPickOrderId} has no associated delivery orders")
            }
            orderIds
        }

        val deliveryNoteInfo = deliveryOrderIds.flatMap { deliveryOrderId ->
            deliveryOrderRepository.findDeliveryOrderInfoById(deliveryOrderId)
        }.toMutableList()

        if (deliveryNoteInfo.isEmpty()) {
            throw NoSuchElementException("Delivery orders not found for IDs: $deliveryOrderIds")
        }

        val pickOrderId = pickOrderIds.first()
        val truckNo = doPickOrder.truckLanceCode ?: ""
        val selectedPickOrder = pickOrderRepository.findById(pickOrderId).orElse(null)

        val deliveryOrders = deliveryOrderRepository.findAllById(deliveryOrderIds)
        val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") }
        val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra }
        val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(deliveryOrderIds)

        val exportLines = deliveryNoteExportLines(deliveryNoteInfo)
        val sortedLines = exportLines.sortedBy { row ->
            row.line.itemId?.let { itemId ->
                getWarehouseOrderByItemId(itemId)
            } ?: Int.MAX_VALUE
        }
        val uniqueItemIds = sortedLines.mapNotNull { it.line.itemId }.distinct()
        val itemsMap = if (uniqueItemIds.isNotEmpty()) {
            itemsRepository.findAllById(uniqueItemIds).associateBy { it.id }
        } else {
            emptyMap()
        }

        sortedLines.forEach { row ->
            val line = row.line
            val field = mutableMapOf<String, Any>()
            val sequenceNumber = fields.size + 1
            val isExtra = isExtraDeliveryTicket(
                lineTicketNo = null,
                deliveryOrderIsExtra = isExtraByDoId[row.deliveryOrderId] == true,
                headerIsMerge = false,
            )

            field["sequenceNumber"] = formatSequenceNumber(
                sequenceNumber,
                isExtra,
                replenishPdfIndex.matches(row.deliveryOrderId, line.itemId, null),
            )
            field["deliveryOrderCode"] = formatDoCodeWithUnderlineLast4(
                deliveryOrderCodeById[row.deliveryOrderId].orEmpty(),
            )
            field["itemNo"] = line.itemNo
            field["itemName"] = line.itemName ?: ""
            field["uom"] = line.uom ?: ""
            field["qty"] = line.qty.toString()
            field["shortName"] = line.uomShortDesc ?: ""

            val route = line.itemId?.let { itemId ->
                getWarehouseCodeByItemId(itemId)
            } ?: "-"
            field["route"] = route

            //USE SUGGESTED PICK LOTS
            val lotNo = line.itemId?.let { itemId ->
                pickOrderIds.mapNotNull { pickOrderId ->
                    val lots = getLotNumbersForPickOrderByItemId(itemId, pickOrderId)
                    lots.ifBlank { null }
                }.distinct().joinToString(", ")
            }?.ifBlank { "沒有庫存" } ?: "沒有庫存"
            field["lotNo"] = lotNo

            val signOff = line.itemId?.let { itemId ->
                val item = itemsMap[itemId]
                if (item?.isEgg == true) {
                    "簽署: __________"
                } else {
                    ""
                }
            } ?: ""
            field["signOff"] = signOff

            fields.add(field)
        }

        params["dnTitle"] = "送貨單(初稿)"
        params["colQty"] = "所需數量"
        params["totalCartonTitle"] = ""
        params["deliveryNoteCodeTitle"] = ""
        params["deliveryNoteCode"] = ""

        params["numOfCarton"] = request.numOfCarton.toString()
        if (params["numOfCarton"] == "0") {
            params["numOfCarton"] = ""
        }

        params["shopName"] = formatShopNameForDeliveryNote(
            doPickOrder.shopName ?: deliveryNoteInfo[0].shopName ?: "",
        )
        params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: ""
        params["deliveryDate"] =
            deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
        params["truckNo"] = truckNo
        params["deliveryOrderCodeAll"] = formatDoCodesPlain(deliveryNoteInfo.map { it.code })
        params["loadingSequence"] = doPickOrder.loadingSequence?.let {
            "裝載順序：$it"
        } ?: ""

        return mapOf(
            "report" to PdfUtils.fillReport(deliveryNote, fields, params),
            "filename" to deliveryNoteInfo.joinToString("_") { it.code }
        )
    }

    private fun exportDeliveryNoteFromRecord(
        request: ExportDeliveryNoteRequest,
        deliveryNote: net.sf.jasperreports.engine.JasperReport,
        fields: MutableList<MutableMap<String, Any>>,
        params: MutableMap<String, Any>
    ): Map<String, Any> {
    
        val doPickOrderRecord = doPickOrderRecordRepository.findById(request.doPickOrderId).orElseThrow {
            NoSuchElementException("DoPickOrderRecord not found with ID: ${request.doPickOrderId}")
        }
    
        val doPickOrderLineRecords = doPickOrderLineRecordRepository.findByDoPickOrderId(doPickOrderRecord.recordId)
    
        val pickOrderIds = doPickOrderLineRecords.mapNotNull { it.pickOrderId }.distinct()
        if (pickOrderIds.isEmpty()) {
            throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated pick orders")
        }
    
        val deliveryOrderIds = doPickOrderLineRecords.mapNotNull { it.doOrderId }.distinct()
        if (deliveryOrderIds.isEmpty()) {
            throw IllegalStateException("DoPickOrderRecord ${request.doPickOrderId} has no associated delivery orders")
        }
    
        val deliveryNoteInfo = deliveryOrderIds.flatMap { deliveryOrderId ->
            deliveryOrderRepository.findDeliveryOrderInfoById(deliveryOrderId)
        }.toMutableList()
    
        val pickOrderId = pickOrderIds.first()
        val truckNo = doPickOrderRecord.truckLanceCode ?: ""
        val selectedPickOrder = pickOrderRepository.findById(pickOrderId).orElse(null)
    
        val pickOrderIdByDoId = doPickOrderLineRecords.mapNotNull { rec ->
            val doId = rec.doOrderId
            val poId = rec.pickOrderId
            if (doId != null && poId != null) doId to poId else null
        }.toMap()

        val exportLines = deliveryNoteExportLines(deliveryNoteInfo)

        val pickOrderLines = pickOrderIds.flatMap { pid ->
            pickOrderLineRepository.findAllByPickOrderId(pid)
        }

        val allPickOrderLineIds = pickOrderLines.mapNotNull { it.id }
        val stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>> =
            if (allPickOrderLineIds.isNotEmpty()) {
                allPickOrderLineIds.associateWith { polId ->
                    stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
                }
            } else {
                emptyMap()
            }
    
        val doStoreFloorKey = doPickOrderRecord.storeId?.toString()?.trim()?.uppercase(Locale.ROOT)
            ?.replace("/", "")
            ?.replace(" ", "")
            ?: ""
    
        val uniqueItemIdsForSort = exportLines.mapNotNull { it.line.itemId }.distinct()
        val itemsById = if (uniqueItemIdsForSort.isNotEmpty()) {
            itemsRepository.findAllById(uniqueItemIdsForSort).associateBy { it.id!! }
        } else {
            emptyMap()
        }
    
        val sortedLines = when (doStoreFloorKey) {
            "2F" -> exportLines.sortedWith(
                compareBy(
                    { row -> itemsById[row.line.itemId]?.item_Order ?: Int.MAX_VALUE },
                    { row -> row.line.itemNo },
                ),
            )
            "4F" -> exportLines.sortedBy { row ->
                row.line.itemId?.let { getWarehouseOrderByItemId(it) } ?: Int.MAX_VALUE
            }
            else -> exportLines.sortedBy { row ->
                row.line.itemId?.let { getWarehouseOrderByItemId(it) } ?: Int.MAX_VALUE
            }
        }
    
        val allIllIds = stockOutLinesByPickOrderLineId.values
            .flatMap { lines -> lines.mapNotNull { it.inventoryLotLineId } }
            .distinct()
        val illById: Map<Long, InventoryLotLine> = if (allIllIds.isNotEmpty()) {
            inventoryLotLineRepository.findAllById(allIllIds).associateBy { it.id!! }
        } else {
            emptyMap()
        }

        val deliveryOrders = deliveryOrderRepository.findAllById(deliveryOrderIds)
        val deliveryOrderCodeById = deliveryOrders.associate { it.id!! to (it.code ?: "") }
        val isExtraByDoId = deliveryOrders.associate { it.id!! to it.isExtra }
        val replenishPdfIndex = doReplenishmentService.buildReplenishPdfIndex(deliveryOrderIds)
    
        sortedLines.forEach { row ->
            fields.add(
                buildDeliveryNotePdfLineField(
                    deliveryOrderId = row.deliveryOrderId,
                    line = row.line,
                    sequenceNumber = fields.size + 1,
                    pickOrderIdByDoId = pickOrderIdByDoId,
                    pickOrderLines = pickOrderLines,
                    stockOutLinesByPickOrderLineId = stockOutLinesByPickOrderLineId,
                    illById = illById,
                    itemsById = itemsById,
                    deliveryOrderCodeById = deliveryOrderCodeById,
                    isExtraByDoId = isExtraByDoId,
                    headerTicketNo = null,
                    headerIsMerge = false,
                    replenishPdfIndex = replenishPdfIndex,
                ),
            )
        }

        doReplenishmentService.replenishmentsWithoutDeliveryOrderLine(deliveryOrderIds, exportLines)
            .sortedWith(compareBy({ it.targetDoId }, { it.itemNo }, { it.code }))
            .forEach { replenishment ->
                fields.add(
                    buildDeliveryNotePdfLineFieldForReplenishment(
                        replenishment = replenishment,
                        sequenceNumber = fields.size + 1,
                        pickOrderLines = pickOrderLines,
                        stockOutLinesByPickOrderLineId = stockOutLinesByPickOrderLineId,
                        illById = illById,
                        itemsById = itemsById,
                        deliveryOrderCodeById = deliveryOrderCodeById,
                        isExtraByDoId = isExtraByDoId,
                    ),
                )
            }
    
        params["dnTitle"] = "送貨單"
        params["colQty"] = "已提數量"
        params["totalCartonTitle"] = "總箱數："
        params["deliveryNoteCodeTitle"] = "送貨單編號："
        params["deliveryNoteCode"] = doPickOrderRecord.deliveryNoteCode ?: ""
    
        params["numOfCarton"] = request.numOfCarton.toString()
        if (params["numOfCarton"] == "0") {
            params["numOfCarton"] = ""
        }
    
        params["shopName"] = formatShopNameForDeliveryNote(
            doPickOrderRecord.shopName ?: deliveryNoteInfo[0].shopName ?: "",
        )
        params["shopAddress"] = deliveryNoteInfo[0].shopAddress ?: ""
        params["deliveryDate"] =
            deliveryNoteInfo[0].estimatedArrivalDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
        params["truckNo"] = truckNo
        params["deliveryOrderCodeAll"] = formatDoCodesPlain(deliveryNoteInfo.map { it.code })
        params["loadingSequence"] = doPickOrderRecord.loadingSequence?.let {
            "裝載順序：$it"
        } ?: ""
    
        return mapOf(
            "report" to PdfUtils.fillReport(deliveryNote, fields, params),
            "filename" to deliveryNoteInfo.joinToString("_") { it.code }
        )
    }
    
    data class DeliveryNoteExportLine(
        val deliveryOrderId: Long,
        val line: DeliveryOrderLineInfo,
    )

    fun deliveryNoteExportLines(deliveryNoteInfo: List<DeliveryOrderInfo>): List<DeliveryNoteExportLine> =
        deliveryNoteInfo.flatMap { info ->
            info.deliveryOrderLines.filter { it.deleted != true }.map { line -> DeliveryNoteExportLine(info.id, line) }
        }

    fun resolvePickOrderLineIdForDeliveryNoteLine(
        deliveryOrderId: Long,
        itemId: Long?,
        pickOrderIdByDoId: Map<Long, Long>,
        pickOrderLines: List<PickOrderLine>,
    ): Long? {
        if (itemId == null) return null
        val pickOrderId = pickOrderIdByDoId[deliveryOrderId] ?: return null
        return pickOrderLines
            .firstOrNull { pol -> pol.pickOrder?.id == pickOrderId && pol.item?.id == itemId }
            ?.id
    }

    fun parseTicketTypeLetter(ticketNo: String?): String? {
        if (ticketNo.isNullOrBlank()) return null
        val parts = ticketNo.split("-")
        return if (parts.size >= 2) parts[1] else null
    }

    fun isExtraDeliveryTicket(
        lineTicketNo: String?,
        deliveryOrderIsExtra: Boolean,
        headerIsMerge: Boolean,
    ): Boolean {
        if (headerIsMerge) return deliveryOrderIsExtra
        if (lineTicketNo != null) return parseTicketTypeLetter(lineTicketNo) == "E"
        return deliveryOrderIsExtra
    }

    private fun escapeXmlForJasperStyled(text: String): String =
        text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

    fun formatDoCodeWithUnderlineLast4(code: String): String {
        if (code.isBlank()) return ""
        return if (code.length <= 4) {
            "<style isBold=\"true\" isUnderline=\"true\">${escapeXmlForJasperStyled(code)}</style>"
        } else {
            val prefix = escapeXmlForJasperStyled(code.dropLast(4))
            val suffix = escapeXmlForJasperStyled(code.takeLast(4))
            "$prefix<style isBold=\"true\" isUnderline=\"true\">$suffix</style>"
        }
    }

    fun formatShopNameForDeliveryNote(raw: String): String {
        if (raw.isBlank()) return ""
        val trimmed = raw.trim()
        val (shopCodePart, restPart) = trimmed.split(" - ", limit = 2).let { parts ->
            (parts.getOrNull(0)?.trim().orEmpty()) to (parts.getOrNull(1)?.trim().orEmpty())
        }
        return if (shopCodePart.isNotBlank() && restPart.isNotBlank()) {
            val styledCode =
                "<style isBold=\"true\" isUnderline=\"true\">${escapeXmlForJasperStyled(shopCodePart)}</style>"
            "$styledCode - ${escapeXmlForJasperStyled(restPart)}"
        } else {
            escapeXmlForJasperStyled(trimmed)
        }
    }

    fun formatDoCodesStyled(codes: Collection<String>): String =
        codes.filter { it.isNotBlank() }.distinct().sorted().joinToString(", ") { formatDoCodeWithUnderlineLast4(it) }

    fun formatDoCodesPlain(codes: Collection<String>): String =
        codes.filter { it.isNotBlank() }.distinct().sorted()
            .joinToString(", ") { escapeXmlForJasperStyled(it) }

    fun formatSequenceNumber(sequenceNumber: Int, isExtra: Boolean, isReplenish: Boolean = false): String {
        var label = sequenceNumber.toString()
        if (isExtra) label += "(加單)"
        if (isReplenish) label += "(補貨)"
        return label
    }

    fun buildDeliveryNotePdfLineFieldForReplenishment(
        replenishment: DoReplenishment,
        sequenceNumber: Int,
        pickOrderLines: List<PickOrderLine>,
        stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>,
        illById: Map<Long, InventoryLotLine>,
        itemsById: Map<Long, Items>,
        deliveryOrderCodeById: Map<Long, String>,
        isExtraByDoId: Map<Long, Boolean>,
        headerTicketNo: String? = null,
        headerIsMerge: Boolean = false,
    ): MutableMap<String, Any> {
        val deliveryOrderId = replenishment.targetDoId!!
        val polId = replenishment.pickOrderLineId!!
        val polIdsForRow = listOf(polId)
        val pol = pickOrderLines.firstOrNull { it.id == polId }
        val itemId = replenishment.itemId
        val isExtra = isExtraDeliveryTicket(
            lineTicketNo = headerTicketNo,
            deliveryOrderIsExtra = isExtraByDoId[deliveryOrderId] == true,
            headerIsMerge = headerIsMerge,
        )

        val field = mutableMapOf<String, Any>()
        field["sequenceNumber"] = formatSequenceNumber(sequenceNumber, isExtra, isReplenish = true)
        field["deliveryOrderCode"] = formatDoCodeWithUnderlineLast4(
            deliveryOrderCodeById[deliveryOrderId].orEmpty(),
        )
        field["itemNo"] = replenishment.itemNo ?: pol?.item?.code ?: ""
        field["itemName"] = replenishment.itemName ?: pol?.item?.name ?: ""
        field["uom"] = pol?.uom?.udfudesc ?: ""
        field["shortName"] = pol?.uom?.udfShortDesc ?: ""
        field["qty"] = sumActualPickQtyForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId)
        field["route"] = when {
            itemId != null -> {
                routeFromStockOutsForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId, illById)
                    .takeIf { it != "-" } ?: getWarehouseCodeByItemId(itemId) ?: "-"
            }
            else -> "-"
        }
        field["lotNo"] = lotNumbersForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId)
            .ifBlank { "沒有庫存" }
        field["signOff"] = if (itemId != null && itemsById[itemId]?.isEgg == true) {
            "簽署: __________"
        } else {
            ""
        }
        return field
    }

    fun buildDeliveryNotePdfLineField(
        deliveryOrderId: Long,
        line: DeliveryOrderLineInfo,
        sequenceNumber: Int,
        pickOrderIdByDoId: Map<Long, Long>,
        pickOrderLines: List<PickOrderLine>,
        stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>,
        illById: Map<Long, InventoryLotLine>,
        itemsById: Map<Long, Items>,
        deliveryOrderCodeById: Map<Long, String> = emptyMap(),
        isExtraByDoId: Map<Long, Boolean> = emptyMap(),
        headerTicketNo: String? = null,
        headerIsMerge: Boolean = false,
        replenishPdfIndex: DoReplenishmentService.ReplenishPdfIndex = DoReplenishmentService.ReplenishPdfIndex.EMPTY,
    ): MutableMap<String, Any> {
        val field = mutableMapOf<String, Any>()
        val isExtra = isExtraDeliveryTicket(
            lineTicketNo = headerTicketNo,
            deliveryOrderIsExtra = isExtraByDoId[deliveryOrderId] == true,
            headerIsMerge = headerIsMerge,
        )

        val polId = resolvePickOrderLineIdForDeliveryNoteLine(
            deliveryOrderId = deliveryOrderId,
            itemId = line.itemId,
            pickOrderIdByDoId = pickOrderIdByDoId,
            pickOrderLines = pickOrderLines,
        )
        val isReplenish = replenishPdfIndex.matches(deliveryOrderId, line.itemId, polId)

        field["sequenceNumber"] = formatSequenceNumber(sequenceNumber, isExtra, isReplenish)
        field["deliveryOrderCode"] = formatDoCodeWithUnderlineLast4(
            deliveryOrderCodeById[deliveryOrderId].orEmpty(),
        )
        field["itemNo"] = line.itemNo
        field["itemName"] = line.itemName ?: ""
        field["uom"] = line.uom ?: ""
        field["shortName"] = line.uomShortDesc ?: ""
        val polIdsForRow = listOfNotNull(polId)

        field["qty"] = if (polId != null) {
            sumActualPickQtyForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId)
        } else {
            line.qty.toString()
        }

        val itemId = line.itemId
        field["route"] = when {
            itemId != null && polId != null -> {
                routeFromStockOutsForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId, illById)
                    .takeIf { it != "-" } ?: getWarehouseCodeByItemId(itemId) ?: "-"
            }
            itemId != null -> getWarehouseCodeByItemId(itemId) ?: "-"
            else -> "-"
        }

        field["lotNo"] = if (itemId != null && polId != null) {
            lotNumbersForPickOrderLineIds(polIdsForRow, stockOutLinesByPickOrderLineId).ifBlank { "沒有庫存" }
        } else {
            "沒有庫存"
        }

        field["signOff"] = if (itemId != null && itemsById[itemId]?.isEgg == true) {
            "簽署: __________"
        } else {
            ""
        }

        return field
    }

    fun sumActualPickQtyForPickOrderLineIds(
        pickOrderLineIds: List<Long>,
        stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>,
    ): String {
        if (pickOrderLineIds.isEmpty()) return "0"
        return pickOrderLineIds.sumOf { polId ->
            stockOutLinesByPickOrderLineId[polId].orEmpty().sumOf { it.qty }
        }.toString()
    }

    fun lotNumbersForPickOrderLineIds(
        pickOrderLineIds: List<Long>,
        stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>,
    ): String =
        pickOrderLineIds.flatMap { polId ->
            stockOutLinesByPickOrderLineId[polId].orEmpty().mapNotNull { it.lotNo }
        }.distinct().joinToString(", ")

    fun routeFromStockOutsForPickOrderLineIds(
        pickOrderLineIds: List<Long>,
        stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>,
        illById: Map<Long, InventoryLotLine>,
    ): String {
        val codes = linkedSetOf<String>()
        for (polId in pickOrderLineIds) {
            for (sol in stockOutLinesByPickOrderLineId[polId].orEmpty()) {
                val illId = sol.inventoryLotLineId ?: continue
                val ill = illById[illId] ?: continue
                ill.warehouse?.code?.takeIf { it.isNotBlank() }?.let { codes.add(it) }
            }
        }
        return if (codes.isEmpty()) "-" else codes.joinToString(", ")
    }

    fun routeFromStockOutsForItem(
        itemId: Long,
        pickOrderLineIdsByItemId: Map<Long?, List<Long>>,
        stockOutLinesByPickOrderLineId: Map<Long, List<StockOutLineInfo>>,
        illById: Map<Long, InventoryLotLine>,
    ): String {
        val polIds = pickOrderLineIdsByItemId[itemId] ?: return "-"
        return routeFromStockOutsForPickOrderLineIds(polIds, stockOutLinesByPickOrderLineId, illById)
    }
    //Print Delivery Note
    @Transactional
    open fun printDeliveryNote(request: PrintDeliveryNoteRequest) {
        val t0 = System.nanoTime()
        val printer =
            printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

        val pdf = exportDeliveryNote(
            ExportDeliveryNoteRequest(
                doPickOrderId = request.doPickOrderId,
                numOfCarton = request.numOfCarton,
                isDraft = request.isDraft
            )
        )

        val jasperPrint = pdf["report"] as JasperPrint

        val tempPdfFile = File.createTempFile("print_job_", ".pdf")

        try {
            val t1 = System.nanoTime()
            JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath)
            val exportPdfToFileMs = (System.nanoTime() - t1) / 1_000_000.0

            val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty

            val t2 = System.nanoTime()
            printer.ip?.let { ip ->
                val port = printer.port ?: 9100
                val driver = A4PrintDriverRegistry.getDriver(printer.brand)
                driver.print(tempPdfFile, ip, port, printQty)
            }
            val printToPrinterMs = (System.nanoTime() - t2) / 1_000_000.0
            val totalMs = (System.nanoTime() - t0) / 1_000_000.0
            log.info(
                "DeliveryNote print-DN timing: exportPdfToFileMs={} printToPrinterMs={} totalMs={} doPickOrderId={} printerId={} printQty={} printerIp={}",
                exportPdfToFileMs,
                printToPrinterMs,
                totalMs,
                request.doPickOrderId,
                request.printerId,
                printQty,
                printer.ip ?: "null",
            )
            updateRecordCartonQty(request.doPickOrderId, request.numOfCarton)
        } finally {
            //tempPdfFile.delete()
        }

    }

    //Carton Labels
    open fun exportDNLabels(request: ExportDNLabelsRequest): Map<String, Any> {
        val context = buildDNLabelContext(request.doPickOrderId)
        val rows = buildDnLabelRows(
            startCarton = 1,
            endCarton = request.numOfCarton,
            totalCartonsOnShipment = request.numOfCarton,
        )

        return mapOf(
            "report" to PdfUtils.fillReport(context.cartonLabelTemplate, rows, context.params),
            "filename" to context.filename,
        )
    }

    open fun exportDNLabelsReprint(request: ExportDNLabelsReprintRequest): Map<String, Any> {
        validateCartonReprintRange(
            fromCarton = request.fromCarton,
            toCarton = request.toCarton,
            totalCartonsOnShipment = request.totalCartonsOnShipment,
        )

        val context = buildDNLabelContext(request.doPickOrderId)
        val rows = buildDnLabelRows(
            startCarton = request.fromCarton,
            endCarton = request.toCarton,
            totalCartonsOnShipment = request.totalCartonsOnShipment,
        )

        return mapOf(
            "report" to PdfUtils.fillReport(context.cartonLabelTemplate, rows, context.params),
            "filename" to "${context.filename}_reprint_${request.fromCarton}-${request.toCarton}",
        )
    }

     data class ParsedShopLabelForCartonLabel(
        val shopCode: String,
        val shopNameForLabel: String,
    )

     fun parseShopLabelForCartonLabel(rawInput: String): ParsedShopLabelForCartonLabel {
        // Input format: shopCode - shopName1-shopName2 (e.g. CT001 - 茶泰-啟田店)
        // Optional trailing note in parentheses on its own line, e.g. (午更落貨表:15:00到貨)
        val raw = rawInput.trim()

        val (shopCodePartRaw, restPart) = raw.split(" - ", limit = 2).let { parts ->
            (parts.getOrNull(0)?.trim().orEmpty()) to (parts.getOrNull(1)?.trim().orEmpty())
        }

        val trailingNoteRegex = Regex("\\s*(\\([^)]+\\))\\s*$")
        val trailingNoteMatch = trailingNoteRegex.find(restPart)
        val namePart = trailingNoteMatch
            ?.let { restPart.removeRange(it.range).trim() }
            ?: restPart
        val trailingNote = trailingNoteMatch?.groupValues?.get(1)?.trim().orEmpty()

        val (shopName1, shopName2) = namePart.split("-", limit = 2).let { parts ->
            (parts.getOrNull(0)?.trim().orEmpty()) to (parts.getOrNull(1)?.trim().orEmpty())
        }

        val shopNameLines = buildList {
            if (shopName1.isNotBlank()) add(shopName1)
            if (shopName2.isNotBlank()) add(shopName2)
            if (trailingNote.isNotBlank()) add(trailingNote)
        }

        return ParsedShopLabelForCartonLabel(
            shopCode = shopCodePartRaw.trim(),
            shopNameForLabel = shopNameLines.joinToString("\n"),
        )
    }

    fun formatTruckLaneCodeForCartonLabel(raw: String?): String {
        val trimmed = raw?.trim().orEmpty()
        if (trimmed.isEmpty()) return trimmed

        val withoutLanePrefix = if (trimmed.startsWith("車線-")) {
            trimmed.removePrefix("車線-")
        } else {
            trimmed
        }

        // 4F route board: P06B_Wed_區6_新界,九龍 → 區6_新界,九龍
        val pRouteDistrict = Regex("^P[^_]+_[^_]+_(.+)$").matchEntire(withoutLanePrefix)
        if (pRouteDistrict != null) {
            return pRouteDistrict.groupValues[1]
        }

        return withoutLanePrefix
    }


    //Print Carton Labels
    @Transactional
    open fun printDNLabels(request: PrintDNLabelsRequest) {
        val printer =
            printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

        val pdf = exportDNLabels(
            ExportDNLabelsRequest(
                doPickOrderId = request.doPickOrderId,
                numOfCarton = request.numOfCarton
            )
        )

        val jasperPrint = pdf["report"] as JasperPrint

        val tempPdfFile = File.createTempFile("print_job_", ".pdf")

        try {
            JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath)


            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
                    )
                }
            }
            updateRecordCartonQty(request.doPickOrderId, request.numOfCarton)

            println("Test PDF saved to: ${tempPdfFile.absolutePath}")

        } finally {
            //tempPdfFile.delete()
        }

    }

    @Transactional
    open fun printDNLabelsReprint(request: PrintDNLabelsReprintRequest) {
        validateCartonReprintRange(
            fromCarton = request.fromCarton,
            toCarton = request.toCarton,
            totalCartonsOnShipment = request.totalCartonsOnShipment,
        )

        val printer =
            printerService.findById(request.printerId) ?: throw java.util.NoSuchElementException("No such printer")

        val pdf = exportDNLabelsReprint(
            ExportDNLabelsReprintRequest(
                doPickOrderId = request.doPickOrderId,
                fromCarton = request.fromCarton,
                toCarton = request.toCarton,
                totalCartonsOnShipment = request.totalCartonsOnShipment,
            )
        )

        val jasperPrint = pdf["report"] as JasperPrint
        val tempPdfFile = File.createTempFile("print_job_", ".pdf")

        try {
            JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath)

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

            // Reprint should not update cartonQty persisted on the record.
            println("Reprint PDF saved to: ${tempPdfFile.absolutePath}")
        } finally {
            //tempPdfFile.delete()
        }
    }

    private data class DnLabelExportContext(
        val cartonLabelTemplate: net.sf.jasperreports.engine.JasperReport,
        val params: MutableMap<String, Any>,
        val filename: String,
    )

    private fun buildDNLabelContext(doPickOrderId: Long): DnLabelExportContext {
        val dnLabelsPdf = "DeliveryNote/DeliveryNoteCartonLabelsPDF.jrxml"
        val resource = ClassPathResource(dnLabelsPdf)
        if (!resource.exists()) {
            throw FileNotFoundException("Label file not found: $dnLabelsPdf")
        }

        val doPickOrderRecord = doPickOrderRecordRepository.findById(doPickOrderId).orElseThrow {
            NoSuchElementException("DoPickOrderRecord not found with ID: $doPickOrderId")
        }
        val doPickOrderLineRecords = doPickOrderLineRecordRepository.findByDoPickOrderId(doPickOrderRecord.recordId)
        val deliveryOrderIds = doPickOrderLineRecords.mapNotNull { it.doOrderId }.distinct()
        if (deliveryOrderIds.isEmpty()) {
            throw IllegalStateException("DoPickOrderRecord $doPickOrderId has no associated delivery orders")
        }

        val cartonLabelTemplate = JasperCompileManager.compileReport(resource.inputStream)
        val cartonLabelInfo = deliveryOrderIds.flatMap { deliveryOrderId ->
            deliveryOrderRepository.findDeliveryOrderInfoById(deliveryOrderId)
        }.toMutableList()
        if (cartonLabelInfo.isEmpty()) {
            throw IllegalStateException("No delivery order info found for DoPickOrderRecord $doPickOrderId")
        }

        val params = mutableMapOf<String, Any>()
        params["shopPurchaseOrderNo"] = "請查閲送貨單（採購單共${deliveryOrderIds.size}張）"
        params["deliveryNoteCode"] = doPickOrderRecord.deliveryNoteCode ?: ""
        params["shopAddress"] = cartonLabelInfo[0].shopAddress ?: ""

        val rawShopLabel = doPickOrderRecord.shopName ?: cartonLabelInfo[0].shopName ?: ""
        val parsedShopLabel = parseShopLabelForCartonLabel(rawShopLabel)
        params["shopCode"] = parsedShopLabel.shopCode
        params["shopName"] = parsedShopLabel.shopNameForLabel
        params["truckNo"] = formatTruckLaneCodeForCartonLabel(doPickOrderRecord.truckLanceCode)

        return DnLabelExportContext(
            cartonLabelTemplate = cartonLabelTemplate,
            params = params,
            filename = "${cartonLabelInfo[0].code}_carton_labels",
        )
    }

    private fun buildDnLabelRows(
        startCarton: Int,
        endCarton: Int,
        totalCartonsOnShipment: Int,
    ): List<DnCartonLabelSheetRow> {
        if (endCarton < startCarton) {
            return emptyList()
        }

        return (startCarton..endCarton).map { cartonIndex ->
            DnCartonLabelSheetRow(
                cartonIndex = cartonIndex,
                cartonTotal = totalCartonsOnShipment,
            )
        }
    }

    private fun validateCartonReprintRange(
        fromCarton: Int,
        toCarton: Int,
        totalCartonsOnShipment: Int,
    ) {
        require(totalCartonsOnShipment >= 1) {
            "totalCartonsOnShipment must be at least 1"
        }
        require(fromCarton >= 1) {
            "fromCarton must be at least 1"
        }
        require(toCarton >= fromCarton) {
            "toCarton must be greater than or equal to fromCarton"
        }
        require(toCarton <= totalCartonsOnShipment) {
            "toCarton must be less than or equal to totalCartonsOnShipment"
        }
    }

    private fun updateRecordCartonQty(doPickOrderRecordId: Long, cartonQty: Int) {
        if (cartonQty <= 0) return
        val record = doPickOrderRecordRepository.findById(doPickOrderRecordId).orElse(null) ?: return
        record.cartonQty = cartonQty
        doPickOrderRecordRepository.save(record)
    }

    @Transactional(rollbackFor = [Exception::class])
    open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult {
        println("    DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}")

        val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id)
            ?: throw NoSuchElementException("Delivery Order not found")

        //  检查状态，跳过已完成或已发布的DO
        if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) {
            throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release")
        }

        //  更新状态为released (使用RECEIVING表示已发布)
        deliveryOrder.apply {
            status = DeliveryOrderStatus.RECEIVING  // 使用RECEIVING表示已发布状态
        }
        deliveryOrderRepository.save(deliveryOrder)

        // 创建 pick order
        val pols = mapDeliveryOrderLinesForPickOrderRelease(deliveryOrder)
        val po = SavePickOrderRequest(
            doId = deliveryOrder.id,
            type = PickOrderType.DELIVERY_ORDER,
            targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(),
            pickOrderLine = pols
        )

        val createdPickOrder = pickOrderService.create(po)
        val consoCode = pickOrderService.assignConsoCode()
        val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null)

        if (pickOrderEntity != null) {
            pickOrderEntity.consoCode = consoCode
            pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED
            pickOrderRepository.saveAndFlush(pickOrderEntity)
              // 创建 stock out
              val stockOut = StockOut().apply {
                this.type = "do"
                this.consoPickOrderCode = consoCode
                this.status = StockOutStatus.PENDING.status
                this.handler = request.userId
            }
            val savedStockOut = stockOutRepository.saveAndFlush(stockOut)

            // 创建 suggestions 和 hold inventory
            val lines = pickOrderLineRepository.findAllByPickOrderId(pickOrderEntity.id!!)
            val suggestions = suggestedPickLotService.suggestionForPickOrderLines(
                SuggestedPickLotForPolRequest(pickOrderLines = lines)
            )

            val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList)
            val insufficientCount = suggestions.suggestedList.count { it.suggestedLotLine == null }
            if (insufficientCount > 0) {
                println(" WARNING: $insufficientCount items have insufficient stock (issues auto-created)")
            }
            val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(
                saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id }
            )

            saveSuggestedPickLots.forEach { lot ->
                if (lot.suggestedLotLine != null && lot.suggestedLotLine?.id != null && lot.suggestedLotLine!!.id!! > 0) {
                    val lineIndex = inventoryLotLines.indexOf(lot.suggestedLotLine)
                    if (lineIndex >= 0) {
                        inventoryLotLines[lineIndex].holdQty =
                            (inventoryLotLines[lineIndex].holdQty ?: BigDecimal.ZERO).plus(lot.qty ?: BigDecimal.ZERO)
                    }
                }
            }
            inventoryLotLineRepository.saveAll(inventoryLotLines)
            val pickOrderLineMap = lines.associateBy { it.id }
            val inventoryLotLineMap = inventoryLotLines.associateBy { it.id }
          
            // No-lot (insufficient stock) lines are created in suggestedPickLotService.saveAll → createStockOutLineForSuggestion; skip here to avoid duplicates.
            saveSuggestedPickLots.forEach { lot ->
                if (lot.suggestedLotLine == null) return@forEach
                val polId = lot.pickOrderLine?.id
                val illId = lot.suggestedLotLine?.id
                if (polId != null) {
                    val pickOrderLine = pickOrderLineMap[polId]
val inventoryLotLine = illId?.let { inventoryLotLineMap[it] }

                    if (pickOrderLine != null) {
                        val line = StockOutLine().apply {
                            this.stockOut = savedStockOut
                            this.pickOrderLine = pickOrderLine
                            this.inventoryLotLine = inventoryLotLine  // 可能为 null
                            this.item = pickOrderLine.item
                            //  修复：根据是否有 inventoryLotLine 设置状态
                            this.status = if (inventoryLotLine == null) {
                                StockOutLineStatus.PARTIALLY_COMPLETE.status  // 没有库存批次时使用 PARTIALLY_COMPLETE
                            } else {
                                StockOutLineStatus.PENDING.status  // 有正常库存批次时使用 PENDING
                            }
                            this.qty = 0.0
                            this.type = "NOR"
                        }
                        stockOutLineRepository.save(line)
                    }
                }
            }
        }

        //  新逻辑：根据 supplier code 决定楼层（清單來自 settings）
        val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
        val supplierCode = deliveryOrder.supplier?.code
        val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
        val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)

        println("    DEBUG: Floor calculation for DO ${deliveryOrder.id}")
        println("   - Supplier code: $supplierCode")
        println("   - Preferred floor: $preferredFloor")

        val truck = deliveryOrder.shop?.id?.let { shopId ->
            val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
            val preferredStoreId = when (preferredFloor) {
                "2F" -> "2F"
                "4F" -> "4F"
                "3F" -> "3F"
                else -> "2F"
            }

            val matchedTrucks = trucks.filter { it.storeId == preferredStoreId }

            if (matchedTrucks.isEmpty()) {
                null
            } else {
                if (preferredStoreId == "4F" && matchedTrucks.size > 1) {
                    deliveryOrder.estimatedArrivalDate?.let { estimatedArrivalDate ->
                        val targetDate = estimatedArrivalDate.toLocalDate()
                        val dayOfWeek = targetDate.dayOfWeek

                        val dayAbbr = when (dayOfWeek) {
                            java.time.DayOfWeek.MONDAY -> "Mon"
                            java.time.DayOfWeek.TUESDAY -> "Tue"
                            java.time.DayOfWeek.WEDNESDAY -> "Wed"
                            java.time.DayOfWeek.THURSDAY -> "Thu"
                            java.time.DayOfWeek.FRIDAY -> "Fri"
                            java.time.DayOfWeek.SATURDAY -> "Sat"
                            java.time.DayOfWeek.SUNDAY -> "Sun"
                        }

                        println("    DEBUG: DO ${deliveryOrder.id} - Target date: $targetDate ($dayAbbr), Shop: $shopId")
                        println("    DEBUG: Found ${matchedTrucks.size} matched 4F trucks")

                        val dayMatchedTrucks = matchedTrucks.filter {
                            it.truckLanceCode?.contains(dayAbbr, ignoreCase = true) == true
                        }

                        println("    DEBUG: Found ${dayMatchedTrucks.size} trucks matching $dayAbbr")
                        dayMatchedTrucks.forEach { t ->
                            println("   - Truck ID=${t.id}, Code=${t.truckLanceCode}, Time=${t.departureTime}")
                        }

                        if (dayMatchedTrucks.isNotEmpty()) {
                            val selected = dayMatchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
                            println("✅ DEBUG: Selected truck matching $dayAbbr - ID=${selected?.id}, Code=${selected?.truckLanceCode}")
                            selected
                        } else {
                            println("⚠️ WARNING: No truck matching $dayAbbr, using first available truck")
                            matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
                        }
                    } ?: run {
                        matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
                    }
                } else {
                    matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
                }
            }

        }
        val defaultTruckId = 5577L
        val effectiveTruck = truck ?: truckRepository.findById(defaultTruckId).orElse(null) 
        val usedDefaultTruck = (truck == null)
        if (effectiveTruck == null) {
            val errorMsg = "No matching truck for preferredFloor ($preferredFloor) and default truck $defaultTruckId not found. Skipping DO ${deliveryOrder.id}."
            println("⚠️ $errorMsg")
            throw IllegalStateException(errorMsg)
        }

        println(" DEBUG: Matched truck - ID=${effectiveTruck.id}, Store=${effectiveTruck.storeId}, Floor=$preferredFloor")

        return ReleaseDoResult(
            deliveryOrderId = deliveryOrder.id!!,
            deliveryOrderCode = deliveryOrder.code,
            pickOrderId = createdPickOrder.id!!,
            pickOrderCode = pickOrderEntity?.code,
            shopId = deliveryOrder.shop?.id,
            shopCode = deliveryOrder.shop?.code,
            shopName = deliveryOrder.shop?.name,
            estimatedArrivalDate = targetDate,
            preferredFloor = preferredFloor,
            truckId = effectiveTruck.id,
            truckDepartureTime = effectiveTruck.departureTime,
            truckLanceCode = effectiveTruck.truckLanceCode,
            loadingSequence = effectiveTruck.loadingSequence,
            usedDefaultTruck = usedDefaultTruck,
            isExtra = deliveryOrder.isExtra ?: false,

        )
    }

    /**
     * Workbench NO-HOLD release (V1):
     * - Create pick_order + stock_out header
     * - Do NOT create suggested_pick_lot / stock_out_line
     * - Do NOT update inventory_lot_line.holdQty
     *
     * Downstream should be handled by workbench batch release (no-hold suggestion + stock_out_line creation),
     * or on ticket assign when using [releaseDeliveryOrderWithoutTicketNoHoldV2].
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun releaseDeliveryOrderWithoutTicketNoHold(request: ReleaseDoRequest): ReleaseDoResult =
        releaseDeliveryOrderWithoutTicketNoHoldInternal(request, createStockOutHeader = true)

    /**
     * Workbench NO-HOLD release (V2): same as [releaseDeliveryOrderWithoutTicketNoHold] but **does not** create a
     * [StockOut] row. Suggested pick lots, stock out header, and stock out lines are created when the workbench
     * assigns the [delivery_order_pick_order] ([com.ffii.fpsms.modules.deliveryOrder.service.DoWorkbenchDopoAssignmentService]).
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun releaseDeliveryOrderWithoutTicketNoHoldV2(request: ReleaseDoRequest): ReleaseDoResult =
        releaseDeliveryOrderWithoutTicketNoHoldInternal(request, createStockOutHeader = false)

    private fun releaseDeliveryOrderWithoutTicketNoHoldInternal(
        request: ReleaseDoRequest,
        createStockOutHeader: Boolean,
    ): ReleaseDoResult {
        val tag = if (createStockOutHeader) "V1" else "V2"
        println("    DEBUG: Starting releaseDeliveryOrderWithoutTicketNoHold ($tag) for DO ID: ${request.id}")

        val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.id)
            ?: throw NoSuchElementException("Delivery Order not found")

        if (deliveryOrder.status == DeliveryOrderStatus.COMPLETED || deliveryOrder.status == DeliveryOrderStatus.RECEIVING) {
            throw IllegalStateException("Delivery Order ${deliveryOrder.id} is already ${deliveryOrder.status?.value}, skipping release")
        }

        // Mark released (same semantics as normal release)
        deliveryOrder.status = DeliveryOrderStatus.RECEIVING
        deliveryOrderRepository.save(deliveryOrder)

        // Create pick order (same as normal release)
        val pols = mapDeliveryOrderLinesForPickOrderRelease(deliveryOrder)
        val po = SavePickOrderRequest(
            doId = deliveryOrder.id,
            type = PickOrderType.DELIVERY_ORDER,
            targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now(),
            pickOrderLine = pols
        )

        val createdPickOrder = pickOrderService.create(po)
        val consoCode = pickOrderService.assignConsoCode()
        val pickOrderEntity = pickOrderRepository.findById(createdPickOrder.id!!).orElse(null)

        if (pickOrderEntity != null) {
            pickOrderEntity.consoCode = consoCode
            pickOrderEntity.status = com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.RELEASED
            pickOrderRepository.saveAndFlush(pickOrderEntity)

            if (createStockOutHeader) {
                // Create stock out header only; stock_out_line is created by workbench service (V1 batch) or on assign (V2)
                val stockOut = StockOut().apply {
                    this.type = "do"
                    this.consoPickOrderCode = consoCode
                    this.status = StockOutStatus.PENDING.status
                    this.handler = request.userId
                }
                stockOutRepository.saveAndFlush(stockOut)
            }
        }

        // Truck selection (reuse normal logic)
        val targetDate = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()
        val supplierCode = deliveryOrder.supplier?.code
        val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()
        val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)

        val truck = deliveryOrder.shop?.id?.let { shopId ->
            val trucks = truckRepository.findByShopIdAndDeletedFalse(shopId)
            val preferredStoreId = when (preferredFloor) {
                "2F" -> "2F"
                "4F" -> "4F"
                "3F" -> "3F"
                else -> "2F"
            }

            val matchedTrucks = trucks.filter { it.storeId == preferredStoreId }
            if (matchedTrucks.isEmpty()) {
                null
            } else {
                if (preferredStoreId == "4F" && matchedTrucks.size > 1) {
                    deliveryOrder.estimatedArrivalDate?.let { estimatedArrivalDate ->
                        val targetDate2 = estimatedArrivalDate.toLocalDate()
                        val dayAbbr = when (targetDate2.dayOfWeek) {
                            java.time.DayOfWeek.MONDAY -> "Mon"
                            java.time.DayOfWeek.TUESDAY -> "Tue"
                            java.time.DayOfWeek.WEDNESDAY -> "Wed"
                            java.time.DayOfWeek.THURSDAY -> "Thu"
                            java.time.DayOfWeek.FRIDAY -> "Fri"
                            java.time.DayOfWeek.SATURDAY -> "Sat"
                            java.time.DayOfWeek.SUNDAY -> "Sun"
                        }

                        val dayMatchedTrucks = matchedTrucks.filter {
                            it.truckLanceCode?.contains(dayAbbr, ignoreCase = true) == true
                        }

                        if (dayMatchedTrucks.isNotEmpty()) {
                            dayMatchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
                        } else {
                            matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
                        }
                    } ?: run {
                        matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
                    }
                } else {
                    matchedTrucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
                }
            }
        }

        val defaultTruckId = 5577L
        val effectiveTruck = truck ?: truckRepository.findById(defaultTruckId).orElse(null)
        val usedDefaultTruck = (truck == null)
        if (effectiveTruck == null) {
            val errorMsg = "No matching truck for preferredFloor ($preferredFloor) and default truck $defaultTruckId not found. Skipping DO ${deliveryOrder.id}."
            throw IllegalStateException(errorMsg)
        }

        return ReleaseDoResult(
            deliveryOrderId = deliveryOrder.id!!,
            deliveryOrderCode = deliveryOrder.code,
            pickOrderId = createdPickOrder.id!!,
            pickOrderCode = pickOrderEntity?.code,
            shopId = deliveryOrder.shop?.id,
            shopCode = deliveryOrder.shop?.code,
            shopName = deliveryOrder.shop?.name,
            estimatedArrivalDate = targetDate,
            preferredFloor = preferredFloor,
            truckId = effectiveTruck.id,
            truckDepartureTime = effectiveTruck.departureTime,
            truckLanceCode = effectiveTruck.truckLanceCode,
            loadingSequence = effectiveTruck.loadingSequence,
            usedDefaultTruck = usedDefaultTruck,
            isExtra = deliveryOrder.isExtra ?: false,
        )
    }

    private fun scanDoLiteSupplierFilteredWithLanePredicate(
        code: String?,
        shopName: String?,
        statusEnum: DeliveryOrderStatus?,
        etaStart: LocalDateTime?,
        etaEnd: LocalDateTime?,
        isExtra: Boolean?,
        allowedSupplierCodes: List<String>,
        lanePredicate: (String?) -> Boolean,
    ): List<DeliveryOrderInfoLiteDto> {
        val out = ArrayList<DeliveryOrderInfoLiteDto>()
        var dbPage = 0
        var rawScanned = 0
        while (rawScanned < 100_000) {
            val slice = deliveryOrderRepository.searchDoLitePageWithSupplierCodes(
                code = code,
                shopName = shopName,
                status = statusEnum,
                etaStart = etaStart,
                etaEnd = etaEnd,
                isExtra = isExtra,
                allowedSupplierCodes = allowedSupplierCodes,
                pageable = PageRequest.of(dbPage, 500),
            )
            if (slice.content.isEmpty()) break
            val dtos = buildResolvedTruckDtosForLiteRows(slice.content)
            for (dto in dtos) {
                if (lanePredicate(dto.truckLanceCode)) out.add(dto)
            }
            rawScanned += slice.content.size
            dbPage++
            if (!slice.hasNext()) break
        }
        return out
    }

    private fun buildResolvedTruckDtosForLiteRows(
        rows: List<DeliveryOrderInfoLite>,
    ): List<DeliveryOrderInfoLiteDto> {
        val ids = rows.mapNotNull { it.id }
        if (ids.isEmpty()) return emptyList()
        val deliveryOrdersMap = deliveryOrderRepository.findAllById(ids).associateBy { it.id }
        val (floorSuppliers2F, floorSuppliers4F) = loadDoFloorSupplierLists()

        val shopIdAndDatePairs = rows.mapNotNull { info ->
            val d = deliveryOrdersMap[info.id]
            val shopId = d?.shop?.id
            val eta = info.estimatedArrivalDate
            if (shopId != null && eta != null) {
                val targetDate = eta.toLocalDate()
                val dayAbbr = getDayOfWeekAbbr(targetDate)
                val supplierCode = d.supplier?.code
                val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
                Triple(shopId, preferredFloor, dayAbbr)
            } else {
                null
            }
        }.distinct()

        val truckCache = mutableMapOf<Triple<Long, String, String>, Truck?>()
        shopIdAndDatePairs.forEach { (shopId, preferredFloor, dayAbbr) ->
            truckCache[Triple(shopId, preferredFloor, dayAbbr)] =
                resolveTruckForShopFloorAndDay(shopId, preferredFloor, dayAbbr)
        }

        return rows.map { info ->
            val deliveryOrder = deliveryOrdersMap[info.id]
            val supplierCode = deliveryOrder?.supplier?.code
            val preferredFloor = preferredStoreFloorForSupplier(supplierCode, floorSuppliers2F, floorSuppliers4F)
            val shopId = deliveryOrder?.shop?.id
            val infoEta = info.estimatedArrivalDate
            val calculatedTruckLanceCode =
                if (deliveryOrder != null && shopId != null && infoEta != null) {
                    val targetDate = infoEta.toLocalDate()
                    val dayAbbr = getDayOfWeekAbbr(targetDate)
                    truckCache[Triple(shopId, preferredFloor, dayAbbr)]?.truckLanceCode
                } else {
                    null
                }
            DeliveryOrderInfoLiteDto(
                id = info.id,
                code = info.code,
                orderDate = info.orderDate,
                estimatedArrivalDate = info.estimatedArrivalDate,
                status = info.status,
                shopName = info.shopName,
                supplierName = info.supplierName,
                shopAddress = info.shopAddress,
                truckLanceCode = calculatedTruckLanceCode,
                isExtra = deliveryOrder?.isExtra ?: info.isExtra,
            )
        }
    }

    /**
     * 依店鋪 + 揀貨樓層解析當日應顯示之車線。
     * - **2F**（P07/P06D/P06Y）：`TruckLanceCode` 多為 `車線-…` 且不含星期縮寫，不依 `LIKE '%Mon%'` 篩選；取該店該樓層未刪除車輛中出發時間最早者。
     * - **4F**（P06B）：維持以星期縮寫篩選 [TruckRepository.findByShopIdAndStoreIdAndDayOfWeek]；無命中時再退回同樓層最早出發。
     */
    private fun resolveTruckForShopFloorAndDay(
        shopId: Long,
        preferredFloor: String,
        dayAbbr: String,
    ): Truck? {
        if (preferredFloor == "2F") {
            return truckRepository.findByShopIdAndDeletedFalse(shopId)
                .filter { it.storeId == preferredFloor }
                .minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
        }
        val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, preferredFloor, dayAbbr)
        return if (trucks.isEmpty()) {
            truckRepository.findByShopIdAndDeletedFalse(shopId)
                .filter { it.storeId == preferredFloor }
                .minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
        } else {
            trucks.minByOrNull { it.departureTime ?: LocalTime.of(23, 59, 59) }
        }
    }

    private fun getDayOfWeekAbbr(date: LocalDate): String =
        when (date.dayOfWeek) {
            java.time.DayOfWeek.MONDAY -> "Mon"
            java.time.DayOfWeek.TUESDAY -> "Tue"
            java.time.DayOfWeek.WEDNESDAY -> "Wed"
            java.time.DayOfWeek.THURSDAY -> "Thu"
            java.time.DayOfWeek.FRIDAY -> "Fri"
            java.time.DayOfWeek.SATURDAY -> "Sat"
            java.time.DayOfWeek.SUNDAY -> "Sun"
        }

    open fun check4FTruckAvailabilityForDoList(doIds: List<Long>): Check4FTruckBatchResponse {
        val problems = mutableListOf<ProblemDoDto>()

        doIds.forEach { doId ->
            val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId) ?: return@forEach
            val shopId = deliveryOrder.shop?.id ?: return@forEach
            val eta = deliveryOrder.estimatedArrivalDate?.toLocalDate() ?: return@forEach

            // 1) Check if this DO is a 4F order (your “all items on 4F, no 2F” rule)
            val sql = """
            SELECT 
                COUNT(DISTINCT CASE WHEN i.store_id = '4F' THEN dol.itemId END) AS count_4f,
                COUNT(DISTINCT CASE WHEN i.store_id = '2F' THEN dol.itemId END) AS count_2f,
                COUNT(DISTINCT dol.itemId) AS total_items
            FROM fpsmsdb.delivery_order_line dol
            INNER JOIN fpsmsdb.items i ON i.id = dol.itemId AND i.deleted = 0
            WHERE dol.deliveryOrderId = :doId AND dol.deleted = 0
        """.trimIndent()

            val res = jdbcDao.queryForList(sql, mapOf("doId" to doId)).firstOrNull() ?: return@forEach
            val count4F = (res["count_4f"] as? Number)?.toInt() ?: 0
            val count2F = (res["count_2f"] as? Number)?.toInt() ?: 0
            val totalItems = (res["total_items"] as? Number)?.toInt() ?: 0

            val is4FOrder = (totalItems > 0 && count4F == totalItems && count2F == 0)
            if (!is4FOrder) {
                // Not a 4F order → no problem, skip
                return@forEach
            }

            // 2) Find 4F trucks for that shop on that weekday
            val dayAbbr = getDayOfWeekAbbr(eta)
            val trucks = truckRepository.findByShopIdAndStoreIdAndDayOfWeek(shopId, "4F", dayAbbr)

            if (trucks.isEmpty()) {
                // Problem DO: 4F order but no matching 4F truck that day
                val availableTrucksAll4F = truckRepository.findByShopIdAndDeletedFalse(shopId)
                    .filter { it.storeId == "4F" }

                val truckDtos = availableTrucksAll4F.map { t ->
                    TruckInfoDto(
                        id = t.id,
                        truckLanceCode = t.truckLanceCode,
                        departureTime = t.departureTime?.toString(),
                        storeId = t.storeId,
                        shopCode = t.shopCode,
                        shopName = t.shopName
                    )
                }

                problems += ProblemDoDto(
                    deliveryOrderId = deliveryOrder.id!!,
                    deliveryOrderCode = deliveryOrder.code,
                    targetDate = eta,
                    availableTrucks = truckDtos
                )
            }
        }

        return Check4FTruckBatchResponse(
            hasProblem = problems.isNotEmpty(),
            problems = problems
        )
    }

    private companion object {
        private val log: Logger = LoggerFactory.getLogger(DeliveryOrderService::class.java)

        private const val DELIVERYNOTE_JRXML = "DeliveryNote/DeliveryNotePDF.jrxml"

        /** Compile once; avoids per-request JRXML recompilation (shared by download + print). */
        private val deliveryNoteJasperTemplate: JasperReport by lazy {
            val resource = ClassPathResource(DELIVERYNOTE_JRXML)
            if (!resource.exists()) {
                throw FileNotFoundException("Report file not found: $DELIVERYNOTE_JRXML")
            }
            resource.inputStream.use { inputStream -> JasperCompileManager.compileReport(inputStream) }
        }
    }
}
