package com.ffii.fpsms.modules.stock.trace

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.stock.web.model.*
import org.springframework.stereotype.Component

data class BomStepAssignment(
    val bomProcessId: Long,
    val bomProcessSeqNo: Long,
    val stepName: String,
)

@Component
open class TraceBomLoader(
    private val jdbcDao: JdbcDao,
    private val sql: TraceSqlSupport,
    private val lotResolver: TraceLotResolver,
) {
    open fun itemCodeKey(code: String): String = code.trim().uppercase()

    open fun loadBomTrace(
        inventoryLotId: Long,
        primaryStockInLineId: Long?,
        itemId: Long,
        lotLineIds: List<Long>,
    ): ItemLotTraceBomTrace {
        val isFinishedGood = primaryStockInLineId?.let { hasJoStockIn(it) } == true
        val isMaterial = lotLineIds.isNotEmpty() && hasJoStockOut(lotLineIds)

        val direction = when {
            isFinishedGood -> "FINISHED_GOOD"
            isMaterial -> "MATERIAL"
            else -> "UNKNOWN"
        }

        val downstream = if (isMaterial) loadBomDownstream(lotLineIds) else emptyList()
        val upstream = if (isFinishedGood && primaryStockInLineId != null) {
            loadBomUpstream(primaryStockInLineId)
        } else {
            emptyList()
        }

        val bomRecipe = when {
            isFinishedGood && primaryStockInLineId != null -> loadBomRecipeForFgStockIn(primaryStockInLineId)
            isMaterial && downstream.isNotEmpty() -> {
                val joId = downstream.first().jobOrderId
                if (joId != null) loadBomRecipeForJobOrder(joId) else emptyList()
            }
            else -> emptyList()
        }

        return ItemLotTraceBomTrace(
            direction = direction,
            downstream = downstream,
            upstream = upstream,
            bomRecipe = bomRecipe,
        )
    }

    open fun hasJoStockIn(stockInLineId: Long): Boolean {
        val query = """
            SELECT COUNT(*) AS cnt FROM stock_in_line
            WHERE id = :stockInLineId AND deleted = 0 AND jobOrderId IS NOT NULL
        """.trimIndent()
        val cnt = jdbcDao.queryForList(query, mapOf("stockInLineId" to stockInLineId))
            .firstOrNull()?.let { (it["cnt"] as? Number)?.toLong() ?: 0L } ?: 0L
        return cnt > 0
    }

    /** Resolve the JO finished-goods stock-in line for a material lot (semi-finished from another JO). */
    open fun resolveJoProducedStockInLineId(
        materialStockInLineId: Long?,
        materialInventoryLotId: Long?,
        silIdsByLot: Map<Long, List<Long>>,
    ): Long? {
        materialStockInLineId?.takeIf { hasJoStockIn(it) }?.let { return it }
        materialInventoryLotId?.let { lotId ->
            silIdsByLot[lotId]
                ?.sortedDescending()
                ?.firstOrNull { hasJoStockIn(it) }
        }?.let { return it }
        if (materialInventoryLotId == null) return null
        val query = """
            SELECT id FROM stock_in_line
            WHERE inventoryLotId = :inventoryLotId AND deleted = 0 AND jobOrderId IS NOT NULL
            ORDER BY created DESC
            LIMIT 1
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotId" to materialInventoryLotId))
            .firstOrNull()?.let { sql.longVal(it["id"]) }
    }

    open fun isJoProducedMaterial(
        resolvedSilId: Long?,
        materialInventoryLotId: Long?,
        stockInOrigin: ItemLotTraceOrigin?,
    ): Boolean {
        if (resolvedSilId != null && hasJoStockIn(resolvedSilId)) return true
        if (stockInOrigin?.type.equals("JO", ignoreCase = true)) return true
        return materialInventoryLotId?.let { lotResolver.resolveJobOrderIdForLot(it, null) != null } == true
    }

    open fun hasJoStockOut(lotLineIds: List<Long>): Boolean {
        val query = """
            SELECT COUNT(*) AS cnt
            FROM stock_out_line sol
            INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0
            WHERE sol.deleted = 0 AND sol.inventoryLotLineId IN (:lotLineIds)
              AND so.type IN ('job', 'jo')
        """.trimIndent()
        val cnt = jdbcDao.queryForList(query, mapOf("lotLineIds" to lotLineIds))
            .firstOrNull()?.let { (it["cnt"] as? Number)?.toLong() ?: 0L } ?: 0L
        return cnt > 0
    }

    open fun loadBomDownstream(lotLineIds: List<Long>): List<ItemLotTraceBomDownstream> {
        val query = """
            SELECT DISTINCT
                COALESCE(jo.code, '') AS jobOrderCode,
                jo.id AS jobOrderId,
                COALESCE(fg_stock_it.code, fg_bom_it.code, '') AS finishedItemCode,
                COALESCE(fg_il.lotNo, fg_sil.lotNo, '') AS finishedLotNo,
                fg_sil.id AS finishedStockInLineId,
                COALESCE(fg_sil.acceptedQty, 0) AS fgQty,
                COALESCE(fg_uc.udfudesc, '') AS fgUom,
                COALESCE(sol.qty, 0) AS materialQtyUsed
            FROM stock_out_line sol
            INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0
                AND so.type IN ('job', 'jo')
            INNER JOIN pick_order_line pol ON sol.pickOrderLineId = pol.id AND pol.deleted = 0
            INNER JOIN pick_order po ON pol.poId = po.id AND po.deleted = 0 AND po.joId IS NOT NULL
            INNER JOIN job_order jo ON po.joId = jo.id AND jo.deleted = 0
            LEFT JOIN bom b ON jo.bomId = b.id AND b.deleted = 0
            LEFT JOIN items fg_bom_it ON b.itemId = fg_bom_it.id AND fg_bom_it.deleted = 0
            LEFT JOIN stock_in_line fg_sil ON fg_sil.jobOrderId = jo.id AND fg_sil.deleted = 0
            LEFT JOIN inventory_lot fg_il ON fg_sil.inventoryLotId = fg_il.id AND fg_il.deleted = 0
            LEFT JOIN items fg_stock_it ON fg_il.itemId = fg_stock_it.id AND fg_stock_it.deleted = 0
            LEFT JOIN items fg_item ON fg_item.id = COALESCE(fg_stock_it.id, fg_bom_it.id)
            LEFT JOIN item_uom fg_iu ON fg_item.id = fg_iu.itemId AND fg_iu.stockUnit = 1 AND fg_iu.deleted = 0
            LEFT JOIN uom_conversion fg_uc ON fg_iu.uomId = fg_uc.id AND fg_uc.deleted = 0
            WHERE sol.deleted = 0 AND sol.inventoryLotLineId IN (:lotLineIds)
            ORDER BY jobOrderCode, finishedLotNo
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("lotLineIds" to lotLineIds)).map { row ->
            ItemLotTraceBomDownstream(
                jobOrderCode = sql.str(row["jobOrderCode"]),
                jobOrderId = sql.longVal(row["jobOrderId"]),
                finishedItemCode = sql.str(row["finishedItemCode"]),
                finishedLotNo = sql.str(row["finishedLotNo"]),
                finishedStockInLineId = sql.longVal(row["finishedStockInLineId"]),
                fgQty = sql.decimal(row["fgQty"]),
                fgUom = sql.str(row["fgUom"]),
                materialQtyUsed = sql.decimal(row["materialQtyUsed"]),
            )
        }
    }

    open fun loadBomUpstream(primaryStockInLineId: Long): List<ItemLotTraceBomUpstream> {
        val query = """
            SELECT
                COALESCE(jo.code, '') AS jobOrderCode,
                jo.id AS jobOrderId,
                COALESCE(mat_it.code, '') AS materialItemCode,
                COALESCE(mat_il.lotNo, mat_sil.lotNo, '') AS materialLotNo,
                mat_sil.id AS materialStockInLineId,
                COALESCE(sol.qty, 0) AS materialQty,
                bm.qty AS bomQtyPerUnit
            FROM stock_in_line fg_sil
            INNER JOIN job_order jo ON fg_sil.jobOrderId = jo.id AND jo.deleted = 0
            INNER JOIN pick_order po ON po.joId = jo.id AND po.deleted = 0
                AND po.type IN ('jo', 'JOB_ORDER')
            INNER JOIN pick_order_line pol ON pol.poId = po.id AND pol.deleted = 0
            INNER JOIN stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = 0
            INNER JOIN inventory_lot_line mat_ill ON sol.inventoryLotLineId = mat_ill.id AND mat_ill.deleted = 0
            INNER JOIN inventory_lot mat_il ON mat_ill.inventoryLotId = mat_il.id AND mat_il.deleted = 0
            LEFT JOIN stock_in_line mat_sil ON mat_il.stockInLineId = mat_sil.id AND mat_sil.deleted = 0
            LEFT JOIN items mat_it ON mat_il.itemId = mat_it.id AND mat_it.deleted = 0
            LEFT JOIN bom b ON jo.bomId = b.id AND b.deleted = 0
            LEFT JOIN bom_material bm ON bm.bomId = b.id AND bm.itemId = mat_it.id AND bm.deleted = 0
            WHERE fg_sil.id = :primaryStockInLineId AND fg_sil.deleted = 0
            ORDER BY mat_it.code, mat_il.lotNo
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("primaryStockInLineId" to primaryStockInLineId)).map { row ->
            ItemLotTraceBomUpstream(
                jobOrderCode = sql.str(row["jobOrderCode"]),
                jobOrderId = sql.longVal(row["jobOrderId"]),
                materialItemCode = sql.str(row["materialItemCode"]),
                materialLotNo = sql.str(row["materialLotNo"]),
                materialStockInLineId = sql.longVal(row["materialStockInLineId"]),
                materialQty = sql.decimal(row["materialQty"]),
                bomQtyPerUnit = row["bomQtyPerUnit"]?.let { sql.decimal(it) },
            )
        }
    }

    open fun loadBomRecipeForFgStockIn(primaryStockInLineId: Long): List<ItemLotTraceBomRecipeLine> {
        val joId = getJobOrderIdForFgStockIn(primaryStockInLineId)
        return if (joId != null) loadBomRecipeForJobOrder(joId) else emptyList()
    }

    open fun loadBomRecipeForJobOrder(jobOrderId: Long): List<ItemLotTraceBomRecipeLine> {
        val stepByItem = loadBomMaterialStepAssignments(jobOrderId)
        val query = """
            SELECT
                COALESCE(mat_it.code, '') AS materialItemCode,
                COALESCE(bm.itemName, mat_it.name, '') AS materialItemName,
                COALESCE(bm.qty, 0) AS qtyPerUnit,
                COALESCE(bm.uomName, uc.udfudesc, '') AS uom
            FROM job_order jo
            INNER JOIN bom b ON jo.bomId = b.id AND b.deleted = 0
            INNER JOIN bom_material bm ON bm.bomId = b.id AND bm.deleted = 0
            LEFT JOIN items mat_it ON bm.itemId = mat_it.id AND mat_it.deleted = 0
            LEFT JOIN uom_conversion uc ON bm.uomId = uc.id
            WHERE jo.id = :jobOrderId AND jo.deleted = 0
            ORDER BY mat_it.code
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("jobOrderId" to jobOrderId)).map { row ->
            val itemCode = sql.str(row["materialItemCode"])
            val assignment = stepByItem[itemCodeKey(itemCode)]
            ItemLotTraceBomRecipeLine(
                materialItemCode = itemCode,
                materialItemName = sql.str(row["materialItemName"]),
                qtyPerUnit = sql.decimal(row["qtyPerUnit"]),
                uom = sql.str(row["uom"]),
                bomProcessId = assignment?.bomProcessId,
                bomProcessSeqNo = assignment?.bomProcessSeqNo,
                assignedStepName = assignment?.stepName ?: "",
            )
        }
    }

    open fun loadBomMaterialStepAssignments(jobOrderId: Long): Map<String, BomStepAssignment> {
        val query = """
            SELECT
                COALESCE(it.code, '') AS materialItemCode,
                bp.id AS bomProcessId,
                bp.seqNo AS bomProcessSeqNo,
                COALESCE(p.name, bp.description, '') AS stepName
            FROM job_order jo
            INNER JOIN bom b ON jo.bomId = b.id AND b.deleted = 0
            INNER JOIN bom_material bm ON bm.bomId = b.id AND bm.deleted = 0
            INNER JOIN bom_process_material bpm ON bpm.bomMaterialId = bm.id AND bpm.deleted = 0
            INNER JOIN bom_process bp ON bp.id = bpm.bomProcessId AND bp.deleted = 0
            LEFT JOIN process p ON p.id = bp.processId AND p.deleted = 0
            LEFT JOIN items it ON bm.itemId = it.id AND it.deleted = 0
            WHERE jo.id = :jobOrderId AND jo.deleted = 0
            ORDER BY bp.seqNo, it.code
        """.trimIndent()
        val result = linkedMapOf<String, BomStepAssignment>()
        jdbcDao.queryForList(query, mapOf("jobOrderId" to jobOrderId)).forEach { row ->
            val key = itemCodeKey(sql.str(row["materialItemCode"]))
            if (key.isEmpty() || key in result) return@forEach
            val bomProcessId = sql.longVal(row["bomProcessId"]) ?: return@forEach
            result[key] = BomStepAssignment(
                bomProcessId = bomProcessId,
                bomProcessSeqNo = sql.longVal(row["bomProcessSeqNo"]) ?: 0L,
                stepName = sql.str(row["stepName"]),
            )
        }
        return result
    }

    open fun loadStepMaterialsByJobOrder(jobOrderId: Long): Map<Long, List<ItemLotTraceStepMaterialRef>> {
        val query = """
            SELECT
                bp.id AS bomProcessId,
                COALESCE(it.code, '') AS materialItemCode,
                COALESCE(bm.itemName, it.name, '') AS materialItemName,
                COALESCE(bm.qty, 0) AS qtyPerUnit,
                COALESCE(bm.uomName, uc.udfudesc, '') AS uom
            FROM job_order jo
            INNER JOIN bom b ON jo.bomId = b.id AND b.deleted = 0
            INNER JOIN bom_process bp ON bp.bomId = b.id AND bp.deleted = 0
            INNER JOIN bom_process_material bpm ON bpm.bomProcessId = bp.id AND bpm.deleted = 0
            INNER JOIN bom_material bm ON bm.id = bpm.bomMaterialId AND bm.deleted = 0
            LEFT JOIN items it ON bm.itemId = it.id AND it.deleted = 0
            LEFT JOIN uom_conversion uc ON bm.uomId = uc.id
            WHERE jo.id = :jobOrderId AND jo.deleted = 0
            ORDER BY bp.seqNo, it.code
        """.trimIndent()
        val grouped = linkedMapOf<Long, MutableList<ItemLotTraceStepMaterialRef>>()
        jdbcDao.queryForList(query, mapOf("jobOrderId" to jobOrderId)).forEach { row ->
            val bomProcessId = sql.longVal(row["bomProcessId"]) ?: return@forEach
            val itemCode = sql.str(row["materialItemCode"])
            val list = grouped.getOrPut(bomProcessId) { mutableListOf() }
            if (list.any { it.materialItemCode.equals(itemCode, ignoreCase = true) }) return@forEach
            list.add(
                ItemLotTraceStepMaterialRef(
                    materialItemCode = itemCode,
                    materialItemName = sql.str(row["materialItemName"]),
                    qtyPerUnit = sql.decimal(row["qtyPerUnit"]),
                    uom = sql.str(row["uom"]),
                ),
            )
        }
        return grouped
    }

    open fun getJobOrderIdForFgStockIn(primaryStockInLineId: Long): Long? {
        val query = """
            SELECT jo.id AS jobOrderId FROM stock_in_line sil
            INNER JOIN job_order jo ON sil.jobOrderId = jo.id AND jo.deleted = 0
            WHERE sil.id = :primaryStockInLineId AND sil.deleted = 0
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("primaryStockInLineId" to primaryStockInLineId))
            .firstOrNull()?.let { sql.longVal(it["jobOrderId"]) }
    }
}
