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

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.web.model.*
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.server.ResponseStatusException

@Component
open class TraceLotResolver(
    private val jdbcDao: JdbcDao,
    private val inventoryLotRepository: InventoryLotRepository,
    private val inventoryLotLineRepository: InventoryLotLineRepository,
    private val stockInLineRepository: StockInLineRepository,
    private val sql: TraceSqlSupport,
) {
    open fun resolveInventoryLotId(
        stockInLineId: Long?,
        inventoryLotLineId: Long?,
        lotNo: String?,
        itemId: Long?,
    ): Long {
        stockInLineId?.let { silId ->
            val sil = stockInLineRepository.findById(silId).orElseThrow {
                ResponseStatusException(HttpStatus.NOT_FOUND, "stockInLineId $silId not found")
            }
            val lotId = sil.inventoryLot?.id
                ?: inventoryLotRepository.findFirstByStockInLineIdAndDeletedFalseOrderByIdAsc(silId)?.id
            return lotId ?: throw ResponseStatusException(
                HttpStatus.NOT_FOUND,
                "No inventory lot for stockInLineId $silId",
            )
        }

        inventoryLotLineId?.let { illId ->
            val ill = inventoryLotLineRepository.findById(illId).orElseThrow {
                ResponseStatusException(HttpStatus.NOT_FOUND, "inventoryLotLineId $illId not found")
            }
            return ill.inventoryLot?.id
                ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "No inventory lot for inventoryLotLineId $illId")
        }

        if (!lotNo.isNullOrBlank() && itemId != null) {
            val trimmed = lotNo.trim()
            val inventoryLotId = resolvePrimaryInventoryLotIdByLotNo(trimmed, itemId)
                ?: throw ResponseStatusException(
                    HttpStatus.NOT_FOUND,
                    "Lot not found: lotNo=$trimmed itemId=$itemId",
                )
            return inventoryLotId
        }

        throw ResponseStatusException(
            HttpStatus.BAD_REQUEST,
            "Provide stockInLineId, inventoryLotLineId, or lotNo+itemId/itemCode",
        )
    }

    /** lotNo+itemId is not unique — pick the lot with the most available qty (tie: lowest id). */
    open fun resolvePrimaryInventoryLotIdByLotNo(lotNo: String, itemId: Long): Long? {
        val query = """
            SELECT il.id AS inventoryLotId
            FROM inventory_lot il
            LEFT JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id AND ill.deleted = 0
            WHERE il.deleted = 0
              AND il.itemId = :itemId
              AND il.lotNo = :lotNo
            GROUP BY il.id
            ORDER BY COALESCE(SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)), 0) DESC, il.id ASC
            LIMIT 1
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("itemId" to itemId, "lotNo" to lotNo))
            .firstOrNull()
            ?.let { sql.longVal(it["inventoryLotId"]) }
    }

    open fun loadAlternateLocations(
        itemId: Long,
        lotNo: String,
        currentInventoryLotId: Long,
    ): List<ItemLotTraceAlternateLocation> {
        if (lotNo.isBlank()) return emptyList()
        val query = """
            SELECT
                il.id AS inventoryLotId,
                il.stockInLineId AS stockInLineId,
                ill.id AS inventoryLotLineId,
                COALESCE(wh.code, '') AS warehouseCode,
                COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) AS availableQty
            FROM inventory_lot il
            INNER JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id AND ill.deleted = 0
            LEFT JOIN warehouse wh ON ill.warehouseId = wh.id AND wh.deleted = 0
            WHERE il.deleted = 0
              AND il.itemId = :itemId
              AND il.lotNo = :lotNo
              AND il.id <> :currentInventoryLotId
            ORDER BY wh.code, il.id, ill.id
        """.trimIndent()
        return jdbcDao.queryForList(
            query,
            mapOf("itemId" to itemId, "lotNo" to lotNo, "currentInventoryLotId" to currentInventoryLotId),
        ).map { row ->
            ItemLotTraceAlternateLocation(
                inventoryLotId = sql.longVal(row["inventoryLotId"]) ?: 0L,
                stockInLineId = sql.longVal(row["stockInLineId"]),
                inventoryLotLineId = sql.longVal(row["inventoryLotLineId"]) ?: 0L,
                warehouseCode = sql.str(row["warehouseCode"]),
                availableQty = sql.decimal(row["availableQty"]),
            )
        }
    }

    open fun loadLotInfo(inventoryLotId: Long): ItemLotTraceLotInfo {
        val query = """
            SELECT
                il.id AS inventoryLotId,
                COALESCE(il.lotNo, '') AS lotNo,
                it.id AS itemId,
                COALESCE(it.code, '') AS itemCode,
                COALESCE(it.name, '') AS itemName,
                il.expiryDate AS expiryDate,
                (SELECT sil.productionDate
                 FROM stock_in_line sil
                 WHERE sil.inventoryLotId = il.id
                   AND sil.jobOrderId IS NOT NULL
                   AND sil.deleted = 0
                 ORDER BY sil.receiptDate DESC, sil.id DESC
                 LIMIT 1) AS productionDate,
                il.stockInDate AS stockInDate,
                COALESCE(uc.udfudesc, '') AS uom,
                il.stockInLineId AS primaryStockInLineId
            FROM inventory_lot il
            INNER JOIN items it ON il.itemId = it.id AND it.deleted = 0
            LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = 1
            LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
            WHERE il.id = :inventoryLotId AND il.deleted = 0
        """.trimIndent()
        val row = jdbcDao.queryForList(query, mapOf("inventoryLotId" to inventoryLotId)).firstOrNull()
            ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory lot $inventoryLotId not found")

        return ItemLotTraceLotInfo(
            inventoryLotId = inventoryLotId,
            lotNo = sql.str(row["lotNo"]),
            itemId = sql.longVal(row["itemId"]) ?: inventoryLotId,
            itemCode = sql.str(row["itemCode"]),
            itemName = sql.str(row["itemName"]),
            expiryDate = sql.formatDate(row["expiryDate"]),
            productionDate = sql.formatDateTime(row["productionDate"]),
            stockInDate = sql.formatDateTime(row["stockInDate"]),
            uom = sql.str(row["uom"]),
            primaryStockInLineId = sql.longVal(row["primaryStockInLineId"]),
        )
    }

    open fun loadLotLineIds(inventoryLotId: Long): List<Long> {
        val query = """
            SELECT id FROM inventory_lot_line
            WHERE inventoryLotId = :inventoryLotId AND deleted = 0
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotId" to inventoryLotId))
            .mapNotNull { sql.longVal(it["id"]) }
    }

    open fun loadStockInLineIds(inventoryLotId: Long): List<Long> {
        val query = """
            SELECT sil.id AS id
            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.inventoryLotId = :inventoryLotId
                OR ill.inventoryLotId = :inventoryLotId
              )
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotId" to inventoryLotId))
            .mapNotNull { sql.longVal(it["id"]) }
            .distinct()
    }

    open fun loadWarehouseLines(inventoryLotId: Long): List<ItemLotTraceWarehouseLine> {
        val query = """
            SELECT
                ill.id AS inventoryLotLineId,
                COALESCE(wh.code, '') AS warehouseCode,
                COALESCE(ill.inQty, 0) AS inQty,
                COALESCE(ill.outQty, 0) AS outQty,
                COALESCE(ill.issueQty, 0) AS issueQty,
                COALESCE(ill.status, '') AS status,
                COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) AS availableQty
            FROM inventory_lot_line ill
            LEFT JOIN warehouse wh ON ill.warehouseId = wh.id AND wh.deleted = 0
            WHERE ill.inventoryLotId = :inventoryLotId AND ill.deleted = 0
            ORDER BY wh.code
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotId" to inventoryLotId)).map { row ->
            ItemLotTraceWarehouseLine(
                inventoryLotLineId = sql.longVal(row["inventoryLotLineId"]) ?: 0L,
                warehouseCode = sql.str(row["warehouseCode"]),
                inQty = sql.decimal(row["inQty"]),
                outQty = sql.decimal(row["outQty"]),
                issueQty = sql.decimal(row["issueQty"]),
                status = sql.str(row["status"]),
                availableQty = sql.decimal(row["availableQty"]),
            )
        }
    }

    /** Batch variant of [loadLotLineIds]. */
    open fun loadLotLineIdsForLots(inventoryLotIds: List<Long>): Map<Long, List<Long>> {
        if (inventoryLotIds.isEmpty()) return emptyMap()
        val query = """
            SELECT inventoryLotId, id FROM inventory_lot_line
            WHERE inventoryLotId IN (:inventoryLotIds) AND deleted = 0
            ORDER BY id
        """.trimIndent()
        val grouped = mutableMapOf<Long, MutableList<Long>>()
        jdbcDao.queryForList(query, mapOf("inventoryLotIds" to inventoryLotIds)).forEach { row ->
            val lotId = sql.longVal(row["inventoryLotId"]) ?: return@forEach
            val illId = sql.longVal(row["id"]) ?: return@forEach
            grouped.getOrPut(lotId) { mutableListOf() }.add(illId)
        }
        return grouped
    }

    open fun loadStockInLineIdsForLots(inventoryLotIds: List<Long>): Map<Long, List<Long>> {
        if (inventoryLotIds.isEmpty()) return emptyMap()
        val query = """
            SELECT inventoryLotId, id FROM stock_in_line
            WHERE inventoryLotId IN (:inventoryLotIds) AND deleted = 0
        """.trimIndent()
        val grouped = mutableMapOf<Long, MutableList<Long>>()
        jdbcDao.queryForList(query, mapOf("inventoryLotIds" to inventoryLotIds)).forEach { row ->
            val lotId = sql.longVal(row["inventoryLotId"]) ?: return@forEach
            val silId = sql.longVal(row["id"]) ?: return@forEach
            grouped.getOrPut(lotId) { mutableListOf() }.add(silId)
        }
        return grouped
    }

    open fun loadLotInfoBatch(inventoryLotIds: List<Long>): Map<Long, ItemLotTraceLotInfo> {
        if (inventoryLotIds.isEmpty()) return emptyMap()
        val query = """
            SELECT
                il.id AS inventoryLotId,
                COALESCE(il.lotNo, '') AS lotNo,
                it.id AS itemId,
                COALESCE(it.code, '') AS itemCode,
                COALESCE(it.name, '') AS itemName,
                il.expiryDate AS expiryDate,
                (SELECT sil.productionDate
                 FROM stock_in_line sil
                 WHERE sil.inventoryLotId = il.id
                   AND sil.jobOrderId IS NOT NULL
                   AND sil.deleted = 0
                 ORDER BY sil.receiptDate DESC, sil.id DESC
                 LIMIT 1) AS productionDate,
                il.stockInDate AS stockInDate,
                COALESCE(uc.udfudesc, '') AS uom,
                il.stockInLineId AS primaryStockInLineId
            FROM inventory_lot il
            INNER JOIN items it ON il.itemId = it.id AND it.deleted = 0
            LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = 1
            LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
            WHERE il.id IN (:inventoryLotIds) AND il.deleted = 0
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotIds" to inventoryLotIds))
            .associate { row ->
                val lotId = sql.longVal(row["inventoryLotId"]) ?: return@associate 0L to ItemLotTraceLotInfo(
                    inventoryLotId = 0L, lotNo = "", itemId = 0L, itemCode = "", itemName = "",
                    expiryDate = null, productionDate = null, stockInDate = null, uom = "",
                    primaryStockInLineId = null,
                )
                lotId to ItemLotTraceLotInfo(
                    inventoryLotId = lotId,
                    lotNo = sql.str(row["lotNo"]),
                    itemId = sql.longVal(row["itemId"]) ?: lotId,
                    itemCode = sql.str(row["itemCode"]),
                    itemName = sql.str(row["itemName"]),
                    expiryDate = sql.formatDate(row["expiryDate"]),
                    productionDate = sql.formatDateTime(row["productionDate"]),
                    stockInDate = sql.formatDateTime(row["stockInDate"]),
                    uom = sql.str(row["uom"]),
                    primaryStockInLineId = sql.longVal(row["primaryStockInLineId"]),
                )
            }
    }

    open fun resolveJobOrderIdForLot(inventoryLotId: Long, primaryStockInLineId: Long?): Long? {
        primaryStockInLineId?.let { silId ->
            jdbcDao.queryForList(
                """
                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(),
                mapOf("primaryStockInLineId" to silId),
            ).firstOrNull()?.let { return sql.longVal(it["jobOrderId"]) }
        }
        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
            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.created DESC
            LIMIT 1
        """.trimIndent()
        return jdbcDao.queryForList(query, mapOf("inventoryLotId" to inventoryLotId))
            .firstOrNull()?.let { sql.longVal(it["jobOrderId"]) }
    }
}
