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

import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
import com.ffii.fpsms.modules.stock.entity.SuggestedPickLot
import com.ffii.fpsms.modules.stock.entity.StockOutLine
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus
import com.ffii.fpsms.modules.stock.enums.SuggestedPickLotType
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDate

data class WorkbenchSuggestionSummary(
    val pickOrderId: Long,
    val created: Int,
    val skippedExisting: Int,
)

@Service
open class SuggestedPickLotWorkbenchService(
    private val pickOrderLineRepository: PickOrderLineRepository,
    private val inventoryLotLineRepository: InventoryLotLineRepository,
    private val stockOutLIneRepository: StockOutLIneRepository,
    private val suggestPickLotRepository: SuggestPickLotRepository,
) {

    private val log = LoggerFactory.getLogger(SuggestedPickLotWorkbenchService::class.java)

    /**
     * Historical SPL rows: linked to a [StockOutLine] that is already **completed** — never soft-deleted or
     * repointed by rebuild (已揀 segment). Remaining work uses non-frozen SPL rows only.
     */
    private fun isSplFrozen(spl: SuggestedPickLot): Boolean {
        val sol = spl.stockOutLine ?: return false
        val st = sol.status?.trim()?.lowercase() ?: return false
        return st == "completed" || st.equals(StockOutLineStatus.COMPLETE.status, ignoreCase = true)
    }

    /**
     * After [StockOutLine] is created from workbench ensure, bind 1:1 SPL ↔ SOL (剩餘 / pending segment).
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun linkSplToStockOutLineWhenSolCreated(sol: StockOutLine) {
        val pol = sol.pickOrderLine ?: return
        val polId = pol.id ?: return
        val illId = sol.inventoryLotLine?.id
        val candidates = suggestPickLotRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
            .asSequence()
            .filter { !isSplFrozen(it) }
            .filter {
                val sid = it.suggestedLotLine?.id
                when {
                    illId == null -> sid == null
                    else -> sid == illId
                }
            }
            .toList()
        val spl = candidates.filter { it.stockOutLine == null }.maxByOrNull { it.id ?: 0L }
            ?: candidates.maxByOrNull { it.id ?: 0L }
            ?: return
        spl.stockOutLine = sol
        suggestPickLotRepository.save(spl)
    }

    /**
     * After workbench scan-pick updates [StockOutLine] qty/status, bind SPL and set **qty** to match SOL (已揀 + 進行中).
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun linkSplToStockOutLineAfterWorkbenchPick(stockOutLineId: Long) {
        val sol = stockOutLIneRepository.findById(stockOutLineId).orElse(null) ?: return
        val pol = sol.pickOrderLine ?: return
        val polId = pol.id ?: return
        val ill = sol.inventoryLotLine
        val illId = ill?.id
        val qtyBd = BigDecimal.valueOf(sol.qty ?: 0.0)

        var spl = suggestPickLotRepository.findFirstByStockOutLineId(stockOutLineId)
        if (spl == null) {
            spl = suggestPickLotRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
                .asSequence()
                .filter { !isSplFrozen(it) }
                .filter {
                    val sid = it.suggestedLotLine?.id
                    when {
                        illId == null -> sid == null
                        else -> sid == illId
                    }
                }
                .maxByOrNull { it.id ?: 0L }
        }
        if (spl != null) {
            spl.stockOutLine = sol
            spl.suggestedLotLine = ill
            spl.pickOrderLine = pol
            spl.qty = qtyBd
            spl.type = SuggestedPickLotType.PICK_ORDER
            spl.deleted = false
            suggestPickLotRepository.saveAndFlush(spl)
        } else {
            suggestPickLotRepository.save(
                SuggestedPickLot().apply {
                    type = SuggestedPickLotType.PICK_ORDER
                    stockOutLine = sol
                    suggestedLotLine = ill
                    pickOrderLine = pol
                    qty = qtyBd
                },
            )
        }
    }

    /**
     * Backend-default avoid warehouse list for workbench suggestions.
     * - Case-insensitive match against `warehouse.code`.
     * - When request.excludeWarehouseCodes is provided, it overrides this default.
     *
     * Keep it empty if you don't need any default excludes.
     */
    private val defaultExcludeWarehouseCodes: Set<String> = setOf(
    "2F-W202-01-00",
    "2F-W200-#A-00",
)
    private fun normalizeStoreId(raw: String?): String? {
        val s = raw?.trim()?.uppercase() ?: return null
        if (s.isEmpty()) return null
        return s.replace("/", "").replace(" ", "")
    }

    private fun isWarehouseExcluded(lot: InventoryLotLine, excludeWarehouseCodes: Set<String>): Boolean {
        if (excludeWarehouseCodes.isEmpty()) return false
        val code = lot.warehouse?.code?.trim()?.uppercase()
        return !code.isNullOrEmpty() && code in excludeWarehouseCodes
    }

    private fun effectiveExcludeWarehouseCodes(raw: List<String>?): Set<String> {
        if (raw == null) return defaultExcludeWarehouseCodes
        val normalized = raw
            .map { it.trim() }
            .filter { it.isNotEmpty() }
            .map { it.uppercase() }
            .toSet()
        return normalized
    }

    /**
     * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22
     * Lot stock UOM must match pick_order_line.uom (UomConversion id).
     */
    private fun matchesPolUom(lot: InventoryLotLine, line: PickOrderLine): Boolean {
        val polUomId = line.uom?.id ?: return false
        return lot.stockUom?.uom?.id == polUomId
    }

    /**
     * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22
     * Build suggestions with no-hold rule:
     * available = inQty - outQty (ignore holdQty).
     * When [desired] is a single segment, reuses one SPL row (max id) and repoints lot + qty (switch-lot without new id).
     * When multiple segments, matches by [suggestedLotLineId], inserts gaps, soft-deletes obsolete rows.
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun rebuildNoHoldSuggestionsForPickOrder(
        pickOrderId: Long,
        excludeWarehouseCodes: List<String>? = null,
    ): WorkbenchSuggestionSummary {
        stockOutLIneRepository.flush()
        val lines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId)
        if (lines.isEmpty()) {
            return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 0)
        }

        val lineIds = lines.mapNotNull { it.id }
        val alreadyPickedByLineId: Map<Long, BigDecimal> = if (lineIds.isEmpty()) {
            emptyMap()
        } else {
            // Avoid N+1: load all SOL infos once (projection), aggregate picked qty per pick_order_line.
            val solInfos = stockOutLIneRepository.findAllInfoByPickOrderLineIdInAndDeletedFalse(lineIds)
            solInfos.groupBy { it.pickOrderLineId ?: 0L }
                .filterKeys { it != 0L }
                .mapValues { (_, sols) -> WorkbenchStockOutLinePickProgress.sumPickedQty(sols) }
        }

        val itemIds = lines.mapNotNull { it.item?.id }.distinct()
        val today = LocalDate.now()
        val excludeSet = effectiveExcludeWarehouseCodes(excludeWarehouseCodes)
        val lotsByItem = inventoryLotLineRepository.findAllByItemIdIn(itemIds)
            .filter { !it.deleted }
            .filter { it.status == InventoryLotLineStatus.AVAILABLE }
            .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true }
            .filter { !isWarehouseExcluded(it, excludeSet) }
            .sortedBy { it.inventoryLot?.expiryDate }
            .groupBy { it.inventoryLot?.item?.id }

        var created = 0
        var skipped = 0
        lines.forEach { line ->
            val lineId = line.id ?: return@forEach
            val required = line.qty ?: BigDecimal.ZERO
            if (required <= BigDecimal.ZERO) {
                softDeleteAllSuggestionsForLine(lineId)
                skipped++
                return@forEach
            }

            val alreadyPicked = alreadyPickedByLineId[lineId] ?: BigDecimal.ZERO
            val remaining = required.subtract(alreadyPicked)
            if (remaining <= BigDecimal.ZERO) {
                softDeleteAllSuggestionsForLine(lineId)
                skipped++
                return@forEach
            }

            val lots = lotsByItem[line.item?.id].orEmpty().filter { matchesPolUom(it, line) }
            val desired = buildDesiredAllocations(lots, remaining, lineId)
            created += syncSuggestedPickLotsForLine(line, desired)
        }

        return WorkbenchSuggestionSummary(
            pickOrderId = pickOrderId,
            created = created,
            skippedExisting = skipped,
        )
    }

    /**
     * Workbench assign-time "priming" mode:
     * - Do NOT pre-split across multiple lots.
     * - For each pick_order_line, create/update exactly ONE SPL row pointing to the next available lot
     *   with qty = remaining (required - already picked).
     *
     * Intended for workbench ticket assignment so scan-pick can drive subsequent re-suggest/splits.
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun primeNextSingleLotSuggestionsForPickOrder(
        pickOrderId: Long,
        storeId: String? = null,
        excludeWarehouseCodes: List<String>? = null,
    ): WorkbenchSuggestionSummary {
        stockOutLIneRepository.flush()
        val lines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId)
        if (lines.isEmpty()) {
            return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 0)
        }

        val lineIds = lines.mapNotNull { it.id }
        val alreadyPickedByLineId: Map<Long, BigDecimal> = if (lineIds.isEmpty()) {
            emptyMap()
        } else {
            val solInfos = stockOutLIneRepository.findAllInfoByPickOrderLineIdInAndDeletedFalse(lineIds)
            solInfos.groupBy { it.pickOrderLineId ?: 0L }
                .filterKeys { it != 0L }
                .mapValues { (_, sols) -> WorkbenchStockOutLinePickProgress.sumPickedQty(sols) }
        }

        var created = 0
        var skipped = 0
        lines.forEach { line ->
            val lineId = line.id ?: return@forEach
            val required = line.qty ?: BigDecimal.ZERO
            if (required <= BigDecimal.ZERO) {
                softDeleteAllSuggestionsForLine(lineId)
                skipped++
                return@forEach
            }

            val alreadyPicked = alreadyPickedByLineId[lineId] ?: BigDecimal.ZERO
            val remaining = required.subtract(alreadyPicked)
            if (remaining <= BigDecimal.ZERO) {
                softDeleteAllSuggestionsForLine(lineId)
                skipped++
                return@forEach
            }

            val r = setNoHoldSuggestionsForPickOrderLineNextSingleLot(
                pickOrderLineId = lineId,
                targetQty = remaining,
                storeId = storeId,
                excludeInventoryLotLineId = null,
                excludeWarehouseCodes = excludeWarehouseCodes,
            )
            created += r.created
        }

        return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = created, skippedExisting = skipped)
    }

    /**
     * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22
     * Same no-hold allocation as [rebuildNoHoldSuggestionsForPickOrder], but only for one [pickOrderLineId].
     * Used by workbench scan-pick to avoid rebuilding suggestions for every line on the pick order.
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun rebuildNoHoldSuggestionsForPickOrderLine(
        pickOrderLineId: Long,
        storeId: String? = null,
        excludeWarehouseCodes: List<String>? = null,
    ): WorkbenchSuggestionSummary {
        stockOutLIneRepository.flush()
        val line = pickOrderLineRepository.findById(pickOrderLineId).orElse(null)
            ?: return WorkbenchSuggestionSummary(pickOrderId = 0L, created = 0, skippedExisting = 0)
        if (line.deleted) {
            return WorkbenchSuggestionSummary(
                pickOrderId = line.pickOrder?.id ?: 0L,
                created = 0,
                skippedExisting = 0,
            )
        }
        val pickOrderId = line.pickOrder?.id ?: 0L

        val lineFresh = pickOrderLineRepository.findById(pickOrderLineId).orElse(line)
        val required = lineFresh.qty ?: BigDecimal.ZERO
        if (required <= BigDecimal.ZERO) {
            softDeleteAllSuggestionsForLine(pickOrderLineId)
            return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 1)
        }

        val solInfos = stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
        val solSnapshot = solInfos.joinToString("; ") { info ->
            "sol${info.id} st=${info.status} qty=${info.qty} picked=${WorkbenchStockOutLinePickProgress.isCountedAsPicked(info)}"
        }
        val alreadyPicked = WorkbenchStockOutLinePickProgress.sumPickedQty(solInfos)
        val remaining = required.subtract(alreadyPicked)
        log.info(
            "WORKBENCH_SPL_TRACE polLine={} poId={} polRequired={} alreadyPicked={} remainingIntoAllocate={} sols=[{}]",
            pickOrderLineId,
            pickOrderId,
            required,
            alreadyPicked,
            remaining,
            solSnapshot,
        )
        if (remaining <= BigDecimal.ZERO) {
            softDeleteAllSuggestionsForLine(pickOrderLineId)
            return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 1)
        }

        val itemId = line.item?.id
            ?: return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 0)
        val today = LocalDate.now()
        val storeKey = normalizeStoreId(storeId)
        val excludeSet = effectiveExcludeWarehouseCodes(excludeWarehouseCodes)
        val lots = inventoryLotLineRepository.findAllByItemIdIn(listOf(itemId))
            .filter { !it.deleted }
            .filter { it.status == InventoryLotLineStatus.AVAILABLE }
            .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true }
            .filter { !isWarehouseExcluded(it, excludeSet) }
            .filter { matchesPolUom(it, lineFresh) }
            .filter { lot ->
                if (storeKey == null) true
                else normalizeStoreId(lot.warehouse?.store_id) == storeKey
            }
            .sortedBy { it.inventoryLot?.expiryDate }

        val desired = buildDesiredAllocations(lots, remaining, pickOrderLineId)
        val created = syncSuggestedPickLotsForLine(lineFresh, desired)

        val sumSplQty = desired.fold(BigDecimal.ZERO) { acc, p -> acc.add(p.second) }
        log.info(
            "WORKBENCH_SPL_TRACE polLine={} poId={} desiredRows={} sumDesiredQty={} newInserts={} detail=[{}]",
            pickOrderLineId,
            pickOrderId,
            desired.size,
            sumSplQty,
            created,
            desired.joinToString("; ") { (ill, q) ->
                val illId = ill?.id
                val ln = ill?.inventoryLot?.lotNo ?: "no-lot"
                "qty=$q illId=$illId lotNo=$ln"
            },
        )

        return WorkbenchSuggestionSummary(
            pickOrderId = pickOrderId,
            created = created,
            skippedExisting = 0,
        )
    }

    /**
     * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22
     * Workbench explicit-qty remainder support (single-next-lot):
     * Create exactly ONE suggested lot row for this POL with [targetQty] pointing to the next available lot.
     * When [storeId] is provided, the next lot must be within the same store (warehouse.store_id).
     * If no eligible lot exists, create a single "no-lot tail" row (suggestedLotLine = null) for visibility.
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun setNoHoldSuggestionsForPickOrderLineNextSingleLot(
        pickOrderLineId: Long,
        targetQty: BigDecimal,
        storeId: String? = null,
        excludeInventoryLotLineId: Long? = null,
        excludeWarehouseCodes: List<String>? = null,
    ): WorkbenchSuggestionSummary {
        stockOutLIneRepository.flush()
        val line = pickOrderLineRepository.findById(pickOrderLineId).orElse(null)
            ?: return WorkbenchSuggestionSummary(pickOrderId = 0L, created = 0, skippedExisting = 0)
        if (line.deleted) {
            return WorkbenchSuggestionSummary(
                pickOrderId = line.pickOrder?.id ?: 0L,
                created = 0,
                skippedExisting = 0,
            )
        }
        val pickOrderId = line.pickOrder?.id ?: 0L
        if (targetQty <= BigDecimal.ZERO) {
            softDeleteAllSuggestionsForLine(pickOrderLineId)
            return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 1)
        }
        val itemId = line.item?.id
            ?: return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 0)
        val today = LocalDate.now()
        val storeKey = normalizeStoreId(storeId)
        val excludeSet = effectiveExcludeWarehouseCodes(excludeWarehouseCodes)
        val nextLot = inventoryLotLineRepository.findAllByItemIdIn(listOf(itemId))
            .asSequence()
            .filter { !it.deleted }
            .filter { it.status == InventoryLotLineStatus.AVAILABLE }
            .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true }
            .filter { !isWarehouseExcluded(it, excludeSet) }
            .filter { matchesPolUom(it, line) }
            .filter { lot ->
                if (excludeInventoryLotLineId == null) true else lot.id != excludeInventoryLotLineId
            }
            .filter { lot ->
                if (storeKey == null) true
                else normalizeStoreId(lot.warehouse?.store_id) == storeKey
            }
            .sortedBy { it.inventoryLot?.expiryDate }
            .firstOrNull()

        val desired = listOf(nextLot to targetQty)
        val created = syncSuggestedPickLotsForLine(line, desired)
        return WorkbenchSuggestionSummary(
            pickOrderId = pickOrderId,
            created = created,
            skippedExisting = 0,
        )
    }

    /**
     * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.1 | 2026-07-22
     * Workbench explicit-qty split support:
     * Set suggestions for this POL to an exact [targetQty] (independent of POL.qty).
     * - If [targetQty] <= 0: soft-delete all active suggestions for the line.
     * - Else: allocate across available lots (no-hold) and upsert SPL rows to match the allocation.
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun setNoHoldSuggestionsForPickOrderLineExactQty(
        pickOrderLineId: Long,
        targetQty: BigDecimal,
        excludeWarehouseCodes: List<String>? = null,
    ): WorkbenchSuggestionSummary {
        stockOutLIneRepository.flush()
        val line = pickOrderLineRepository.findById(pickOrderLineId).orElse(null)
            ?: return WorkbenchSuggestionSummary(pickOrderId = 0L, created = 0, skippedExisting = 0)
        if (line.deleted) {
            return WorkbenchSuggestionSummary(
                pickOrderId = line.pickOrder?.id ?: 0L,
                created = 0,
                skippedExisting = 0,
            )
        }
        val pickOrderId = line.pickOrder?.id ?: 0L
        if (targetQty <= BigDecimal.ZERO) {
            softDeleteAllSuggestionsForLine(pickOrderLineId)
            return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 1)
        }
        val itemId = line.item?.id
            ?: return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = 0, skippedExisting = 0)
        val today = LocalDate.now()
        val excludeSet = effectiveExcludeWarehouseCodes(excludeWarehouseCodes)
        val lots = inventoryLotLineRepository.findAllByItemIdIn(listOf(itemId))
            .filter { !it.deleted }
            .filter { it.status == InventoryLotLineStatus.AVAILABLE }
            .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true }
            .filter { !isWarehouseExcluded(it, excludeSet) }
            .filter { matchesPolUom(it, line) }
            .sortedBy { it.inventoryLot?.expiryDate }

        val desired = buildDesiredAllocations(lots, targetQty, pickOrderLineId)
        val created = syncSuggestedPickLotsForLine(line, desired)
        return WorkbenchSuggestionSummary(pickOrderId = pickOrderId, created = created, skippedExisting = 0)
    }

    private fun softDeleteAllSuggestionsForLine(pickOrderLineId: Long) {
        val active = suggestPickLotRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
        val toClear = active.filter { !isSplFrozen(it) }
        if (toClear.isEmpty()) return
        toClear.forEach { it.deleted = true }
        suggestPickLotRepository.saveAllAndFlush(toClear)
    }

    /** Desired (lot line or null for no-lot tail, qty) segments; never exceeds [remainingInit]. */
    private fun buildDesiredAllocations(
        lots: List<InventoryLotLine>,
        remainingInit: BigDecimal,
        pickOrderLineIdForLog: Long,
    ): List<Pair<InventoryLotLine?, BigDecimal>> {
        val out = mutableListOf<Pair<InventoryLotLine?, BigDecimal>>()
        var remaining = remainingInit
        for (lot in lots) {
            if (remaining <= BigDecimal.ZERO) break
            val available = (lot.inQty ?: BigDecimal.ZERO).subtract(lot.outQty ?: BigDecimal.ZERO)
            if (available <= BigDecimal.ZERO) continue
            val assign = available.min(remaining)
            if (assign > BigDecimal.ZERO) {
                log.info(
                    "WORKBENCH_SPL_ALLOC polLine={} illId={} lotNo={} avail={} assign={} remainingAfter={}",
                    pickOrderLineIdForLog,
                    lot.id,
                    lot.inventoryLot?.lotNo,
                    available,
                    assign,
                    remaining.subtract(assign),
                )
                out += lot to assign
                remaining = remaining.subtract(assign)
            }
        }
        if (remaining > BigDecimal.ZERO) {
            log.info(
                "WORKBENCH_SPL_ALLOC polLine={} illId=null lotNo=no-lot avail=n/a assign={} remainingAfter=0 (no-lot tail)",
                pickOrderLineIdForLog,
                remaining,
            )
            out += null to remaining
        }
        return out
    }

    /**
     * Collapse duplicate SPL rows per [suggestedLotLineId]; soft-delete extras. Returns one row per lot key.
     */
    private fun dedupeKeepersBySuggestedLotLine(existing: List<SuggestedPickLot>): List<SuggestedPickLot> {
        val byKey = existing.groupBy { it.suggestedLotLine?.id }
        val keepers = mutableListOf<SuggestedPickLot>()
        val dupSoftDelete = mutableListOf<SuggestedPickLot>()
        for ((_, group) in byKey) {
            if (group.size == 1) {
                keepers.add(group.first())
            } else {
                val keeper = group.maxBy { it.id ?: 0L }
                keepers.add(keeper)
                group.filter { it.id != keeper.id }.forEach { dup ->
                    dup.deleted = true
                    dupSoftDelete.add(dup)
                }
            }
        }
        if (dupSoftDelete.isNotEmpty()) {
            suggestPickLotRepository.saveAllAndFlush(dupSoftDelete)
        }
        return keepers
    }

    /**
     * Align DB rows with [desired]. Returns count of newly inserted SPL rows.
     */
    private fun syncSuggestedPickLotsForLine(
        line: PickOrderLine,
        desired: List<Pair<InventoryLotLine?, BigDecimal>>,
    ): Int {
        val polId = line.id!!
        val existing = suggestPickLotRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
        val mutableExisting = existing.filter { !isSplFrozen(it) }
        val keepers = dedupeKeepersBySuggestedLotLine(mutableExisting)

        // Single allocation segment: repoint a **non-frozen** canonical row — frozen historical SPLs stay untouched.
        if (desired.size == 1) {
            val (ill, qty) = desired.first()
            val toPersist = mutableListOf<SuggestedPickLot>()
            val reuse = keepers.maxByOrNull { it.id ?: 0L }
            if (reuse != null) {
                reuse.suggestedLotLine = ill
                reuse.pickOrderLine = line
                reuse.type = SuggestedPickLotType.PICK_ORDER
                reuse.qty = qty
                reuse.deleted = false
                toPersist.add(reuse)
                for (spl in keepers) {
                    if (spl.id != reuse.id) {
                        spl.deleted = true
                        toPersist.add(spl)
                    }
                }
            } else {
                toPersist.add(
                    SuggestedPickLot().apply {
                        type = SuggestedPickLotType.PICK_ORDER
                        suggestedLotLine = ill
                        pickOrderLine = line
                        this.qty = qty
                    },
                )
            }
            if (toPersist.isNotEmpty()) {
                suggestPickLotRepository.saveAllAndFlush(toPersist)
            }
            return if (reuse == null) 1 else 0
        }

        val matchedIds = mutableSetOf<Long>()
        val toPersist = mutableListOf<SuggestedPickLot>()
        var newInsertCount = 0
        val byIllKey = keepers.associateBy { it.suggestedLotLine?.id }.toMutableMap()

        for ((ill, qty) in desired) {
            val k = ill?.id
            val spl = byIllKey[k]
            if (spl != null) {
                spl.suggestedLotLine = ill
                spl.pickOrderLine = line
                spl.type = SuggestedPickLotType.PICK_ORDER
                spl.qty = qty
                spl.deleted = false
                spl.id?.let { matchedIds.add(it) }
                toPersist.add(spl)
                byIllKey.remove(k)
            } else {
                newInsertCount++
                toPersist.add(
                    SuggestedPickLot().apply {
                        type = SuggestedPickLotType.PICK_ORDER
                        suggestedLotLine = ill
                        pickOrderLine = line
                        this.qty = qty
                    },
                )
            }
        }

        for (spl in keepers) {
            val id = spl.id ?: continue
            if (id !in matchedIds) {
                spl.deleted = true
                toPersist.add(spl)
            }
        }

        if (toPersist.isNotEmpty()) {
            suggestPickLotRepository.saveAllAndFlush(toPersist)
        }
        return newInsertCount
    }
}
