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
import java.math.BigDecimal

data class MaterialInputRow(
    val jobOrderCode: String,
    val jobOrderId: Long?,
    val materialItemCode: String,
    val materialItemName: String,
    val materialLotNo: String,
    val materialStockInLineId: Long?,
    val materialInventoryLotId: Long?,
    val pickedInventoryLotLineId: Long?,
    val pickedWarehouseCode: String,
    val materialQty: BigDecimal,
    val materialUom: String,
    val bomQtyPerUnit: BigDecimal?,
    val pickOrderCode: String,
    val pickOrderId: Long?,
    val consoCode: String,
    val pickedAt: String?,
    val processingStatus: String,
    val matchStatus: String,
)

@Component
open class TraceJoPreludeLoader(
    private val jdbcDao: JdbcDao,
    private val sql: TraceSqlSupport,
    private val bomLoader: TraceBomLoader,
    private val productionLoader: TraceProductionLoader,
    private val inboundLoader: TraceInboundLoader,
    private val lotResolver: TraceLotResolver,
) {
    open fun loadJoPrelude(primaryStockInLineId: Long): ItemLotTraceJoPrelude? {
        val joId = bomLoader.getJobOrderIdForFgStockIn(primaryStockInLineId) ?: return null
        val jobOrder = loadJoContext(joId) ?: return null
        val pickOrders = loadJoPickOrders(joId)
        val materialInputs = loadJoMaterialInputs(primaryStockInLineId, mutableSetOf())
        return ItemLotTraceJoPrelude(
            jobOrder = jobOrder,
            pickOrders = pickOrders,
            materialInputs = materialInputs,
        )
    }

    open fun loadJoContext(jobOrderId: Long): ItemLotTraceJoContext? {
        val query = """
            SELECT
                jo.id AS jobOrderId,
                COALESCE(jo.code, '') AS jobOrderCode,
                jo.planStart AS planStart,
                (
                    SELECT MIN(jpo.created)
                    FROM pick_order po
                    INNER JOIN jo_pick_order jpo
                        ON jpo.pick_order_id = po.id AND jpo.deleted = 0
                    WHERE po.joId = jo.id AND po.deleted = 0
                ) AS createdAt,
                COALESCE(jo.reqQty, 0) AS reqQty,
                COALESCE(jo.status, '') AS status
            FROM job_order jo
            WHERE jo.id = :jobOrderId AND jo.deleted = 0
        """.trimIndent()
        val row = jdbcDao.queryForList(query, mapOf("jobOrderId" to jobOrderId)).firstOrNull() ?: return null
        return ItemLotTraceJoContext(
            jobOrderId = sql.longVal(row["jobOrderId"]) ?: jobOrderId,
            jobOrderCode = sql.str(row["jobOrderCode"]),
            planStart = sql.formatDateTime(row["planStart"]),
            createdAt = sql.formatDateTime(row["createdAt"]),
            reqQty = sql.decimal(row["reqQty"]),
            status = sql.str(row["status"]),
        )
    }

    open fun loadJoPickOrders(jobOrderId: Long): List<ItemLotTraceJoPickOrder> {
        val headerSql = """
            SELECT
                po.id AS pickOrderId,
                COALESCE(po.code, '') AS pickOrderCode,
                COALESCE(po.consoCode, '') AS consoCode,
                COALESCE(po.status, '') AS status,
                po.targetDate AS targetDate,
                po.completeDate AS completeDate,
                po.releasedDate AS releasedDate
            FROM pick_order po
            WHERE po.joId = :jobOrderId AND po.deleted = 0
              AND po.type IN ('jo', 'JOB_ORDER')
            ORDER BY po.code
        """.trimIndent()
        val headers = jdbcDao.queryForList(headerSql, mapOf("jobOrderId" to jobOrderId))
        if (headers.isEmpty()) return emptyList()

        val lineSql = """
            SELECT
                pol.poId AS pickOrderId,
                pol.id AS pickOrderLineId,
                COALESCE(it.code, '') AS itemCode,
                COALESCE(it.name, '') AS itemName,
                COALESCE(pol.qty, 0) AS requiredQty,
                COALESCE(pol.status, '') AS lineStatus,
                COALESCE(SUM(sol.qty), 0) AS pickedQty
            FROM pick_order_line pol
            INNER JOIN pick_order po ON pol.poId = po.id AND po.deleted = 0
            LEFT JOIN items it ON pol.itemId = it.id AND it.deleted = 0
            LEFT JOIN stock_out_line sol ON sol.pickOrderLineId = pol.id AND sol.deleted = 0
            WHERE po.joId = :jobOrderId AND pol.deleted = 0
              AND po.type IN ('jo', 'JOB_ORDER')
            GROUP BY pol.id, pol.poId, it.code, it.name, pol.qty, pol.status
            ORDER BY it.code
        """.trimIndent()
        val linesByPo = jdbcDao.queryForList(lineSql, mapOf("jobOrderId" to jobOrderId))
            .groupBy { sql.longVal(it["pickOrderId"]) }

        return headers.map { row ->
            val poId = sql.longVal(row["pickOrderId"]) ?: 0L
            val lines = (linesByPo[poId] ?: emptyList()).map { lineRow ->
                ItemLotTraceJoPickLine(
                    pickOrderLineId = sql.longVal(lineRow["pickOrderLineId"]) ?: 0L,
                    itemCode = sql.str(lineRow["itemCode"]),
                    itemName = sql.str(lineRow["itemName"]),
                    requiredQty = sql.decimal(lineRow["requiredQty"]),
                    pickedQty = sql.decimal(lineRow["pickedQty"]),
                    status = sql.str(lineRow["lineStatus"]),
                )
            }
            ItemLotTraceJoPickOrder(
                pickOrderId = poId,
                pickOrderCode = sql.str(row["pickOrderCode"]),
                consoCode = sql.str(row["consoCode"]),
                status = sql.str(row["status"]),
                targetDate = sql.formatDate(row["targetDate"]),
                completeDate = sql.formatDateTime(row["completeDate"]),
                releasedDate = sql.formatDateTime(row["releasedDate"]),
                lines = lines,
            )
        }
    }

    open fun loadJoMaterialInputs(
        primaryStockInLineId: Long,
        visitedStockInLineIds: MutableSet<Long>,
    ): List<ItemLotTraceMaterialInput> {
        if (primaryStockInLineId in visitedStockInLineIds) return emptyList()
        visitedStockInLineIds.add(primaryStockInLineId)

        val query = """
            SELECT
                COALESCE(jo.code, '') AS jobOrderCode,
                jo.id AS jobOrderId,
                COALESCE(mat_it.code, '') AS materialItemCode,
                COALESCE(mat_it.name, '') AS materialItemName,
                COALESCE(mat_il.lotNo, mat_sil.lotNo, '') AS materialLotNo,
                mat_sil.id AS materialStockInLineId,
                mat_il.id AS materialInventoryLotId,
                mat_ill.id AS pickedInventoryLotLineId,
                COALESCE(wh_pick.code, '') AS pickedWarehouseCode,
                COALESCE(sol.qty, 0) AS materialQty,
                COALESCE(mat_uc.udfudesc, '') AS materialUom,
                bm.qty AS bomQtyPerUnit,
                COALESCE(po.code, '') AS pickOrderCode,
                po.id AS pickOrderId,
                COALESCE(po.consoCode, '') AS consoCode,
                COALESCE(sol.endTime, sol.pickTime, sol.created) AS pickedAt,
                CASE
                    WHEN sol.status = 'completed' THEN 'completed'
                    WHEN sol.status = 'rejected' THEN 'rejected'
                    WHEN sol.status = 'created' THEN 'pending'
                    ELSE 'pending'
                END AS processingStatus,
                COALESCE(jpo.match_status, '') AS matchStatus
            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 warehouse wh_pick ON mat_ill.warehouseId = wh_pick.id AND wh_pick.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 item_uom mat_iu ON mat_it.id = mat_iu.itemId AND mat_iu.stockUnit = 1 AND mat_iu.deleted = 0
            LEFT JOIN uom_conversion mat_uc ON mat_iu.uomId = mat_uc.id AND mat_uc.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
            LEFT JOIN jo_pick_order jpo ON jpo.pick_order_id = po.id
                AND jpo.item_id = mat_it.id AND jpo.deleted = 0
            WHERE fg_sil.id = :primaryStockInLineId AND fg_sil.deleted = 0
            ORDER BY mat_it.code, mat_il.lotNo, sol.created
        """.trimIndent()
        val rows = jdbcDao.queryForList(query, mapOf("primaryStockInLineId" to primaryStockInLineId)).map { row ->
            MaterialInputRow(
                jobOrderCode = sql.str(row["jobOrderCode"]),
                jobOrderId = sql.longVal(row["jobOrderId"]),
                materialItemCode = sql.str(row["materialItemCode"]),
                materialItemName = sql.str(row["materialItemName"]),
                materialLotNo = sql.str(row["materialLotNo"]),
                materialStockInLineId = sql.longVal(row["materialStockInLineId"]),
                materialInventoryLotId = sql.longVal(row["materialInventoryLotId"]),
                pickedInventoryLotLineId = sql.longVal(row["pickedInventoryLotLineId"]),
                pickedWarehouseCode = sql.str(row["pickedWarehouseCode"]),
                materialQty = sql.decimal(row["materialQty"]),
                materialUom = sql.str(row["materialUom"]),
                bomQtyPerUnit = row["bomQtyPerUnit"]?.let { sql.decimal(it) },
                pickOrderCode = sql.str(row["pickOrderCode"]),
                pickOrderId = sql.longVal(row["pickOrderId"]),
                consoCode = sql.str(row["consoCode"]),
                pickedAt = sql.formatDateTime(row["pickedAt"]),
                processingStatus = sql.str(row["processingStatus"]),
                matchStatus = sql.str(row["matchStatus"]),
            )
        }
        if (rows.isEmpty()) return emptyList()

        val joIdForMapping = rows.firstNotNullOfOrNull { it.jobOrderId }
        val stepAssignments = joIdForMapping?.let { bomLoader.loadBomMaterialStepAssignments(it) } ?: emptyMap()

        val lotIds = rows.mapNotNull { it.materialInventoryLotId }.distinct()
        val silIdsByLot = lotResolver.loadStockInLineIdsForLots(lotIds)
        val allSilIds = silIdsByLot.values.flatten().distinct()
        val allOrigins = inboundLoader.loadOriginsForLots(lotIds)
        val allQc = inboundLoader.loadQcResults(allSilIds)
        val putawayByLot = inboundLoader.loadPutawayEventsByLotIds(lotIds)

        fun materialStockInOriginForLot(lotId: Long?): ItemLotTraceOrigin? {
            if (lotId == null) return null
            val silIds = silIdsByLot[lotId]?.toSet() ?: emptySet()
            if (silIds.isEmpty()) return null
            val origins = allOrigins.filter { it.stockInLineId in silIds }
            if (origins.isEmpty()) return null

            fun earliestOfType(type: String): ItemLotTraceOrigin? =
                origins
                    .filter { it.type.equals(type, ignoreCase = true) }
                    .minWithOrNull(
                        compareBy<ItemLotTraceOrigin> { it.receiptDate ?: "9999-12-31" }
                            .thenBy { it.stockInLineId },
                    )

            earliestOfType("PO")?.let { return it }
            earliestOfType("JO")?.let { return it }
            earliestOfType("TRANSFER")?.let { return it }

            return origins
                .filter { o ->
                    val t = o.type.uppercase()
                    t !in setOf("ADJ", "OPEN", "TKE")
                }
                .minWithOrNull(
                    compareBy<ItemLotTraceOrigin> { it.receiptDate ?: "9999-12-31" }
                        .thenBy { it.stockInLineId },
                )
                ?: origins.minWithOrNull(
                    compareBy<ItemLotTraceOrigin> { it.receiptDate ?: "9999-12-31" }
                        .thenBy { it.stockInLineId },
                )
        }

        fun qcForLot(lotId: Long?): List<ItemLotTraceQcResult> {
            if (lotId == null) return emptyList()
            val silIds = silIdsByLot[lotId]?.toSet() ?: emptySet()
            return allQc.filter { it.stockInLineId in silIds }
        }

        fun purchaseForLot(lotId: Long?): List<ItemLotTracePurchaseEvent> {
            if (lotId == null) return emptyList()
            val silIds = silIdsByLot[lotId].orEmpty()
            return inboundLoader.loadPurchaseEvents(silIds)
        }

        return rows.map { r ->
            val stockInOrigin = materialStockInOriginForLot(r.materialInventoryLotId)
            val resolvedSilId = bomLoader.resolveJoProducedStockInLineId(
                r.materialStockInLineId,
                r.materialInventoryLotId,
                silIdsByLot,
            )
            val joProduced = bomLoader.isJoProducedMaterial(
                resolvedSilId,
                r.materialInventoryLotId,
                stockInOrigin,
            )
            val producingJoId = resolvedSilId?.let { bomLoader.getJobOrderIdForFgStockIn(it) }
                ?: r.materialInventoryLotId?.let { lotResolver.resolveJobOrderIdForLot(it, resolvedSilId) }
            val nestedJoPrelude = if (joProduced && producingJoId != null) {
                val jobOrder = loadJoContext(producingJoId)
                if (jobOrder != null) {
                    val nestedSilId = resolvedSilId ?: producingJoId.let { joId ->
                        jdbcDao.queryForList(
                            """
                            SELECT sil.id AS id FROM stock_in_line sil
                            WHERE sil.jobOrderId = :joId AND sil.deleted = 0
                            ORDER BY sil.created DESC LIMIT 1
                            """.trimIndent(),
                            mapOf("joId" to joId),
                        ).firstOrNull()?.let { sql.longVal(it["id"]) }
                    }
                    val materialInputs = nestedSilId
                        ?.let { loadJoMaterialInputs(it, visitedStockInLineIds) }
                        ?: emptyList()
                    ItemLotTraceJoPrelude(
                        jobOrder = jobOrder,
                        pickOrders = loadJoPickOrders(producingJoId),
                        materialInputs = materialInputs,
                    )
                } else {
                    null
                }
            } else {
                null
            }
            val productionSteps = if (joProduced && producingJoId != null) {
                productionLoader.loadProductionSteps(producingJoId)
            } else {
                emptyList()
            }
            val assignment = stepAssignments[bomLoader.itemCodeKey(r.materialItemCode)]
            ItemLotTraceMaterialInput(
                jobOrderCode = r.jobOrderCode,
                jobOrderId = r.jobOrderId,
                materialItemCode = r.materialItemCode,
                materialItemName = r.materialItemName,
                materialLotNo = r.materialLotNo,
                materialStockInLineId = resolvedSilId ?: r.materialStockInLineId,
                materialInventoryLotId = r.materialInventoryLotId,
                materialQty = r.materialQty,
                materialUom = r.materialUom,
                bomQtyPerUnit = r.bomQtyPerUnit,
                bomProcessId = assignment?.bomProcessId,
                bomProcessSeqNo = assignment?.bomProcessSeqNo,
                assignedStepName = assignment?.stepName ?: "",
                pickOrderCode = r.pickOrderCode,
                pickOrderId = r.pickOrderId,
                consoCode = r.consoCode,
                pickedAt = r.pickedAt,
                processingStatus = r.processingStatus,
                matchStatus = r.matchStatus,
                stockInOrigin = stockInOrigin,
                purchaseEvents = purchaseForLot(r.materialInventoryLotId),
                qcResults = qcForLot(r.materialInventoryLotId),
                putawayEvents = inboundLoader.resolveMaterialPutawayEvents(
                    lotId = r.materialInventoryLotId,
                    origin = stockInOrigin,
                    pickedInventoryLotLineId = r.pickedInventoryLotLineId,
                    pickedWarehouseCode = r.pickedWarehouseCode,
                    putawayByLot = putawayByLot,
                ),
                nestedJoPrelude = nestedJoPrelude,
                productionSteps = productionSteps,
            )
        }
    }
}
