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

import com.ffii.core.support.JdbcDao
import org.springframework.stereotype.Component

/**
 * Resolves upstream inventory lots for trace merge after warehouse transfer / lot split.
 * Merges PO, JO, QC, putaway history from source lots into the traced (destination) lot view
 * via [ItemLotTraceOrchestrator] calling [resolveUpstreamLotIds] (siblings with the same
 * lotNo are left to alternateLocations / locationBlocks).
 */
@Component
open class TraceUpstreamLoader(
    private val jdbcDao: JdbcDao,
    private val sql: TraceSqlSupport,
) {
    data class JoProvenance(
        val jobOrderId: Long,
        val joStockInLineId: Long,
        val upstreamLotIds: List<Long>,
    )

    /**
     * Inbound transfer sources for [inventoryLotId] (this lot received qty via transfer).
     * Mirrors [TraceMovementLoader.loadTransfers] lot matching (sil + ill_to on dest, ill_from on source).
     */
    open fun findInboundTransferSourceLotIds(inventoryLotId: Long): List<Long> {
        val query = """
            SELECT ill_from.inventoryLotId AS sourceInventoryLotId
            FROM stock_transfer_record str
            LEFT JOIN stock_in_line sil ON str.stockInLineId = sil.id AND sil.deleted = 0
            LEFT JOIN stock_out_line sol ON str.stockOutLineId = sol.id AND sol.deleted = 0
            LEFT JOIN inventory_lot_line ill_from ON sol.inventoryLotLineId = ill_from.id AND ill_from.deleted = 0
            LEFT JOIN inventory_lot_line ill_to ON sil.inventoryLotLineId = ill_to.id AND ill_to.deleted = 0
            WHERE str.deleted = 0
              AND ill_from.inventoryLotId IS NOT NULL
              AND ill_from.inventoryLotId <> :inventoryLotId
              AND (
                sil.inventoryLotId = :inventoryLotId
                OR ill_to.inventoryLotId = :inventoryLotId
              )
            GROUP BY ill_from.inventoryLotId
            ORDER BY MIN(str.id) ASC
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotId" to inventoryLotId))
            .mapNotNull { sql.longVal(it["sourceInventoryLotId"]) }
            .distinct()
    }

    /** Walk inbound transfer chain and collect every source lot (PO / JO / any origin). */
    open fun collectTransferUpstreamLotIds(tracedLotId: Long, maxDepth: Int = 12): List<Long> {
        val found = linkedSetOf<Long>()
        var frontier = listOf(tracedLotId)
        val visited = mutableSetOf(tracedLotId)
        repeat(maxDepth) {
            val sources = frontier.flatMap { lotId -> findInboundTransferSourceLotIds(lotId) }
                .filter { it !in visited }
            if (sources.isEmpty()) return found.toList()
            sources.forEach { id ->
                found.add(id)
                visited.add(id)
            }
            frontier = sources
        }
        return found.toList()
    }

    /** Other inventory lots with the same lotNo+itemId (transfer split / multi-warehouse). */
    open fun findSiblingLotIdsBySameLotNo(
        itemId: Long,
        lotNo: String,
        excludeInventoryLotId: Long,
    ): List<Long> {
        if (lotNo.isBlank()) return emptyList()
        val query = """
            SELECT il.id AS inventoryLotId
            FROM inventory_lot il
            WHERE il.deleted = 0
              AND il.itemId = :itemId
              AND il.lotNo = :lotNo
              AND il.id <> :excludeInventoryLotId
            ORDER BY il.stockInDate ASC, il.id ASC
        """.trimIndent()
        return jdbcDao.queryForList(
            query,
            mapOf("itemId" to itemId, "lotNo" to lotNo, "excludeInventoryLotId" to excludeInventoryLotId),
        ).mapNotNull { sql.longVal(it["inventoryLotId"]) }.distinct()
    }

    /** JO receipt lots with same lotNo (legacy helper for JO-only provenance). */
    open fun findJoSourceLotIdsBySameLotNo(
        itemId: Long,
        lotNo: String,
        excludeInventoryLotId: Long,
    ): List<Long> {
        if (lotNo.isBlank()) return emptyList()
        val query = """
            SELECT il.id AS inventoryLotId
            FROM inventory_lot il
            INNER JOIN stock_in_line sil ON sil.inventoryLotId = il.id AND sil.deleted = 0
            WHERE il.deleted = 0
              AND il.itemId = :itemId
              AND il.lotNo = :lotNo
              AND il.id <> :excludeInventoryLotId
              AND sil.jobOrderId IS NOT NULL
            GROUP BY il.id
            ORDER BY MIN(sil.receiptDate) ASC, il.id ASC
        """.trimIndent()
        return jdbcDao.queryForList(
            query,
            mapOf("itemId" to itemId, "lotNo" to lotNo, "excludeInventoryLotId" to excludeInventoryLotId),
        ).mapNotNull { sql.longVal(it["inventoryLotId"]) }.distinct()
    }

    open fun findJoSourceLotIdsByProductLotNo(tracedInventoryLotId: Long): List<Long> {
        val query = """
            SELECT il_jo.id AS inventoryLotId
            FROM stock_in_line sil_trace
            INNER JOIN inventory_lot il_trace ON sil_trace.inventoryLotId = il_trace.id AND il_trace.deleted = 0
            INNER JOIN stock_in_line sil_jo ON sil_jo.deleted = 0
                AND sil_jo.jobOrderId IS NOT NULL
                AND sil_jo.productLotNo IS NOT NULL
                AND TRIM(sil_jo.productLotNo) <> ''
                AND sil_jo.productLotNo = sil_trace.productLotNo
            INNER JOIN inventory_lot il_jo ON sil_jo.inventoryLotId = il_jo.id AND il_jo.deleted = 0
            WHERE sil_trace.deleted = 0
              AND sil_trace.inventoryLotId = :tracedInventoryLotId
              AND il_jo.id <> :tracedInventoryLotId
            GROUP BY il_jo.id
            ORDER BY MIN(sil_jo.receiptDate) ASC, il_jo.id ASC
        """.trimIndent()
        val fromSil = jdbcDao.queryForList(query, mapOf("tracedInventoryLotId" to tracedInventoryLotId))
            .mapNotNull { sql.longVal(it["inventoryLotId"]) }
        if (fromSil.isNotEmpty()) return fromSil.distinct()

        val viaLotLine = """
            SELECT il_jo.id AS inventoryLotId
            FROM inventory_lot_line ill_trace
            INNER JOIN stock_in_line sil_trace ON sil_trace.inventoryLotLineId = ill_trace.id AND sil_trace.deleted = 0
            INNER JOIN stock_in_line sil_jo ON sil_jo.deleted = 0
                AND sil_jo.jobOrderId IS NOT NULL
                AND sil_jo.productLotNo IS NOT NULL
                AND TRIM(sil_jo.productLotNo) <> ''
                AND sil_jo.productLotNo = sil_trace.productLotNo
            INNER JOIN inventory_lot il_jo ON sil_jo.inventoryLotId = il_jo.id AND il_jo.deleted = 0
            WHERE ill_trace.deleted = 0
              AND ill_trace.inventoryLotId = :tracedInventoryLotId
              AND il_jo.id <> :tracedInventoryLotId
            GROUP BY il_jo.id
            ORDER BY MIN(sil_jo.receiptDate) ASC, il_jo.id ASC
        """.trimIndent()
        return jdbcDao.queryForList(viaLotLine, mapOf("tracedInventoryLotId" to tracedInventoryLotId))
            .mapNotNull { sql.longVal(it["inventoryLotId"]) }
            .distinct()
    }

    /**
     * All upstream lots whose inbound history should be merged into the trace response.
     */
    open fun resolveUpstreamLotIds(tracedLotId: Long, itemId: Long, lotNo: String): List<Long> {
        val ids = linkedSetOf<Long>()
        collectTransferUpstreamLotIds(tracedLotId).forEach { ids.add(it) }
        findSiblingLotIdsBySameLotNo(itemId, lotNo, tracedLotId).forEach { ids.add(it) }
        findJoSourceLotIdsByProductLotNo(tracedLotId).forEach { ids.add(it) }
        return ids.filter { it != tracedLotId }
    }

    open fun findJoStockInLineId(inventoryLotId: Long): Long? {
        val query = """
            SELECT sil.id AS stockInLineId
            FROM stock_in_line sil
            LEFT JOIN inventory_lot_line ill ON sil.inventoryLotLineId = ill.id AND ill.deleted = 0
            WHERE sil.deleted = 0
              AND sil.jobOrderId IS NOT NULL
              AND (
                sil.inventoryLotId = :inventoryLotId
                OR ill.inventoryLotId = :inventoryLotId
              )
            ORDER BY sil.receiptDate DESC, sil.id DESC
            LIMIT 1
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotId" to inventoryLotId))
            .firstOrNull()?.let { sql.longVal(it["stockInLineId"]) }
    }

    open fun findJobOrderIdForStockInLine(stockInLineId: 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 = :stockInLineId AND sil.deleted = 0
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("stockInLineId" to stockInLineId))
            .firstOrNull()?.let { sql.longVal(it["jobOrderId"]) }
    }

    private fun provenanceFromJoLot(joLotId: Long, tracedLotId: Long): JoProvenance? {
        val joSil = findJoStockInLineId(joLotId) ?: return null
        val joId = findJobOrderIdForStockInLine(joSil) ?: return null
        val upstream = if (joLotId == tracedLotId) emptyList() else listOf(joLotId)
        return JoProvenance(jobOrderId = joId, joStockInLineId = joSil, upstreamLotIds = upstream)
    }

    private fun walkTransferChainForJo(tracedLotId: Long, maxDepth: Int): JoProvenance? {
        val pathFromTraced = mutableListOf<Long>()
        var current = tracedLotId
        val visited = mutableSetOf<Long>()

        while (current !in visited && pathFromTraced.size < maxDepth) {
            visited.add(current)
            val joSil = findJoStockInLineId(current)
            if (joSil != null) {
                val joId = findJobOrderIdForStockInLine(joSil) ?: return null
                val upstream = if (current == tracedLotId) {
                    emptyList()
                } else {
                    (pathFromTraced + current).distinct().filter { it != tracedLotId }
                }
                return JoProvenance(jobOrderId = joId, joStockInLineId = joSil, upstreamLotIds = upstream)
            }
            val sources = findInboundTransferSourceLotIds(current).filter { it !in visited }
            val next = sources.firstOrNull() ?: break
            pathFromTraced.add(next)
            current = next
        }
        return null
    }

    open fun resolveJoProvenance(
        tracedLotId: Long,
        itemId: Long? = null,
        lotNo: String? = null,
        maxDepth: Int = 12,
    ): JoProvenance? {
        walkTransferChainForJo(tracedLotId, maxDepth)?.let { return it }
        if (itemId != null && !lotNo.isNullOrBlank()) {
            findJoSourceLotIdsBySameLotNo(itemId, lotNo, tracedLotId)
                .firstOrNull()
                ?.let { return provenanceFromJoLot(it, tracedLotId) }
        }
        findJoSourceLotIdsByProductLotNo(tracedLotId)
            .firstOrNull()
            ?.let { return provenanceFromJoLot(it, tracedLotId) }
        return null
    }
}
