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

@Component
open class TraceInboundLoader(
    private val jdbcDao: JdbcDao,
    private val sql: TraceSqlSupport,
) {
    open fun loadOrigins(inventoryLotId: Long): List<ItemLotTraceOrigin> =
        loadOriginsForLots(listOf(inventoryLotId))

    open fun loadOriginsForLots(inventoryLotIds: List<Long>): List<ItemLotTraceOrigin> {
        if (inventoryLotIds.isEmpty()) return emptyList()
        val query = """
            SELECT
                sil.inventoryLotId AS inventoryLotId,
                sil.id AS stockInLineId,
                CASE
                    WHEN sil.purchaseOrderId IS NOT NULL THEN 'PO'
                    WHEN sil.jobOrderId IS NOT NULL THEN 'JO'
                    WHEN sil.stockTakeLineId IS NOT NULL THEN 'TKE'
                    WHEN sil.stockTransferId IS NOT NULL THEN 'TRANSFER'
                    WHEN UPPER(COALESCE(sil.type, '')) IN ('ADJ', 'OPEN') THEN UPPER(sil.type)
                    ELSE COALESCE(UPPER(sil.type), 'OTHER')
                END AS type,
                COALESCE(po.code, jo.code, st.code, si.code, '') AS refCode,
                COALESCE(po.id, jo.id, st.id, si.id) AS refId,
                COALESCE(sp_si.code, sp_po.code, '') AS supplierCode,
                COALESCE(sp_si.name, sp_po.name, '') AS supplierName,
                COALESCE(sil.dnNo, '') AS dnNo,
                sil.receiptDate AS receiptDate,
                COALESCE(sil.acceptedQty, 0) AS acceptedQty,
                COALESCE(sil.status, '') AS status
            FROM stock_in_line sil
            LEFT JOIN stock_in si ON sil.stockInId = si.id AND si.deleted = 0
            LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id AND po.deleted = 0
            LEFT JOIN job_order jo ON sil.jobOrderId = jo.id AND jo.deleted = 0
            LEFT JOIN stock_take_line stl ON sil.stockTakeLineId = stl.id AND stl.deleted = 0
            LEFT JOIN stock_take st ON stl.stockTakeId = st.id AND st.deleted = 0
            LEFT JOIN shop sp_si ON si.supplierId = sp_si.id AND sp_si.deleted = 0
            LEFT JOIN shop sp_po ON po.supplierId = sp_po.id AND sp_po.deleted = 0
            WHERE sil.inventoryLotId IN (:inventoryLotIds) AND sil.deleted = 0
            ORDER BY sil.receiptDate DESC, sil.id DESC
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotIds" to inventoryLotIds)).map { row ->
            ItemLotTraceOrigin(
                stockInLineId = sql.longVal(row["stockInLineId"]) ?: 0L,
                type = sql.str(row["type"]),
                refCode = sql.str(row["refCode"]),
                refId = sql.longVal(row["refId"]),
                supplierCode = sql.str(row["supplierCode"]),
                supplierName = sql.str(row["supplierName"]),
                dnNo = sql.str(row["dnNo"]),
                receiptDate = sql.formatDate(row["receiptDate"]),
                acceptedQty = sql.decimal(row["acceptedQty"]),
                status = sql.str(row["status"]),
            )
        }
    }

    open fun loadPurchaseEvents(stockInLineIds: List<Long>): List<ItemLotTracePurchaseEvent> {
        if (stockInLineIds.isEmpty()) return emptyList()
        val query = """
            SELECT
                pol.id AS purchaseOrderLineId,
                po.id AS purchaseOrderId,
                COALESCE(po.code, '') AS purchaseOrderCode,
                COALESCE(i.code, pol.itemNo, '') AS itemCode,
                COALESCE(i.name, '') AS itemName,
                COALESCE(pol.qtyM18, pol.qty, 0) AS orderQty,
                COALESCE(pa.putAwayQty, 0) AS putAwayQty,
                COALESCE(ucm18.udfudesc, uc.udfudesc, '') AS purchaseUnit,
                COALESCE(sp.code, '') AS supplierCode,
                COALESCE(sp.name, '') AS supplierName,
                po.orderDate AS orderDate,
                COALESCE(pol.status, '') AS status
            FROM stock_in_line sil
            INNER JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id AND pol.deleted = 0
            INNER JOIN purchase_order po ON pol.purchaseOrderId = po.id AND po.deleted = 0
            LEFT JOIN shop sp ON po.supplierId = sp.id AND sp.deleted = 0
            LEFT JOIN items i ON pol.itemId = i.id AND i.deleted = 0
            LEFT JOIN item_uom iu ON iu.itemId = pol.itemId AND iu.purchaseUnit = true AND iu.deleted = 0
            LEFT JOIN uom_conversion uc ON uc.id = iu.uomId AND uc.deleted = 0
            LEFT JOIN uom_conversion ucm18 ON ucm18.id = pol.uomIdM18 AND ucm18.deleted = 0
            LEFT JOIN (
                SELECT
                    sil_pa.purchaseOrderLineId,
                    SUM(
                        COALESCE(ill.inQty, 0)
                        * COALESCE(iu_s.ratioN, 1) / NULLIF(COALESCE(iu_s.ratioD, 1), 0)
                        / NULLIF(
                            COALESCE(iu_p.ratioN, 1) / NULLIF(COALESCE(iu_p.ratioD, 1), 0),
                            0
                        )
                    ) AS putAwayQty
                FROM stock_in_line sil_pa
                INNER JOIN inventory_lot_line ill ON sil_pa.inventoryLotLineId = ill.id AND ill.deleted = 0
                INNER JOIN items it_pa ON sil_pa.itemId = it_pa.id AND it_pa.deleted = 0
                LEFT JOIN item_uom iu_s ON iu_s.itemId = it_pa.id AND iu_s.stockUnit = true AND iu_s.deleted = 0
                LEFT JOIN item_uom iu_p ON iu_p.itemId = it_pa.id AND iu_p.purchaseUnit = true AND iu_p.deleted = 0
                WHERE sil_pa.deleted = 0
                  AND sil_pa.purchaseOrderLineId IS NOT NULL
                  AND COALESCE(ill.inQty, 0) > 0
                GROUP BY sil_pa.purchaseOrderLineId
            ) pa ON pa.purchaseOrderLineId = pol.id
            WHERE sil.deleted = 0
              AND sil.id IN (:stockInLineIds)
              AND sil.purchaseOrderLineId IS NOT NULL
            GROUP BY
                pol.id, po.id, po.code, i.code, pol.itemNo, i.name,
                pol.qtyM18, pol.qty, pa.putAwayQty, ucm18.udfudesc, uc.udfudesc,
                sp.code, sp.name, po.orderDate, pol.status
            ORDER BY po.orderDate ASC, pol.id ASC
        """.trimIndent()
        val fromLines = jdbcDao.queryForList(query, mapOf("stockInLineIds" to stockInLineIds)).map { row ->
            mapPurchaseEventRow(row)
        }
        val headerQuery = """
            SELECT
                po.id AS purchaseOrderId,
                COALESCE(po.code, '') AS purchaseOrderCode,
                COALESCE(i.code, '') AS itemCode,
                COALESCE(i.name, '') AS itemName,
                COALESCE(SUM(sil.acceptedQty), 0) AS orderQty,
                COALESCE(uc.udfudesc, '') AS purchaseUnit,
                COALESCE(sp.code, '') AS supplierCode,
                COALESCE(sp.name, '') AS supplierName,
                po.orderDate AS orderDate,
                COALESCE(MIN(sil.status), '') AS status
            FROM stock_in_line sil
            INNER JOIN purchase_order po ON sil.purchaseOrderId = po.id AND po.deleted = 0
            LEFT JOIN shop sp ON po.supplierId = sp.id AND sp.deleted = 0
            LEFT JOIN items i ON sil.itemId = i.id AND i.deleted = 0
            LEFT JOIN item_uom iu ON iu.itemId = sil.itemId AND iu.purchaseUnit = true AND iu.deleted = 0
            LEFT JOIN uom_conversion uc ON uc.id = iu.uomId AND uc.deleted = 0
            WHERE sil.deleted = 0
              AND sil.id IN (:stockInLineIds)
              AND sil.purchaseOrderId IS NOT NULL
              AND sil.purchaseOrderLineId IS NULL
            GROUP BY po.id, po.code, i.code, i.name, uc.udfudesc, sp.code, sp.name, po.orderDate
            ORDER BY po.orderDate ASC, po.id ASC
        """.trimIndent()
        val fromHeader = jdbcDao.queryForList(headerQuery, mapOf("stockInLineIds" to stockInLineIds)).map { row ->
            ItemLotTracePurchaseEvent(
                purchaseOrderLineId = 0L,
                purchaseOrderId = sql.longVal(row["purchaseOrderId"]),
                purchaseOrderCode = sql.str(row["purchaseOrderCode"]),
                itemCode = sql.str(row["itemCode"]),
                itemName = sql.str(row["itemName"]),
                orderQty = sql.decimal(row["orderQty"]),
                putAwayQty = java.math.BigDecimal.ZERO,
                purchaseUnit = sql.str(row["purchaseUnit"]),
                supplierCode = sql.str(row["supplierCode"]),
                supplierName = sql.str(row["supplierName"]),
                orderDate = sql.formatDateTime(row["orderDate"]),
                status = sql.str(row["status"]),
            )
        }
        return (fromLines + fromHeader).distinctBy {
            if (it.purchaseOrderLineId > 0L) "line-${it.purchaseOrderLineId}" else "po-${it.purchaseOrderId}"
        }
    }

    private fun mapPurchaseEventRow(row: Map<String, Any?>): ItemLotTracePurchaseEvent =
        ItemLotTracePurchaseEvent(
            purchaseOrderLineId = sql.longVal(row["purchaseOrderLineId"]) ?: 0L,
            purchaseOrderId = sql.longVal(row["purchaseOrderId"]),
            purchaseOrderCode = sql.str(row["purchaseOrderCode"]),
            itemCode = sql.str(row["itemCode"]),
            itemName = sql.str(row["itemName"]),
            orderQty = sql.decimal(row["orderQty"]),
            putAwayQty = sql.decimal(row["putAwayQty"]),
            purchaseUnit = sql.str(row["purchaseUnit"]),
            supplierCode = sql.str(row["supplierCode"]),
            supplierName = sql.str(row["supplierName"]),
            orderDate = sql.formatDateTime(row["orderDate"]),
            status = sql.str(row["status"]),
        )

    open fun loadQcResults(stockInLineIds: List<Long>): List<ItemLotTraceQcResult> {
        if (stockInLineIds.isEmpty()) return emptyList()
        val query = """
            SELECT
                qr.stockInLineId AS stockInLineId,
                COALESCE(qr.qcPassed, 1) AS qcPassed,
                COALESCE(qr.failQty, 0) AS failQty,
                COALESCE(sil.acceptedQty, 0) AS acceptedQty,
                COALESCE(qr.remarks, '') AS remarks,
                COALESCE(qr.createdBy, '') AS handledBy,
                qr.created AS created,
                COALESCE(qi.code, '') AS qcItemCode,
                COALESCE(qi.name, '') AS qcItemName,
                COALESCE(qi.description, '') AS qcItemDescription,
                CASE
                    WHEN sil.jobOrderId IS NOT NULL THEN 'EPQC'
                    WHEN sil.purchaseOrderId IS NOT NULL THEN 'IQC'
                    WHEN UPPER(COALESCE(qr.type, '')) IN ('IQC', 'IPQC', 'EPQC', 'FQC') THEN UPPER(qr.type)
                    ELSE 'IQC'
                END AS qcType
            FROM qc_result qr
            LEFT JOIN qc_item qi ON qr.qcItemId = qi.id AND qi.deleted = 0
            LEFT JOIN stock_in_line sil ON qr.stockInLineId = sil.id AND sil.deleted = 0
            WHERE qr.deleted = 0 AND qr.stockInLineId IN (:stockInLineIds)
            ORDER BY qr.created DESC, qi.code ASC
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("stockInLineIds" to stockInLineIds)).map { row ->
            ItemLotTraceQcResult(
                stockInLineId = sql.longVal(row["stockInLineId"]) ?: 0L,
                qcPassed = (row["qcPassed"] as? Number)?.toInt() != 0,
                failQty = sql.decimal(row["failQty"]),
                acceptedQty = sql.decimal(row["acceptedQty"]),
                remarks = sql.str(row["remarks"]),
                handledBy = sql.str(row["handledBy"]),
                created = sql.formatDateTime(row["created"]),
                qcItemCode = sql.str(row["qcItemCode"]),
                qcItemName = sql.str(row["qcItemName"]),
                qcItemDescription = sql.str(row["qcItemDescription"]),
                qcType = sql.str(row["qcType"]),
            )
        }
    }

    /** FP-MTMS Version Checklist | Functions Ref. No. 22 | v1.0.0 | 2026-07-20 */
    open fun loadPutawayEvents(inventoryLotId: Long): List<ItemLotTracePutawayEvent> =
        loadPutawayEventsByLotIds(listOf(inventoryLotId))[inventoryLotId].orEmpty()

    /**
     * Putaway card qty must be the original shelf-in amount, not the live [InventoryLotLine.inQty].
     * Stock-take 盤盈 adds variance onto the same lot line inQty (see applyVarianceAdjustment),
     * which would otherwise inflate the 已上架 node (e.g. 150 → 1500).
     */
    private fun putawayQtySql(illIdExpr: String): String = """
        GREATEST(
            COALESCE(ill.inQty, 0) - COALESCE((
                SELECT SUM(COALESCE(sil_tke.acceptedQty, 0))
                FROM stock_in_line sil_tke
                INNER JOIN stock_take_line stl_tke
                    ON sil_tke.stockTakeLineId = stl_tke.id AND stl_tke.deleted = 0
                WHERE stl_tke.inventoryLotLineId = $illIdExpr
                  AND sil_tke.deleted = 0
                  AND UPPER(COALESCE(sil_tke.type, '')) = 'TKE'
                  AND COALESCE(sil_tke.acceptedQty, 0) > 0
            ), 0),
            0
        )
    """.trimIndent()

    /** FP-MTMS Version Checklist | Functions Ref. No. 22 | v1.0.0 | 2026-07-20 */
    open fun loadPutawayEventsByLotIds(lotIds: List<Long>): Map<Long, List<ItemLotTracePutawayEvent>> {
        if (lotIds.isEmpty()) return emptyMap()
        val illPutawayQty = putawayQtySql("ill.id")
        val fromIllSql = """
            SELECT
                il.id AS inventoryLotId,
                ill.id AS inventoryLotLineId,
                COALESCE(sil_link.id, sil_primary.id) AS stockInLineId,
                COALESCE(wh.code, '') AS warehouseCode,
                $illPutawayQty AS qty,
                COALESCE(ill.createdBy, sil_link.createdBy, sil_primary.createdBy, '') AS handledBy,
                COALESCE(ill.created, sil_link.receiptDate, sil_primary.receiptDate, sil_link.created, sil_primary.created) AS timestamp,
                CASE
                    WHEN COALESCE(sil_link.purchaseOrderId, sil_primary.purchaseOrderId) IS NOT NULL THEN 'PO'
                    WHEN COALESCE(sil_link.jobOrderId, sil_primary.jobOrderId) IS NOT NULL THEN 'JO'
                    WHEN COALESCE(sil_link.stockTakeLineId, sil_primary.stockTakeLineId) IS NOT NULL THEN 'TKE'
                    WHEN COALESCE(sil_link.stockTransferId, sil_primary.stockTransferId) IS NOT NULL THEN 'TRANSFER'
                    WHEN UPPER(COALESCE(sil_link.type, sil_primary.type, '')) IN ('ADJ', 'OPEN') THEN UPPER(COALESCE(sil_link.type, sil_primary.type))
                    ELSE 'STOCK_IN'
                END AS refType,
                COALESCE(po.code, jo.code, st.code, si.code, '') AS refCode,
                COALESCE(po.id, jo.id, st.id, si.id) AS refId,
                COALESCE(sil_link.status, sil_primary.status, '') AS status
            FROM inventory_lot_line ill
            INNER JOIN inventory_lot il ON ill.inventoryLotId = il.id AND il.deleted = 0
            LEFT JOIN warehouse wh ON ill.warehouseId = wh.id AND wh.deleted = 0
            LEFT JOIN stock_in_line sil_link ON sil_link.inventoryLotLineId = ill.id AND sil_link.deleted = 0
            LEFT JOIN stock_in_line sil_primary ON sil_primary.id = il.stockInLineId AND sil_primary.deleted = 0
            LEFT JOIN stock_in si ON COALESCE(sil_link.stockInId, sil_primary.stockInId) = si.id AND si.deleted = 0
            LEFT JOIN purchase_order po ON COALESCE(sil_link.purchaseOrderId, sil_primary.purchaseOrderId) = po.id AND po.deleted = 0
            LEFT JOIN job_order jo ON COALESCE(sil_link.jobOrderId, sil_primary.jobOrderId) = jo.id AND jo.deleted = 0
            LEFT JOIN stock_take_line stl ON COALESCE(sil_link.stockTakeLineId, sil_primary.stockTakeLineId) = stl.id AND stl.deleted = 0
            LEFT JOIN stock_take st ON stl.stockTakeId = st.id AND st.deleted = 0
            WHERE il.id IN (:lotIds) AND ill.deleted = 0
        """.trimIndent()
        val fromSilSql = """
            SELECT
                il.id AS inventoryLotId,
                COALESCE(ill.id, sil.inventoryLotLineId) AS inventoryLotLineId,
                sil.id AS stockInLineId,
                COALESCE(wh.code, '') AS warehouseCode,
                CASE
                    WHEN ill.id IS NOT NULL THEN $illPutawayQty
                    ELSE COALESCE(sil.acceptedQty, 0)
                END AS qty,
                COALESCE(ill.createdBy, sil.createdBy, '') AS handledBy,
                COALESCE(ill.created, sil.receiptDate, sil.created) AS timestamp,
                CASE
                    WHEN sil.purchaseOrderId IS NOT NULL THEN 'PO'
                    WHEN sil.jobOrderId IS NOT NULL THEN 'JO'
                    WHEN sil.stockTakeLineId IS NOT NULL THEN 'TKE'
                    WHEN sil.stockTransferId IS NOT NULL THEN 'TRANSFER'
                    WHEN UPPER(COALESCE(sil.type, '')) IN ('ADJ', 'OPEN') THEN UPPER(sil.type)
                    ELSE 'STOCK_IN'
                END AS refType,
                COALESCE(po.code, jo.code, st.code, si.code, '') AS refCode,
                COALESCE(po.id, jo.id, st.id, si.id) AS refId,
                COALESCE(sil.status, '') AS status
            FROM stock_in_line sil
            INNER JOIN inventory_lot il ON sil.inventoryLotId = il.id AND il.deleted = 0
            LEFT JOIN inventory_lot_line ill ON sil.inventoryLotLineId = ill.id
            LEFT JOIN warehouse wh ON ill.warehouseId = wh.id AND wh.deleted = 0
            LEFT JOIN stock_in si ON sil.stockInId = si.id AND si.deleted = 0
            LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id AND po.deleted = 0
            LEFT JOIN job_order jo ON sil.jobOrderId = jo.id AND jo.deleted = 0
            LEFT JOIN stock_take_line stl ON sil.stockTakeLineId = stl.id AND stl.deleted = 0
            LEFT JOIN stock_take st ON stl.stockTakeId = st.id AND st.deleted = 0
            WHERE il.id IN (:lotIds) AND sil.deleted = 0 AND sil.inventoryLotLineId IS NOT NULL
        """.trimIndent()

        val grouped = mutableMapOf<Long, MutableList<ItemLotTracePutawayEvent>>()
        val seen = mutableSetOf<String>()
        fun addRow(row: Map<String, Any?>) {
            val lotId = sql.longVal(row["inventoryLotId"]) ?: return
            val illId = sql.longVal(row["inventoryLotLineId"]) ?: 0L
            val timestamp = sql.formatDateTime(row["timestamp"])
            val dedupeKey = "$lotId-$illId-$timestamp"
            if (!seen.add(dedupeKey)) return
            grouped.getOrPut(lotId) { mutableListOf() }.add(
                ItemLotTracePutawayEvent(
                    inventoryLotLineId = illId,
                    stockInLineId = sql.longVal(row["stockInLineId"]),
                    warehouseCode = sql.str(row["warehouseCode"]),
                    qty = sql.decimal(row["qty"]),
                    handledBy = sql.str(row["handledBy"]),
                    timestamp = timestamp,
                    refType = sql.str(row["refType"]),
                    refCode = sql.str(row["refCode"]),
                    refId = sql.longVal(row["refId"]),
                    status = sql.str(row["status"]),
                ),
            )
        }
        jdbcDao.queryForList(fromIllSql, mapOf("lotIds" to lotIds)).forEach(::addRow)
        jdbcDao.queryForList(fromSilSql, mapOf("lotIds" to lotIds)).forEach(::addRow)
        grouped.values.forEach { list ->
            list.sortWith(compareBy<ItemLotTracePutawayEvent> { it.timestamp ?: "" }.thenBy { it.inventoryLotLineId })
        }
        return grouped
    }

    open fun synthesizeMaterialPutawayEvent(
        origin: ItemLotTraceOrigin,
        pickedInventoryLotLineId: Long?,
        pickedWarehouseCode: String,
    ): ItemLotTracePutawayEvent =
        ItemLotTracePutawayEvent(
            inventoryLotLineId = pickedInventoryLotLineId ?: 0L,
            stockInLineId = origin.stockInLineId,
            warehouseCode = pickedWarehouseCode,
            qty = origin.acceptedQty,
            handledBy = "",
            timestamp = origin.receiptDate,
            refType = origin.type,
            refCode = origin.refCode,
            refId = origin.refId,
            status = origin.status,
        )

    open fun resolveMaterialPutawayEvents(
        lotId: Long?,
        origin: ItemLotTraceOrigin?,
        pickedInventoryLotLineId: Long?,
        pickedWarehouseCode: String,
        putawayByLot: Map<Long, List<ItemLotTracePutawayEvent>>,
    ): List<ItemLotTracePutawayEvent> {
        if (lotId == null) return emptyList()
        val fromLot = putawayByLot[lotId].orEmpty()
        if (fromLot.isNotEmpty()) return fromLot
        if (origin == null) return emptyList()

        val originType = origin.type.trim().uppercase()
        val immediateInbound = originType in setOf("ADJ", "OPEN", "JO", "TRANSFER")
        val poInbound = originType == "PO"
        if (!immediateInbound && !poInbound) return emptyList()

        return listOf(
            synthesizeMaterialPutawayEvent(
                origin = origin,
                pickedInventoryLotLineId = pickedInventoryLotLineId,
                pickedWarehouseCode = pickedWarehouseCode,
            ),
        )
    }
}
