package com.ffii.fpsms.modules.master.service

import com.ffii.fpsms.modules.master.entity.Bom
import com.ffii.fpsms.modules.master.entity.BomMaterial
import com.ffii.fpsms.modules.master.entity.BomMaterialRepository
import com.ffii.fpsms.modules.master.entity.BomRepository
import com.ffii.fpsms.modules.master.entity.ItemUom
import com.ffii.fpsms.modules.master.entity.Items
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.master.entity.UomConversion
import com.ffii.fpsms.modules.master.web.models.MasterDataIssueResponse
import com.ffii.fpsms.modules.master.web.models.MasterDataIssueSummaryResponse
import com.ffii.fpsms.modules.master.web.models.MasterDataIssueSummaryTiming
import java.time.LocalDateTime
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
open class MasterDataIssueService(
    private val bomRepository: BomRepository,
    private val bomMaterialRepository: BomMaterialRepository,
    private val itemsRepository: ItemsRepository,
    private val itemUomRespository: ItemUomRespository,
    private val uomConversionService: UomConversionService,
) {
    data class UnitHealth(
        val value: String?,
        val correct: Boolean,
        val modifiedAt: String?,
        /** OK | MISSING | DELETED */
        val status: String,
    )

    data class UnitSnapshot(
        val base: UnitHealth,
        val stock: UnitHealth,
        val purchase: UnitHealth,
        val sales: UnitHealth,
    )

    data class IssueContext(
        val scope: String,
        val bomId: Long? = null,
        val bomCode: String? = null,
        val bomName: String? = null,
        val bomMaterialId: Long? = null,
        val description: String? = null,
    )

    @Transactional(readOnly = true)
    open fun findBomMasterDataIssues(): List<MasterDataIssueResponse> {
        val issues = mutableListOf<MasterDataIssueResponse>()
        val uomsByItemId = loadActiveUomsByItemId()
        val materials = bomMaterialRepository.findAllActiveForActiveBoms()
        val materialsByBomId = materials.groupBy { it.bom?.id ?: -1L }
        val uomCache = buildUomCache(uomsByItemId, emptyMap(), materials)
        val boms = bomRepository.findAllActiveWithItemAndUom()
        for (bom in boms) {
            collectBomHeaderIssues(bom, issues, uomsByItemId)
            val bomId = bom.id ?: continue
            for (material in materialsByBomId[bomId].orEmpty()) {
                collectBomMaterialIssues(bom, material, issues, uomsByItemId, uomCache)
            }
        }
        return sortIssues(issues)
    }

    @Transactional(readOnly = true)
    open fun findItemMasterDataIssues(): List<MasterDataIssueResponse> {
        val issues = mutableListOf<MasterDataIssueResponse>()
        val uomsByItemId = loadActiveUomsByItemId()
        val deletedUomsByItemId = loadDeletedUomsByItemId()
        val items = itemsRepository.findAllByDeletedFalse()
        val ctx = IssueContext(scope = "ITEM")
        for (item in items) {
            collectItemUomIssues(item, ctx, issues, uomsByItemId, deletedUomsByItemId)
        }
        return sortIssues(issues)
    }

    @Transactional(readOnly = true)
    open fun findMasterDataIssuesSummary(includeTiming: Boolean = false): MasterDataIssueSummaryResponse {
        val totalStart = System.nanoTime()
        var loadActiveUomsMs = 0L
        var loadDeletedUomsMs = 0L
        var loadMaterialsMs = 0L
        var buildUomCacheMs = 0L
        var loadBomsMs = 0L
        var scanBomTabMs = 0L
        var loadItemsMs = 0L
        var scanItemTabMs = 0L

        val uomsByItemId =
            if (includeTiming) {
                timed { loadActiveUomsByItemId() }.also { loadActiveUomsMs = it.second }.first
            } else {
                loadActiveUomsByItemId()
            }
        val deletedUomsByItemId =
            if (includeTiming) {
                timed { loadDeletedUomsByItemId() }.also { loadDeletedUomsMs = it.second }.first
            } else {
                loadDeletedUomsByItemId()
            }
        val materials =
            if (includeTiming) {
                timed { bomMaterialRepository.findAllActiveForActiveBoms() }
                    .also { loadMaterialsMs = it.second }
                    .first
            } else {
                bomMaterialRepository.findAllActiveForActiveBoms()
            }
        val materialsByBomId = materials.groupBy { it.bom?.id ?: -1L }
        val uomCache =
            if (includeTiming) {
                timed { buildUomCache(uomsByItemId, deletedUomsByItemId, materials) }
                    .also { buildUomCacheMs = it.second }
                    .first
            } else {
                buildUomCache(uomsByItemId, deletedUomsByItemId, materials)
            }
        val boms =
            if (includeTiming) {
                timed { bomRepository.findAllActiveWithItemAndUom() }.also { loadBomsMs = it.second }.first
            } else {
                bomRepository.findAllActiveWithItemAndUom()
            }
        val bomGroupCount =
            if (includeTiming) {
                timed {
                    scanBomTabGroupCount(boms, uomsByItemId, materialsByBomId, uomCache)
                }.also { scanBomTabMs = it.second }.first
            } else {
                scanBomTabGroupCount(boms, uomsByItemId, materialsByBomId, uomCache)
            }
        val items =
            if (includeTiming) {
                timed { itemsRepository.findAllByDeletedFalse() }.also { loadItemsMs = it.second }.first
            } else {
                itemsRepository.findAllByDeletedFalse()
            }
        val itemGroupCount =
            if (includeTiming) {
                timed { scanItemTabGroupCount(items, uomsByItemId, deletedUomsByItemId) }
                    .also { scanItemTabMs = it.second }
                    .first
            } else {
                scanItemTabGroupCount(items, uomsByItemId, deletedUomsByItemId)
            }

        val timing =
            if (includeTiming) {
                MasterDataIssueSummaryTiming(
                    totalMs = (System.nanoTime() - totalStart) / 1_000_000,
                    loadActiveUomsMs = loadActiveUomsMs,
                    loadDeletedUomsMs = loadDeletedUomsMs,
                    loadMaterialsMs = loadMaterialsMs,
                    buildUomCacheMs = buildUomCacheMs,
                    loadBomsMs = loadBomsMs,
                    scanBomTabMs = scanBomTabMs,
                    loadItemsMs = loadItemsMs,
                    scanItemTabMs = scanItemTabMs,
                )
            } else {
                null
            }

        return MasterDataIssueSummaryResponse(
            bomGroupCount = bomGroupCount,
            itemGroupCount = itemGroupCount,
            totalGroupCount = bomGroupCount + itemGroupCount,
            timing = timing,
        )
    }

    private inline fun <T> timed(block: () -> T): Pair<T, Long> {
        val start = System.nanoTime()
        val result = block()
        return result to (System.nanoTime() - start) / 1_000_000
    }

    /**
     * Fast BOM-tab group count: one bom fetch, one material batch, no issue DTOs.
     * Group keys match [groupBomTabIssues] on the frontend.
     */
    private fun scanBomTabGroupCount(
        boms: List<Bom>,
        uomsByItemId: Map<Long, List<ItemUom>>,
        materialsByBomId: Map<Long, List<BomMaterial>>,
        uomCache: Map<Long, UomConversion>,
    ): Int {
        val headerKeys = mutableSetOf<String>()
        val materialKeys = mutableSetOf<String>()
        for (bom in boms) {
            scanBomHeaderGroupKeys(bom, headerKeys, uomsByItemId)
            val bomId = bom.id ?: continue
            for (material in materialsByBomId[bomId].orEmpty()) {
                scanBomMaterialGroupKeys(bom, material, materialKeys, uomsByItemId, uomCache)
            }
        }
        return headerKeys.size + materialKeys.size
    }

    /** Fast item-tab group count; skips PICKING-only items (matches UI filter). */
    private fun scanItemTabGroupCount(
        items: List<Items>,
        uomsByItemId: Map<Long, List<ItemUom>>,
        deletedUomsByItemId: Map<Long, List<ItemUom>>,
    ): Int {
        val keys = mutableSetOf<String>()
        for (item in items) {
            if (!itemHasNonPickingTabIssue(item, uomsByItemId, deletedUomsByItemId)) continue
            val itemId = item.id
            val key =
                if (itemId != null) {
                    "item:$itemId"
                } else {
                    "code:${item.code?.trim().orEmpty().ifBlank { "unknown" }}"
                }
            keys.add(key)
        }
        return keys.size
    }

    private fun scanBomHeaderGroupKeys(
        bom: Bom,
        headerKeys: MutableSet<String>,
        uomsByItemId: Map<Long, List<ItemUom>>,
    ) {
        val bomId = bom.id ?: return
        val bomKey = "bom:$bomId"
        var hasIssue = false

        if (bom.code.isNullOrBlank()) hasIssue = true
        if (bom.name.isNullOrBlank()) hasIssue = true

        val item = bom.item
        val itemId = item?.id
        val unitSnapshot = buildUnitSnapshot(itemId, uomsByItemId)
        if (item == null || item.deleted == true) {
            hasIssue = true
        } else {
            if (itemHasAnyUnitIssue(item, uomsByItemId, unitSnapshot)) hasIssue = true
            val bomUom = bom.uom
            val salesConv = findSalesUnitRow(item.id!!, uomsByItemId)?.uom
            if (bomUom != null && salesConv != null && bomUom.id != salesConv.id) hasIssue = true
            if (bomUom != null) {
                val uomDesc = bomUom.udfudesc?.trim().orEmpty()
                val outputDesc = bom.outputQtyUom?.trim().orEmpty()
                if (outputDesc.isNotEmpty() && uomDesc.isNotEmpty() && !outputDesc.equals(uomDesc, ignoreCase = true)) {
                    hasIssue = true
                }
            }
        }
        if (hasIssue) headerKeys.add(bomKey)
    }

    private fun scanBomMaterialGroupKeys(
        bom: Bom,
        material: BomMaterial,
        materialKeys: MutableSet<String>,
        uomsByItemId: Map<Long, List<ItemUom>>,
        uomCache: Map<Long, UomConversion>,
    ) {
        val matItem = material.item
        val unitSnapshot = buildUnitSnapshot(matItem?.id, uomsByItemId)
        val itemId = matItem?.id
        val itemCode = matItem?.code
        val bomMaterialId = material.id

        fun addMaterialKey(issueCode: String, expected: String? = null, actual: String? = null) {
            val itemKey =
                if (itemId != null) {
                    "i:$itemId"
                } else {
                    "ic:${itemCode?.trim().orEmpty().ifBlank { bomMaterialId?.toString() ?: "unknown" }}"
                }
            val patternKey = materialPatternKey(issueCode, expected, actual)
            materialKeys.add("$itemKey|$patternKey")
        }

        if (matItem == null || matItem.deleted == true) {
            addMaterialKey("BOM_MATERIAL_MISSING_ITEM")
            return
        }

        val matUom = material.uom
        if (matUom != null && matUom.deleted == true) {
            addMaterialKey("BOM_MATERIAL_UOM_FK_INVALID", actual = "uomId=${matUom.id}")
        }

        val salesUnitFk = material.salesUnit
        if (salesUnitFk != null && salesUnitFk.deleted == true) {
            addMaterialKey("BOM_MATERIAL_UOM_FK_INVALID", actual = "salesUnitId=${salesUnitFk.id}")
        }

        val itemSales = findSalesUnitRow(matItem.id!!, uomsByItemId)?.uom
        if (salesUnitFk != null && itemSales != null && salesUnitFk.id != itemSales.id) {
            addMaterialKey(
                "BOM_MATERIAL_SALES_UOM_MISMATCH",
                formatUom(itemSales),
                formatUom(salesUnitFk),
            )
        }

        val itemBase = findBaseUnitRow(matItem.id!!, uomsByItemId)?.uom
        val matBaseId = material.baseUnit?.toLong()
        if (matBaseId != null && itemBase != null && matBaseId != itemBase.id) {
            addMaterialKey(
                "BOM_MATERIAL_BASE_UOM_MISMATCH",
                formatUom(itemBase),
                formatUomById(matBaseId, uomCache),
            )
        }

        val itemStock = findStockUnitRow(matItem.id!!, uomsByItemId)?.uom
        val matStockId = material.stockUnit?.toLong()
        if (matStockId != null && itemStock != null && matStockId != itemStock.id) {
            addMaterialKey(
                "BOM_MATERIAL_STOCK_UOM_MISMATCH",
                formatUom(itemStock),
                formatUomById(matStockId, uomCache),
            )
        }
    }

    private fun materialPatternKey(issueCode: String, expected: String?, actual: String?): String {
        val exp = expected ?: ""
        val act = actual ?: ""
        return when (issueCode) {
            "BOM_MATERIAL_SALES_UOM_MISMATCH",
            "BOM_MATERIAL_STOCK_UOM_MISMATCH",
            -> "uom:$exp|$act"
            "BOM_MATERIAL_BASE_UOM_MISMATCH" -> "base:$exp|$act"
            else -> "$issueCode|$exp|$act"
        }
    }

    private fun itemHasNonPickingTabIssue(
        item: Items,
        uomsByItemId: Map<Long, List<ItemUom>>,
        deletedUomsByItemId: Map<Long, List<ItemUom>>,
    ): Boolean {
        val itemId = item.id ?: return false
        val snapshot = buildUnitSnapshot(itemId, uomsByItemId, deletedUomsByItemId) ?: return true
        val allUoms = uomsForItem(itemId, uomsByItemId)
        for (unitKey in ITEM_TAB_UNIT_KEYS) {
            if (itemUnitWouldIssue(item, allUoms, unitKey, snapshot)) return true
        }
        return false
    }

    private fun itemHasAnyUnitIssue(
        item: Items,
        uomsByItemId: Map<Long, List<ItemUom>>,
        unitSnapshot: UnitSnapshot?,
    ): Boolean {
        val itemId = item.id ?: return false
        val allUoms = uomsForItem(itemId, uomsByItemId)
        for (unitKey in ALL_UNIT_KEYS) {
            if (itemUnitWouldIssue(item, allUoms, unitKey, unitSnapshot)) return true
        }
        return false
    }

    private fun itemUnitWouldIssue(
        item: Items,
        allUoms: List<ItemUom>,
        unitKey: String,
        unitSnapshot: UnitSnapshot?,
    ): Boolean {
        val flag = unitFlag(unitKey)
        val rows = allUoms.filter(flag)
        return when {
            rows.isEmpty() -> true
            rows.size > 1 -> true
            else -> uomRowInvalid(rows.first())
        }
    }

    private fun uomRowInvalid(row: ItemUom): Boolean {
        val uom = row.uom
        return uom == null || uom.deleted == true
    }

    private fun unitFlag(unitKey: String): (ItemUom) -> Boolean =
        when (unitKey) {
            "BASE" -> { it -> it.baseUnit == true }
            "SALES" -> { it -> it.salesUnit == true }
            "STOCK" -> { it -> it.stockUnit == true }
            "PURCHASE" -> { it -> it.purchaseUnit == true }
            "PICKING" -> { it -> it.pickingUnit == true }
            else -> { _ -> false }
        }

    private fun buildUomCache(
        uomsByItemId: Map<Long, List<ItemUom>>,
        deletedUomsByItemId: Map<Long, List<ItemUom>>,
        materials: List<BomMaterial> = emptyList(),
    ): Map<Long, UomConversion> {
        val map = mutableMapOf<Long, UomConversion>()
        fun put(uom: UomConversion?) {
            val id = uom?.id ?: return
            map[id] = uom
        }
        for (rows in uomsByItemId.values) {
            for (row in rows) put(row.uom)
        }
        for (rows in deletedUomsByItemId.values) {
            for (row in rows) put(row.uom)
        }
        for (material in materials) {
            put(material.uom)
            put(material.salesUnit)
        }
        return map
    }

    companion object {
        private val ITEM_TAB_UNIT_KEYS = listOf("BASE", "SALES", "STOCK", "PURCHASE")
        private val ALL_UNIT_KEYS = listOf("BASE", "SALES", "STOCK", "PURCHASE", "PICKING")
    }

    /** One query: all active item_uom + uom_conversion, grouped by itemId. */
    private fun loadActiveUomsByItemId(): Map<Long, List<ItemUom>> {
        val grouped = mutableMapOf<Long, MutableList<ItemUom>>()
        for (row in itemUomRespository.findAllActiveWithUom()) {
            val itemId = row.item?.id
                ?: continue
            grouped.computeIfAbsent(itemId) { mutableListOf() }.add(row)
        }
        return grouped
    }

    private fun loadDeletedUomsByItemId(): Map<Long, List<ItemUom>> {
        val grouped = mutableMapOf<Long, MutableList<ItemUom>>()
        for (row in itemUomRespository.findAllDeletedWithUom()) {
            val itemId = row.item?.id ?: continue
            grouped.computeIfAbsent(itemId) { mutableListOf() }.add(row)
        }
        return grouped
    }

    private fun uomsForItem(itemId: Long, uomsByItemId: Map<Long, List<ItemUom>>): List<ItemUom> =
        uomsByItemId[itemId] ?: emptyList()

    private fun rowModifiedTime(row: ItemUom): LocalDateTime? =
        row.modified ?: row.m18LastModifyDate

    private fun newestRow(rows: List<ItemUom>): ItemUom? =
        rows.maxByOrNull { rowModifiedTime(it) ?: LocalDateTime.MIN }

    private fun findSalesUnitRow(itemId: Long, uomsByItemId: Map<Long, List<ItemUom>>): ItemUom? =
        uomsForItem(itemId, uomsByItemId).firstOrNull { it.salesUnit == true }

    private fun findStockUnitRow(itemId: Long, uomsByItemId: Map<Long, List<ItemUom>>): ItemUom? =
        uomsForItem(itemId, uomsByItemId).firstOrNull { it.stockUnit == true }

    private fun findBaseUnitRow(itemId: Long, uomsByItemId: Map<Long, List<ItemUom>>): ItemUom? =
        uomsForItem(itemId, uomsByItemId).firstOrNull { it.baseUnit == true }

    private fun evaluateUnitHealth(
        activeUoms: List<ItemUom>,
        deletedUoms: List<ItemUom>,
        flag: (ItemUom) -> Boolean,
    ): UnitHealth {
        val active = activeUoms.filter(flag)
        when {
            active.size == 1 -> {
                val row = active.first()
                val uom = row.uom
                if (uom == null || uom.deleted == true) {
                    return UnitHealth(
                        value = formatUom(uom).takeIf { uom != null },
                        correct = false,
                        modifiedAt = rowModifiedTime(row)?.toString(),
                        status = "MISSING",
                    )
                }
                return UnitHealth(
                    value = formatUom(uom),
                    correct = true,
                    modifiedAt = rowModifiedTime(row)?.toString(),
                    status = "OK",
                )
            }
            active.size > 1 -> {
                val row = newestRow(active)
                return UnitHealth(
                    value = row?.let { formatUom(it.uom) },
                    correct = false,
                    modifiedAt = active.mapNotNull { rowModifiedTime(it) }.maxOrNull()?.toString(),
                    status = "MISSING",
                )
            }
            else -> {
                val deleted = deletedUoms.filter(flag)
                if (deleted.isNotEmpty()) {
                    val row = newestRow(deleted)!!
                    return UnitHealth(
                        value = formatUom(row.uom),
                        correct = false,
                        modifiedAt = rowModifiedTime(row)?.toString(),
                        status = "DELETED",
                    )
                }
                return UnitHealth(
                    value = null,
                    correct = false,
                    modifiedAt = null,
                    status = "MISSING",
                )
            }
        }
    }

    private fun buildUnitSnapshot(
        itemId: Long?,
        uomsByItemId: Map<Long, List<ItemUom>>,
        deletedUomsByItemId: Map<Long, List<ItemUom>> = emptyMap(),
    ): UnitSnapshot? {
        if (itemId == null) return null
        val active = uomsForItem(itemId, uomsByItemId)
        val deleted = uomsForItem(itemId, deletedUomsByItemId)
        return UnitSnapshot(
            base = evaluateUnitHealth(active, deleted) { it.baseUnit == true },
            stock = evaluateUnitHealth(active, deleted) { it.stockUnit == true },
            purchase = evaluateUnitHealth(active, deleted) { it.purchaseUnit == true },
            sales = evaluateUnitHealth(active, deleted) { it.salesUnit == true },
        )
    }

    private fun collectBomHeaderIssues(
        bom: Bom,
        issues: MutableList<MasterDataIssueResponse>,
        uomsByItemId: Map<Long, List<ItemUom>>,
    ) {
        val bomId = bom.id ?: return
        val bomCode = bom.code
        val bomName = bom.name
        val description = bom.description
        val ctx = IssueContext(
            scope = "BOM",
            bomId = bomId,
            bomCode = bomCode,
            bomName = bomName,
            description = description,
        )

        if (bomCode.isNullOrBlank()) {
            issues.add(issue(ctx, null, "MISSING_BOM_CODE"))
        }
        if (bomName.isNullOrBlank()) {
            issues.add(issue(ctx, null, "MISSING_BOM_NAME"))
        }

        val item = bom.item
        val itemId = item?.id
        val unitSnapshot = buildUnitSnapshot(itemId, uomsByItemId)
        if (item == null || item.deleted == true) {
            issues.add(issue(ctx, itemId, "MISSING_ITEM", unitSnapshot = unitSnapshot))
            return
        }

        collectItemUomIssues(item, ctx, issues, uomsByItemId, unitSnapshot = unitSnapshot)

        val bomUom = bom.uom
        val salesConv = findSalesUnitRow(item.id!!, uomsByItemId)?.uom
        if (bomUom != null && salesConv != null && bomUom.id != salesConv.id) {
            issues.add(
                issue(
                    ctx,
                    item.id,
                    "BOM_OUTPUT_UOM_MISMATCH_SALES",
                    itemCode = item.code,
                    itemName = item.name,
                    expectedValue = formatUom(salesConv),
                    actualValue = formatUom(bomUom),
                    unitSnapshot = unitSnapshot,
                ),
            )
        }
        if (bomUom != null) {
            val uomDesc = bomUom.udfudesc?.trim().orEmpty()
            val outputDesc = bom.outputQtyUom?.trim().orEmpty()
            if (outputDesc.isNotEmpty() && uomDesc.isNotEmpty() && !outputDesc.equals(uomDesc, ignoreCase = true)) {
                issues.add(
                    issue(
                        ctx,
                        item.id,
                        "BOM_OUTPUT_UOM_TEXT_DRIFT",
                        itemCode = item.code,
                        itemName = item.name,
                        expectedValue = uomDesc,
                        actualValue = outputDesc,
                        unitSnapshot = unitSnapshot,
                    ),
                )
            }
        }
    }

    private fun collectBomMaterialIssues(
        bom: Bom,
        material: BomMaterial,
        issues: MutableList<MasterDataIssueResponse>,
        uomsByItemId: Map<Long, List<ItemUom>>,
        uomCache: Map<Long, UomConversion> = emptyMap(),
    ) {
        val bomId = bom.id ?: return
        val materialId = material.id
        val ctx = IssueContext(
            scope = "BOM_MATERIAL",
            bomId = bomId,
            bomCode = bom.code,
            bomName = bom.name,
            bomMaterialId = materialId,
            description = bom.description,
        )

        val matItem = material.item
        val unitSnapshot = buildUnitSnapshot(matItem?.id, uomsByItemId)
        if (matItem == null || matItem.deleted == true) {
            issues.add(
                issue(
                    ctx,
                    matItem?.id,
                    "BOM_MATERIAL_MISSING_ITEM",
                    itemCode = matItem?.code,
                    itemName = matItem?.name ?: material.itemName,
                    unitSnapshot = unitSnapshot,
                ),
            )
            return
        }

        // Item master UOM gaps belong on the Item tab only — not repeated per BOM here.
        val matItemCode = matItem.code
        val matItemName = matItem.name?.takeIf { it.isNotBlank() } ?: material.itemName

        val matUom = material.uom
        if (matUom != null && matUom.deleted == true) {
            issues.add(
                issue(
                    ctx,
                    matItem.id,
                    "BOM_MATERIAL_UOM_FK_INVALID",
                    itemCode = matItemCode,
                    itemName = matItemName,
                    actualValue = "uomId=${matUom.id}",
                    unitSnapshot = unitSnapshot,
                ),
            )
        }

        val salesUnitFk = material.salesUnit
        if (salesUnitFk != null && salesUnitFk.deleted == true) {
            issues.add(
                issue(
                    ctx,
                    matItem.id,
                    "BOM_MATERIAL_UOM_FK_INVALID",
                    itemCode = matItemCode,
                    itemName = matItemName,
                    actualValue = "salesUnitId=${salesUnitFk.id}",
                    unitSnapshot = unitSnapshot,
                ),
            )
        }

        val itemSales = findSalesUnitRow(matItem.id!!, uomsByItemId)?.uom
        if (salesUnitFk != null && itemSales != null && salesUnitFk.id != itemSales.id) {
            issues.add(
                issue(
                    ctx,
                    matItem.id,
                    "BOM_MATERIAL_SALES_UOM_MISMATCH",
                    itemCode = matItemCode,
                    itemName = matItemName,
                    expectedValue = formatUom(itemSales),
                    actualValue = formatUom(salesUnitFk),
                    unitSnapshot = unitSnapshot,
                ),
            )
        }

        val itemBase = findBaseUnitRow(matItem.id!!, uomsByItemId)?.uom
        val matBaseId = material.baseUnit?.toLong()
        if (matBaseId != null && itemBase != null && matBaseId != itemBase.id) {
            issues.add(
                issue(
                    ctx,
                    matItem.id,
                    "BOM_MATERIAL_BASE_UOM_MISMATCH",
                    itemCode = matItemCode,
                    itemName = matItemName,
                    expectedValue = formatUom(itemBase),
                    actualValue = formatUomById(matBaseId, uomCache),
                    unitSnapshot = unitSnapshot,
                ),
            )
        }

        val itemStock = findStockUnitRow(matItem.id!!, uomsByItemId)?.uom
        val matStockId = material.stockUnit?.toLong()
        if (matStockId != null && itemStock != null && matStockId != itemStock.id) {
            issues.add(
                issue(
                    ctx,
                    matItem.id,
                    "BOM_MATERIAL_STOCK_UOM_MISMATCH",
                    itemCode = matItemCode,
                    itemName = matItemName,
                    expectedValue = formatUom(itemStock),
                    actualValue = formatUomById(matStockId, uomCache),
                    unitSnapshot = unitSnapshot,
                ),
            )
        }
    }

    private fun collectItemUomIssues(
        item: Items,
        ctx: IssueContext,
        issues: MutableList<MasterDataIssueResponse>,
        uomsByItemId: Map<Long, List<ItemUom>>,
        deletedUomsByItemId: Map<Long, List<ItemUom>> = emptyMap(),
        unitSnapshot: UnitSnapshot? = null,
    ) {
        val itemId = item.id ?: return
        val allUoms = uomsForItem(itemId, uomsByItemId)
        val snapshot = unitSnapshot ?: buildUnitSnapshot(itemId, uomsByItemId, deletedUomsByItemId)

        checkUnitType(item, ctx, issues, allUoms, { it.baseUnit == true }, "BASE", snapshot)
        checkUnitType(item, ctx, issues, allUoms, { it.salesUnit == true }, "SALES", snapshot)
        checkUnitType(item, ctx, issues, allUoms, { it.stockUnit == true }, "STOCK", snapshot)
        checkUnitType(item, ctx, issues, allUoms, { it.pickingUnit == true }, "PICKING", snapshot)
        checkUnitType(item, ctx, issues, allUoms, { it.purchaseUnit == true }, "PURCHASE", snapshot)
    }

    private fun unitHealthForKey(snapshot: UnitSnapshot?, unitKey: String): UnitHealth? =
        when (unitKey) {
            "BASE" -> snapshot?.base
            "STOCK" -> snapshot?.stock
            "PURCHASE" -> snapshot?.purchase
            "SALES" -> snapshot?.sales
            else -> null
        }

    private fun checkUnitType(
        item: Items,
        ctx: IssueContext,
        issues: MutableList<MasterDataIssueResponse>,
        allUoms: List<ItemUom>,
        flag: (ItemUom) -> Boolean,
        unitKey: String,
        unitSnapshot: UnitSnapshot?,
    ) {
        val rows = allUoms.filter(flag)
        when {
            rows.isEmpty() -> {
                val health = unitHealthForKey(unitSnapshot, unitKey)
                val issueCode =
                    if (health?.status == "DELETED") "DELETED_${unitKey}_UOM" else "MISSING_${unitKey}_UOM"
                issues.add(
                    issue(
                        ctx,
                        item.id,
                        issueCode,
                        itemCode = item.code,
                        itemName = item.name,
                        unitSnapshot = unitSnapshot,
                    ),
                )
            }
            rows.size > 1 -> {
                issues.add(
                    issue(
                        ctx,
                        item.id,
                        "MULTIPLE_${unitKey}_UOM",
                        itemCode = item.code,
                        itemName = item.name,
                        actualValue = "count=${rows.size}",
                        unitSnapshot = unitSnapshot,
                    ),
                )
                rows.forEach { row -> checkUomConversion(item, ctx, issues, row, unitKey, unitSnapshot) }
            }
            else -> checkUomConversion(item, ctx, issues, rows.first(), unitKey, unitSnapshot)
        }
    }

    private fun checkUomConversion(
        item: Items,
        ctx: IssueContext,
        issues: MutableList<MasterDataIssueResponse>,
        row: ItemUom,
        unitKey: String,
        unitSnapshot: UnitSnapshot?,
    ) {
        val uom = row.uom
        if (uom == null || uom.deleted == true) {
            issues.add(
                issue(
                    ctx,
                    item.id,
                    "MISSING_${unitKey}_UOM_CONVERSION",
                    itemCode = item.code,
                    itemName = item.name,
                    unitSnapshot = unitSnapshot,
                ),
            )
        }
    }

    private fun issue(
        ctx: IssueContext,
        itemId: Long?,
        issueCode: String,
        itemCode: String? = null,
        itemName: String? = null,
        expectedValue: String? = null,
        actualValue: String? = null,
        unitSnapshot: UnitSnapshot? = null,
    ): MasterDataIssueResponse =
        MasterDataIssueResponse(
            scope = ctx.scope,
            bomId = ctx.bomId,
            bomCode = ctx.bomCode,
            bomName = ctx.bomName,
            bomMaterialId = ctx.bomMaterialId,
            itemId = itemId,
            itemCode = itemCode,
            itemName = itemName,
            description = ctx.description,
            issueCode = issueCode,
            expectedValue = expectedValue,
            actualValue = actualValue,
            baseUnitValue = unitSnapshot?.base?.value,
            stockUnitValue = unitSnapshot?.stock?.value,
            purchaseUnitValue = unitSnapshot?.purchase?.value,
            salesUnitValue = unitSnapshot?.sales?.value,
            baseUnitCorrect = unitSnapshot?.base?.correct,
            stockUnitCorrect = unitSnapshot?.stock?.correct,
            purchaseUnitCorrect = unitSnapshot?.purchase?.correct,
            salesUnitCorrect = unitSnapshot?.sales?.correct,
            baseUnitModifiedAt = unitSnapshot?.base?.modifiedAt,
            stockUnitModifiedAt = unitSnapshot?.stock?.modifiedAt,
            purchaseUnitModifiedAt = unitSnapshot?.purchase?.modifiedAt,
            salesUnitModifiedAt = unitSnapshot?.sales?.modifiedAt,
            baseUnitStatus = unitSnapshot?.base?.status,
            stockUnitStatus = unitSnapshot?.stock?.status,
            purchaseUnitStatus = unitSnapshot?.purchase?.status,
            salesUnitStatus = unitSnapshot?.sales?.status,
        )

    private fun formatUom(uom: UomConversion?): String {
        if (uom == null) return "-"
        val code = uom.code?.trim().orEmpty()
        val desc = uom.udfudesc?.trim().orEmpty()
        return when {
            code.isNotEmpty() && desc.isNotEmpty() -> "$code / $desc"
            desc.isNotEmpty() -> desc
            code.isNotEmpty() -> code
            else -> "-"
        }
    }

    private fun formatUomById(uomId: Long?, uomCache: Map<Long, UomConversion> = emptyMap()): String {
        if (uomId == null) return "-"
        val uom = uomCache[uomId] ?: uomConversionService.findById(uomId)
        return formatUom(uom)
    }

    private fun sortIssues(issues: List<MasterDataIssueResponse>): List<MasterDataIssueResponse> =
        issues.sortedWith(
            compareBy(
                { it.scope },
                { it.bomCode?.uppercase() ?: "" },
                { it.itemCode?.uppercase() ?: "" },
                { it.issueCode },
                { it.bomId ?: 0L },
                { it.itemId ?: 0L },
            ),
        )
}
