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

import com.ffii.core.support.JdbcDao
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.entity.DoReplenishment
import com.ffii.fpsms.modules.deliveryOrder.entity.DoReplenishmentRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrder
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderPickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.DoReplenishmentResponse
import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoResult
import com.ffii.fpsms.modules.deliveryOrder.web.models.SubmitDoReplenishmentLineRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.SubmitDoReplenishmentRequest
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.master.entity.UomConversionRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.service.WorkbenchStockOutLinePickProgress
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDate
import java.time.format.DateTimeFormatter

@Service
open class DoReplenishmentService(
    private val doReplenishmentRepository: DoReplenishmentRepository,
    private val deliveryOrderRepository: DeliveryOrderRepository,
    private val deliveryOrderPickOrderRepository: DeliveryOrderPickOrderRepository,
    private val deliveryOrderLineRepository: DeliveryOrderLineRepository,
    private val doPickOrderRepository: DoPickOrderRepository,
    private val doPickOrderLineRepository: DoPickOrderLineRepository,
    private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
    private val stockOutLIneRepository: StockOutLIneRepository,
    private val uomConversionRepository: UomConversionRepository,
    private val pickOrderRepository: PickOrderRepository,
    private val pickOrderLineRepository: PickOrderLineRepository,
    private val itemsRepository: ItemsRepository,
    private val jdbcDao: JdbcDao,
) {

    @Transactional
    open fun submit(request: SubmitDoReplenishmentRequest): List<DoReplenishmentResponse> {
        if (request.lines.isEmpty()) {
            throw IllegalArgumentException("No replenishment lines to submit")
        }
        val nextSeqByDate = mutableMapOf<LocalDate, Int>()
        val created = mutableListOf<DoReplenishment>()
        val mergedLines = mergeSubmitLines(request.lines)

        for (lineReq in mergedLines) {
            val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(lineReq.sourceDoId)
                ?: throw IllegalArgumentException("Source delivery order not found: ${lineReq.sourceDoId}")
            if (deliveryOrder.status != DeliveryOrderStatus.COMPLETED) {
                throw IllegalArgumentException("Source delivery order must be completed: ${deliveryOrder.code}")
            }

            val doLine = deliveryOrderLineRepository.findById(lineReq.sourceDoLineId).orElse(null)
                ?: throw IllegalArgumentException("Source delivery order line not found: ${lineReq.sourceDoLineId}")
            if (doLine.deleted == true || doLine.deliveryOrder?.id != lineReq.sourceDoId) {
                throw IllegalArgumentException("Source line does not belong to delivery order ${lineReq.sourceDoId}")
            }

            val existingPending = doReplenishmentRepository.findFirstBySourceDoLineIdAndStatusAndDeletedIsFalse(
                lineReq.sourceDoLineId,
                DoReplenishment.STATUS_PENDING,
            )
            if (existingPending != null) {
                existingPending.replenishQty =
                    (existingPending.replenishQty ?: BigDecimal.ZERO).add(lineReq.replenishQty)
                if (existingPending.truckLaneCode.isNullOrBlank()) {
                    existingPending.truckLaneCode =
                        resolveSourceDoTruckLaneCode(deliveryOrder, lineReq.truckLaneCode)
                }
                lineReq.reason?.trim()?.takeIf { it.isNotEmpty() }?.let { existingPending.reason = it }
                created += doReplenishmentRepository.save(existingPending)
                continue
            }

            val m18DataLog = doLine.m18DataLog
                ?: throw IllegalArgumentException("Source line missing M18 data log")
            val m18Id = m18DataLog.m18Id
                ?: throw IllegalArgumentException("Source line missing M18 id")
            val item = doLine.item
                ?: throw IllegalArgumentException("Source line missing item")

            val seq = nextSeqByDate.getOrPut(lineReq.deliveryDate) {
                nextCodeSequence(lineReq.deliveryDate)
            }
            nextSeqByDate[lineReq.deliveryDate] = seq + 1
            val code = formatReplenishmentCode(lineReq.deliveryDate, seq)

            val shop = deliveryOrder.shop
            val entity = DoReplenishment().apply {
                this.code = code
                deliveryDate = lineReq.deliveryDate
                sourceDoId = lineReq.sourceDoId
                sourceDoCode = deliveryOrder.code
                sourceDoLineId = lineReq.sourceDoLineId
                sourceM18DataLogId = m18DataLog.id
                sourceM18Id = m18Id
                itemId = item.id
                itemNo = doLine.itemNo ?: item.code
                itemName = item.name
                replenishQty = lineReq.replenishQty
                uomId = doLine.uom?.id
                shopId = shop?.id
                shopCode = shop?.code
                shopName = shop?.name
                truckLaneCode = resolveSourceDoTruckLaneCode(deliveryOrder, lineReq.truckLaneCode)
                status = DoReplenishment.STATUS_PENDING
                reason = lineReq.reason?.trim()?.takeIf { it.isNotEmpty() }
            }
            created += doReplenishmentRepository.save(entity)
        }

        return toResponses(created)
    }

    open fun list(deliveryDate: LocalDate?, status: String?): List<DoReplenishmentResponse> {
        val normalizedStatus = status?.trim()?.takeIf { it.isNotEmpty() && it != "all" }
        val rows = doReplenishmentRepository.search(deliveryDate, normalizedStatus)
        return toResponses(rows)
    }

    open fun listForBatchRelease(
        truckLaneCode: String?,
        shopName: String?,
    ): List<DoReplenishmentResponse> {
        val truck = truckLaneCode?.trim()?.takeIf { it.isNotEmpty() }
        val shop = shopName?.trim()?.takeIf { it.isNotEmpty() }
        if (truck == null && shop == null) {
            return emptyList()
        }
        val rows = doReplenishmentRepository.searchForBatchRelease(
            status = DoReplenishment.STATUS_PENDING,
            truckLaneCode = truck,
            shopName = shop,
        )
        return toResponses(rows)
    }

    open fun findReplenishmentsByTargetDoIds(targetDoIds: Collection<Long>): List<DoReplenishment> {
        if (targetDoIds.isEmpty()) return emptyList()
        return doReplenishmentRepository.findByTargetDoIdInAndDeletedIsFalse(targetDoIds)
    }

    data class ReplenishPdfIndex(
        private val targetDoItemKeys: Set<Pair<Long, Long>>,
        private val pickOrderLineIds: Set<Long>,
    ) {
        fun matches(deliveryOrderId: Long, itemId: Long?, pickOrderLineId: Long?): Boolean {
            if (pickOrderLineId != null && pickOrderLineId in pickOrderLineIds) return true
            val resolvedItemId = itemId ?: return false
            return deliveryOrderId to resolvedItemId in targetDoItemKeys
        }

        companion object {
            val EMPTY = ReplenishPdfIndex(emptySet(), emptySet())
        }
    }

    open fun buildReplenishPdfIndex(deliveryOrderIds: Collection<Long>): ReplenishPdfIndex {
        if (deliveryOrderIds.isEmpty()) return ReplenishPdfIndex.EMPTY
        val records = doReplenishmentRepository.findByTargetDoIdInAndDeletedIsFalse(deliveryOrderIds)
            .filter { it.pickOrderLineId != null }
        if (records.isEmpty()) return ReplenishPdfIndex.EMPTY
        return ReplenishPdfIndex(
            targetDoItemKeys = records.mapNotNull { row ->
                val targetDoId = row.targetDoId
                val itemId = row.itemId
                if (targetDoId != null && itemId != null) targetDoId to itemId else null
            }.toSet(),
            pickOrderLineIds = records.mapNotNull { it.pickOrderLineId }.toSet(),
        )
    }

    /** Replenishment POL rows with no matching DOL on the target DO — append as extra DN lines. */
    open fun replenishmentsWithoutDeliveryOrderLine(
        deliveryOrderIds: Collection<Long>,
        exportLines: List<DeliveryOrderService.DeliveryNoteExportLine>,
    ): List<DoReplenishment> {
        if (deliveryOrderIds.isEmpty()) return emptyList()
        val dolItemKeys = exportLines.mapNotNull { row ->
            row.line.itemId?.let { row.deliveryOrderId to it }
        }.toSet()
        return doReplenishmentRepository.findByTargetDoIdInAndDeletedIsFalse(deliveryOrderIds)
            .filter { replenishment ->
                val polId = replenishment.pickOrderLineId ?: return@filter false
                val targetDoId = replenishment.targetDoId ?: return@filter false
                val itemId = replenishment.itemId ?: return@filter false
                polId > 0 && (targetDoId to itemId) !in dolItemKeys
            }
    }

    @Transactional
    open fun completeByPickOrderLineId(pickOrderLineId: Long) {
        val row = doReplenishmentRepository.findFirstByPickOrderLineIdAndStatusAndDeletedIsFalse(
            pickOrderLineId,
            DoReplenishment.STATUS_PROCESSING,
        ) ?: return
        row.status = DoReplenishment.STATUS_COMPLETED
        doReplenishmentRepository.save(row)
    }

    @Transactional
    open fun completeProcessingByDeliveryOrderPickOrderId(deliveryOrderPickOrderId: Long) {
        if (deliveryOrderPickOrderId <= 0L) return
        val rows = doReplenishmentRepository.findByDeliveryOrderPickOrderIdAndStatusAndDeletedIsFalse(
            deliveryOrderPickOrderId,
            DoReplenishment.STATUS_PROCESSING,
        )
        for (row in rows) {
            row.pickOrderLineId?.let { completeByPickOrderLineId(it) }
        }
    }

    /**
     * After workbench batch release links pick orders to a ticket, create replenishment POL rows
     * (no DOL), assign target DO / ticket FKs, and move status pending → processing.
     *
     * @return pick order ids that received new replenishment lines (for V1 downstream rebuild)
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun releasePendingReplenishmentsForWorkbenchBatch(
        releasedResults: List<ReleaseDoResult>,
    ): Set<Long> {
        if (releasedResults.isEmpty()) return emptySet()

        val pending = findPendingReplenishmentsForReleasedResults(releasedResults)
        if (pending.isEmpty()) return emptySet()

        val affectedPickOrderIds = mutableSetOf<Long>()
        for (replenishment in pending) {
            val matchingResults = releasedResults.filter { replenishmentMatchesResult(replenishment, it) }
            if (matchingResults.isEmpty()) continue

            val targetResult = matchingResults.minByOrNull { it.deliveryOrderId }
                ?: continue
            val dopoId = resolveDeliveryOrderPickOrderId(targetResult.pickOrderId)
                ?: continue
            val ticketPickOrderIds = pickOrderRepository.findIdsByDeliveryOrderPickOrderId(dopoId)
            if (ticketPickOrderIds.isEmpty()) continue

            val itemId = replenishment.itemId ?: continue
            val targetPickOrderId = resolveTargetPickOrderId(ticketPickOrderIds, itemId)
            val pickOrder = pickOrderRepository.findById(targetPickOrderId).orElse(null) ?: continue
            val item = itemsRepository.findById(itemId).orElse(null) ?: continue
            val uom = replenishment.uomId?.let { uomConversionRepository.findById(it).orElse(null) }
                ?: continue

            val pol = PickOrderLine().apply {
                this.pickOrder = pickOrder
                this.item = item
                this.qty = replenishment.replenishQty
                this.uom = uom
                this.status = PickOrderLineStatus.PENDING
            }
            val savedPol = pickOrderLineRepository.save(pol)

            pickOrder.totalLines = (pickOrder.totalLines ?: 0) + 1
            pickOrderRepository.save(pickOrder)

            replenishment.targetDoId = targetResult.deliveryOrderId
            replenishment.targetDoCode = targetResult.deliveryOrderCode
            replenishment.pickOrderLineId = savedPol.id
            replenishment.deliveryOrderPickOrderId = dopoId
            replenishment.status = DoReplenishment.STATUS_PROCESSING
            doReplenishmentRepository.save(replenishment)

            affectedPickOrderIds += targetPickOrderId
        }

        return affectedPickOrderIds
    }

    private fun findPendingReplenishmentsForReleasedResults(
        releasedResults: List<ReleaseDoResult>,
    ): List<DoReplenishment> {
        val pairs = releasedResults
            .map { result ->
                (result.shopName?.trim()?.takeIf { it.isNotEmpty() }
                    ?: result.shopCode?.trim()?.takeIf { it.isNotEmpty() }
                    ?: "") to (result.truckLanceCode?.trim()?.takeIf { it.isNotEmpty() } ?: "")
            }
            .distinct()
            .filter { (shop, truck) -> shop.isNotEmpty() || truck.isNotEmpty() }

        if (pairs.isEmpty()) return emptyList()

        val byId = linkedMapOf<Long, DoReplenishment>()
        for ((shop, truck) in pairs) {
            val rows = doReplenishmentRepository.searchForBatchRelease(
                status = DoReplenishment.STATUS_PENDING,
                truckLaneCode = truck.takeIf { it.isNotEmpty() },
                shopName = shop.takeIf { it.isNotEmpty() },
            )
            for (row in rows) {
                val id = row.id ?: continue
                if (releasedResults.any { replenishmentMatchesResult(row, it) }) {
                    byId[id] = row
                }
            }
        }
        return byId.values.toList()
    }

    private fun replenishmentMatchesResult(
        replenishment: DoReplenishment,
        result: ReleaseDoResult,
    ): Boolean {
        val doTruck = normalizeText(result.truckLanceCode)
        val recordTruck = normalizeText(replenishment.truckLaneCode)
        if (doTruck.isNotEmpty()) {
            if (recordTruck.isEmpty() || recordTruck != doTruck) return false
        }

        val doShopToken = shopTokenFromResult(result)
        if (doShopToken.isEmpty()) return false

        val recordShopCode = normalizeText(replenishment.shopCode)
        val recordShopName = normalizeText(replenishment.shopName)
        return recordShopCode == doShopToken ||
            recordShopName.startsWith(doShopToken) ||
            (recordShopCode.isNotEmpty() && doShopToken.startsWith(recordShopCode)) ||
            (recordShopCode.isNotEmpty() && recordShopCode.startsWith(doShopToken))
    }

    private fun shopTokenFromResult(result: ReleaseDoResult): String {
        val raw = result.shopCode?.trim()?.takeIf { it.isNotEmpty() }
            ?: result.shopName?.trim()
            ?: ""
        if (raw.isEmpty()) return ""
        return normalizeText(raw.split(" - ").firstOrNull() ?: raw)
    }

    private fun normalizeText(value: String?): String = value?.trim()?.lowercase() ?: ""

    private fun resolveDeliveryOrderPickOrderId(pickOrderId: Long): Long? {
        return jdbcDao.queryForList(
            """
            SELECT deliveryOrderPickOrderId AS dopoId
            FROM fpsmsdb.pick_order
            WHERE id = :pickOrderId AND deleted = 0
            """.trimIndent(),
            mapOf("pickOrderId" to pickOrderId),
        ).firstOrNull()
            ?.get("dopoId")
            ?.let { (it as Number).toLong() }
    }

    /** Prefer pick_order that already has the same item on this ticket; else smallest pick_order.id. */
    private fun resolveTargetPickOrderId(ticketPickOrderIds: List<Long>, itemId: Long): Long {
        val sortedIds = ticketPickOrderIds.sorted()
        val lines = pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(sortedIds)
        val pickOrderWithItem = lines
            .filter { it.item?.id == itemId }
            .mapNotNull { it.pickOrder?.id }
            .minOrNull()
        return pickOrderWithItem ?: sortedIds.first()
    }

    private fun nextCodeSequence(deliveryDate: LocalDate): Int {
        val prefix = codePrefix(deliveryDate)
        val suffixPattern = Regex("^${Regex.escape(prefix)}(\\d+)$")
        var maxSeq = 0
        for (code in doReplenishmentRepository.findCodesByPrefix(prefix)) {
            suffixPattern.find(code)?.groupValues?.getOrNull(1)?.toIntOrNull()?.let { n ->
                if (n > maxSeq) maxSeq = n
            }
        }
        return maxSeq + 1
    }

    private fun formatReplenishmentCode(deliveryDate: LocalDate, sequence: Int): String {
        return "${codePrefix(deliveryDate)}${sequence.toString().padStart(3, '0')}"
    }

    private fun codePrefix(deliveryDate: LocalDate): String {
        val ymd = deliveryDate.format(DateTimeFormatter.BASIC_ISO_DATE)
        return "RP-$ymd-"
    }

    /** 同一批次內相同來源行合併補貨數量。 */
    private fun mergeSubmitLines(
        lines: List<SubmitDoReplenishmentLineRequest>,
    ): List<SubmitDoReplenishmentLineRequest> {
        if (lines.size <= 1) return lines
        val merged = linkedMapOf<String, SubmitDoReplenishmentLineRequest>()
        for (line in lines) {
            val key = "${line.sourceDoId}:${line.sourceDoLineId}"
            val existing = merged[key]
            if (existing == null) {
                merged[key] = line
            } else {
                merged[key] = existing.copy(
                    replenishQty = existing.replenishQty.add(line.replenishQty),
                    truckLaneCode = existing.truckLaneCode?.takeIf { it.isNotBlank() } ?: line.truckLaneCode,
                    reason = existing.reason?.takeIf { it.isNotBlank() } ?: line.reason,
                )
            }
        }
        return merged.values.toList()
    }

    /** 來源 DO 車線：優先 do_pick_order / do_pick_order_record，其次請求帶入值。 */
    private fun resolveSourceDoTruckLaneCode(
        deliveryOrder: DeliveryOrder,
        requestTruckLaneCode: String?,
    ): String? {
        val sourceDoId = deliveryOrder.id ?: return requestTruckLaneCode?.trim()?.takeIf { it.isNotEmpty() }

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

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

        return requestTruckLaneCode?.trim()?.takeIf { it.isNotEmpty() }
    }

    /**
     * Actual shipped qty per item on a completed source DO: sum of [stock_out_line.qty]
     * for the linked pick order line. Falls back to [fallbackQtyByItemId] when no pick link exists
     * (same rule as delivery note PDF).
     */
    open fun resolveActualShippedQtyForDeliveryOrder(
        doId: Long,
        fallbackQtyByItemId: Map<Long, BigDecimal> = emptyMap(),
    ): Map<Long, BigDecimal> {
        val keys = fallbackQtyByItemId.keys.map { doId to it }
        if (keys.isEmpty()) {
            return emptyMap()
        }
        return resolveActualShippedQtyBySourceKeys(keys, keys.associateWith { fallbackQtyByItemId[it.second]!! })
            .mapKeys { it.key.second }
    }

    private fun resolveActualShippedQtyBySourceKeys(
        keys: List<Pair<Long, Long>>,
        fallbackQtyByKey: Map<Pair<Long, Long>, BigDecimal>,
    ): Map<Pair<Long, Long>, BigDecimal> {
        if (keys.isEmpty()) {
            return emptyMap()
        }

        val doIds = keys.map { it.first }.distinct()
        val pickOrderIdByDoId = doIds.mapNotNull { doId ->
            resolvePickOrderIdForDo(doId)?.let { doId to it }
        }.toMap()

        val pickOrderIds = pickOrderIdByDoId.values.distinct()
        val pickOrderLines = if (pickOrderIds.isEmpty()) {
            emptyList()
        } else {
            pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(pickOrderIds)
        }
        val pickOrderLinesByPoId = pickOrderLines.groupBy { it.pickOrder?.id }

        val polIds = pickOrderLines.mapNotNull { it.id }
        val stockOutQtyByPolId = if (polIds.isEmpty()) {
            emptyMap()
        } else {
            stockOutLIneRepository.findAllByPickOrderLineIdInAndDeletedFalse(polIds)
                .groupBy { it.pickOrderLine?.id }
                .mapValues { (_, lines) ->
                    lines.fold(BigDecimal.ZERO) { acc, line ->
                        acc.add(BigDecimal.valueOf(line.qty ?: 0.0))
                    }
                }
        }

        return keys.associateWith { (doId, itemId) ->
            val pickOrderId = pickOrderIdByDoId[doId]
            val polId = pickOrderId?.let { poId ->
                pickOrderLinesByPoId[poId]?.firstOrNull { it.item?.id == itemId }?.id
            }
            if (polId != null) {
                stockOutQtyByPolId[polId] ?: BigDecimal.ZERO
            } else {
                fallbackQtyByKey[doId to itemId] ?: BigDecimal.ZERO
            }
        }
    }

    private fun resolvePickOrderIdForDo(doId: Long): Long? {
        pickOrderRepository.findByDeliveryOrderId(doId).firstOrNull()?.id?.let { return it }
        return doPickOrderLineRepository.findByDoOrderIdAndDeletedFalse(doId)
            .mapNotNull { it.pickOrderId }
            .firstOrNull()
    }

    private fun toResponses(entities: List<DoReplenishment>): List<DoReplenishmentResponse> {
        val uomIds = entities.mapNotNull { it.uomId }.distinct()
        val shortUomById = if (uomIds.isEmpty()) {
            emptyMap()
        } else {
            uomConversionRepository.findAllById(uomIds).associate { uom ->
                uom.id!! to (uom.udfShortDesc?.takeIf { it.isNotBlank() } ?: uom.code)
            }
        }

        val sourceLineIds = entities.mapNotNull { it.sourceDoLineId }.distinct()
        val sourceLineQtyById = if (sourceLineIds.isEmpty()) {
            emptyMap()
        } else {
            deliveryOrderLineRepository.findAllById(sourceLineIds).associate { line ->
                line.id!! to line.qty
            }
        }

        val dopoIds = entities.mapNotNull { it.deliveryOrderPickOrderId }.distinct()
        val dopoById = if (dopoIds.isEmpty()) {
            emptyMap()
        } else {
            deliveryOrderPickOrderRepository.findAllById(dopoIds).associateBy { it.id!! }
        }

        val sourceKeys = entities.map { it.sourceDoId!! to it.itemId!! }.distinct()
        val fallbackQtyBySourceKey = entities.associate { row ->
            (row.sourceDoId!! to row.itemId!!) to (
                row.sourceDoLineId?.let { sourceLineQtyById[it] } ?: BigDecimal.ZERO
                )
        }
        val actualShippedQtyBySourceKey = resolveActualShippedQtyBySourceKeys(
            keys = sourceKeys,
            fallbackQtyByKey = fallbackQtyBySourceKey,
        )

        val polIds = entities.mapNotNull { it.pickOrderLineId }.distinct()
        val stockOutLinesByPolId = if (polIds.isEmpty()) {
            emptyMap()
        } else {
            stockOutLIneRepository.findAllInfoByPickOrderLineIdInAndDeletedFalse(polIds)
                .groupBy { it.pickOrderLineId }
        }

        return entities.map { row ->
            DoReplenishmentResponse(
                id = row.id!!,
                code = row.code!!,
                deliveryDate = row.deliveryDate!!,
                sourceDoId = row.sourceDoId!!,
                sourceDoCode = row.sourceDoCode,
                sourceDoLineId = row.sourceDoLineId!!,
                itemId = row.itemId!!,
                itemNo = row.itemNo,
                itemName = row.itemName,
                originalQty = actualShippedQtyBySourceKey[row.sourceDoId!! to row.itemId!!],
                actualPickQty = row.pickOrderLineId?.let { polId ->
                    WorkbenchStockOutLinePickProgress.sumPickedQty(
                        stockOutLinesByPolId[polId].orEmpty(),
                    )
                },
                replenishQty = row.replenishQty!!,
                shortUom = row.uomId?.let { shortUomById[it] },
                shopCode = row.shopCode,
                shopName = row.shopName,
                truckLaneCode = row.truckLaneCode,
                targetDoId = row.targetDoId,
                targetDoCode = row.targetDoCode,
                targetDoEstimatedArrivalDate = row.deliveryOrderPickOrderId?.let { dopoById[it]?.requiredDeliveryDate },
                pickOrderLineId = row.pickOrderLineId,
                deliveryOrderPickOrderId = row.deliveryOrderPickOrderId,
                relateTicketNo = row.deliveryOrderPickOrderId?.let { dopoById[it]?.ticketNo },
                status = row.status,
                reason = row.reason,
                created = row.created,
            )
        }
    }
}
