package com.ffii.fpsms.modules.report.service

import org.springframework.stereotype.Service
import net.sf.jasperreports.engine.*
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource
import java.io.ByteArrayOutputStream
import java.io.InputStream
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.m18.M18GrnRules
import com.ffii.fpsms.modules.master.entity.ShopRepository
import com.ffii.fpsms.modules.master.enums.ShopType
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService
import java.math.BigDecimal
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter
import net.sf.jasperreports.export.SimpleExporterInput
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput

@Service
open class ReportService(
    private val jdbcDao: JdbcDao,
    private val itemUomService: ItemUomService,
    private val shopRepository: ShopRepository,
    private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {
    /**
     * Queries the database for inventory data based on dates and optional item type.
     */
    fun searchReport1(fromDate: String, toDate: String, itemType: String?): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>(
            "fromDate" to fromDate, 
            "toDate" to toDate
        )
        
        val itemTypeSql = if (!itemType.isNullOrBlank()) {
            args["itemType"] = "%$itemType%"
            "AND it.type LIKE :itemType"
        } else ""

        val sql = """
            SELECT 
                -- fake value for demo
                8.8 * iv.onHandQty as stockValue,
                iv.onHandQty, 
                it.code as itemCode, 
                it.name as itemName, 
                uc.code as uom, 
                it.type, iv.status
            FROM inventory iv
            LEFT JOIN items it ON iv.itemId = it.id
            LEFT JOIN uom_conversion uc ON iv.uomId = uc.id
            WHERE iv.created >= :fromDate AND iv.created < :toDate
            $itemTypeSql
        """.trimIndent()
        
        return jdbcDao.queryForList(sql, args)
    }


    fun searchReport3( itemType: String?): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>(

        )
        
        val itemTypeSql = if (!itemType.isNullOrBlank()) {
            args["itemType"] = "%$itemType%"
            "AND it.type LIKE :itemType"
        } else ""

        val sql = """
            SELECT 
                -- fake value for demo
                8.8 * iv.onHandQty as stockValue,
                iv.onHandQty, 
                it.code as itemCode, 
                it.name as itemName, 
                uc.code as uom, 
                it.type, iv.status
            FROM inventory iv
            LEFT JOIN items it ON iv.itemId = it.id
            LEFT JOIN uom_conversion uc ON iv.uomId = uc.id
            WHERE iv.created >= :fromDate AND iv.created < :toDate
            $itemTypeSql
        """.trimIndent()
        
        return jdbcDao.queryForList(sql, args)
    }
    fun searchFGDeliveryReport(
        stockCategory: String?,
        stockSubCategory: String?,
        itemCode: String?,
        year: String?,
        lastOutDateStart: String?,
        lastOutDateEnd: String?
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()
        val stockCategorySql = ""
        val stockSubCategorySql = ""


        
        val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
        
        val yearSql = if (!year.isNullOrBlank()) {
            args["year"] = year
            "AND YEAR(IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate)) = :year"
        } else {
            ""
        }
        
        val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) {
            val formattedDate = lastOutDateStart.replace("/", "-")
            args["lastOutDateStart"] = formattedDate
            "AND DATE(IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate)) >= DATE(:lastOutDateStart)"
        } else ""
        
        val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) {
            val formattedDate = lastOutDateEnd.replace("/", "-")
            args["lastOutDateEnd"] = formattedDate
            "AND DATE(IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)"
        } else ""
    
        val supplierFloorSqlCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("supplier.code")

        val sql = """
    SELECT
        IFNULL(DATE_FORMAT(
            IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate),
            '%Y-%m-%d'
        ), '')                                        AS deliveryDate,
        IFNULL(it.code, '')                           AS itemNo,
        IFNULL(it.name, '')                           AS itemName,
        IFNULL(uc.udfudesc, '')                       AS unitOfMeasure,
        IFNULL(dopo.deliveryNoteCode, '')             AS dnNo,
        CAST(IFNULL(sp.id, 0) AS CHAR)                AS customerId,
        IFNULL(sp.name, '')                           AS customerName,
   CAST(
    SUM(IFNULL(IFNULL(sol.qty, dol.qty), 0)) OVER (
        PARTITION BY it.code
    ) AS DECIMAL(14,2)
) AS qtyNumeric,

FORMAT(ROUND(IFNULL(IFNULL(sol.qty, dol.qty), 0), 0), 0) AS qty,
        COALESCE(
            dopo.truckLanceCode,
            (SELECT t2.TruckLanceCode
             FROM truck t2
             WHERE t2.shopId = do.shopId
               AND t2.deleted = 0
               AND t2.Store_id = ${supplierFloorSqlCases.floorStringCase}
               AND (
                   (${supplierFloorSqlCases.floorStringCase}
                    AND (SELECT COUNT(*) FROM truck t3 
                         WHERE t3.shopId = do.shopId AND t3.deleted = 0 
                         AND t3.Store_id = '4F') > 1
                    AND IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate) IS NOT NULL
                    AND t2.TruckLanceCode LIKE CONCAT('%', 
                        CASE DAYNAME(IFNULL(dopo.requiredDeliveryDate, do.estimatedArrivalDate))
                            WHEN 'Monday' THEN 'Mon'
                            WHEN 'Tuesday' THEN 'Tue'
                            WHEN 'Wednesday' THEN 'Wed'
                            WHEN 'Thursday' THEN 'Thu'
                            WHEN 'Friday' THEN 'Fri'
                            WHEN 'Saturday' THEN 'Sat'
                            WHEN 'Sunday' THEN 'Sun'
                            ELSE ''
                        END, '%'))
                   OR
                   t2.Store_id = ${supplierFloorSqlCases.floorStringCase}
               )
             ORDER BY t2.DepartureTime ASC
             LIMIT 1),
            ''
        )                                             AS truckNo,
        ''                                            AS driver,
        IFNULL(do.code, '')                           AS deliveryOrderNo,
        IFNULL(qc.name, '')                            AS stockSubCategory
    FROM delivery_order_pick_order dopo
        INNER JOIN pick_order po
            ON po.deliveryOrderPickOrderId = dopo.id
           AND po.deleted = 0
        INNER JOIN delivery_order do
            ON po.doId = do.id
           AND do.deleted = 0
        LEFT JOIN shop supplier
            ON do.supplierId = supplier.id
           AND supplier.deleted = 0
        LEFT JOIN shop sp
            ON do.shopId = sp.id
           AND sp.deleted = 0
        LEFT JOIN pick_order_line pol
            ON pol.poId = po.id
           AND pol.deleted = 0
        LEFT JOIN delivery_order_line dol
            ON dol.deliveryOrderId = do.id
           AND dol.itemId = pol.itemId
           AND dol.deleted = 0
        LEFT JOIN items it
            ON dol.itemId = it.id
           AND it.deleted = 0
        LEFT JOIN items_qc_category_mapping iqcm
            ON it.id = iqcm.itemId
        LEFT JOIN qc_category qc
            ON iqcm.qcCategoryId = qc.id
           AND qc.deleted = 0
        LEFT JOIN item_category ic
            ON it.categoryId = ic.id
        LEFT JOIN item_uom iu
            ON it.id = iu.itemId
           AND iu.stockUnit = 1
        LEFT JOIN uom_conversion uc
            ON iu.uomId = uc.id
        LEFT JOIN stock_out_line sol
            ON pol.id = sol.pickOrderLineId
           AND sol.itemId = it.id
           AND sol.deleted = 0
           AND sol.status = 'completed'
        LEFT JOIN inventory_lot_line ill
            ON sol.inventoryLotLineId = ill.id
           AND ill.deleted = 0
        LEFT JOIN inventory_lot il
            ON ill.inventoryLotId = il.id
           AND il.deleted = 0
        LEFT JOIN stock_in_line sil
            ON il.stockInLineId = sil.id
           AND sil.deleted = 0
    WHERE
        dopo.deleted = 0
        AND dopo.ticketStatus = 'completed'
        AND COALESCE(sol.qty, dol.qty, 0) <> 0
        $stockCategorySql
        $stockSubCategorySql
        $itemCodeSql
        $yearSql
        $lastOutDateStartSql
        $lastOutDateEndSql
     ORDER BY
        it.code,
        deliveryDate,
        qc.name,
        IFNULL(sil.lotNo, '')
""".trimIndent()

val result = jdbcDao.queryForList(sql, args)

return result
     
    }

 fun getDistinctHandlersForFGStockOutTraceability(): List<String> {
    val sql = """
        SELECT DISTINCT h.handler
        FROM (
            SELECT TRIM(COALESCE(picker_user.name, modified_user.name, '')) AS handler
            FROM stock_out_line sol
            INNER JOIN stock_out so ON sol.stockOutId = so.id AND so.deleted = 0 AND so.type = 'do'
            LEFT JOIN user picker_user ON sol.handled_by = picker_user.id AND picker_user.deleted = 0
            LEFT JOIN user modified_user ON sol.modifiedBy = modified_user.staffNo AND modified_user.deleted = 0 AND sol.handled_by IS NULL
            WHERE sol.deleted = 0
            UNION
            SELECT TRIM(IFNULL(handlerName, '')) AS handler
            FROM delivery_order_pick_order
            WHERE deleted = 0
              AND ticketStatus = 'completed'
              AND IFNULL(handlerName, '') <> ''
        ) h
        WHERE TRIM(IFNULL(h.handler, '')) <> ''
        ORDER BY h.handler
    """.trimIndent()
    return jdbcDao.queryForList(sql, emptyMap<String, Any>()).map { row -> (row["handler"]?.toString() ?: "").trim() }.filter { it.isNotBlank() }
}

    /**
     * Dropdown for stock balance report: [value] and [label] both identify the round;
     * label shows stockTakeRoundId and plan-start date (same round may span multiple sections).
     */
    fun getStockTakeRoundOptions(): List<Map<String, Any>> {
        val sql = """
            SELECT
                CAST(st.stockTakeRoundId AS CHAR) AS value,
                CONCAT(
                    CAST(st.stockTakeRoundId AS CHAR),
                    ' — ',
                    DATE_FORMAT(MIN(st.planStart), '%Y-%m-%d')
                ) AS label
            FROM stock_take st
            WHERE st.deleted = 0
              AND st.stockTakeRoundId IS NOT NULL
            GROUP BY st.stockTakeRoundId
            ORDER BY MIN(st.planStart) DESC
        """.trimIndent()

        return jdbcDao.queryForList(sql, emptyMap<String, Any>())
    }

 fun searchFGStockOutTraceabilityReport(
    stockCategory: String?,
    stockSubCategory: String?,
    itemCode: String?,
    year: String?,
    lastOutDateStart: String?,
    lastOutDateEnd: String?,
    handler: String?
): List<Map<String, Any>> {
    val args = mutableMapOf<String, Any>()
    
    // Stock Category 过滤：通过 items.type
    val stockCategorySql = if (!stockCategory.isNullOrBlank()) {
        val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() }
        if (categories.isNotEmpty()) {
            val conditions = categories.mapIndexed { index, cat ->
                val paramName = "stockCategory_$index"
                args[paramName] = cat
                "it.type = :$paramName"
            }
            "AND (${conditions.joinToString(" OR ")})"
        } else {
            ""
        }
    } else {
        ""
    }
    
    // 移除 stockSubCategory 过滤（不需要）
    
    val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
    
    val yearSql = if (!year.isNullOrBlank()) {
        args["year"] = year
        "AND YEAR(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) = :year"
    } else {
        ""
    }
    
    val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) {
        val formattedDate = lastOutDateStart.replace("/", "-")
        args["lastOutDateStart"] = formattedDate
        "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) >= DATE(:lastOutDateStart)"
    } else ""
    
    val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) {
        val formattedDate = lastOutDateEnd.replace("/", "-")
        args["lastOutDateEnd"] = formattedDate
        "AND DATE(IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate)) <= DATE(:lastOutDateEnd)"
    } else ""

    val handlerSql = buildMultiValueExactClause(
        handler,
        "COALESCE(picker_user.name, modified_user.name, '')",
        "handler",
        args
    )

    val sql = """
      SELECT
        IFNULL(DATE_FORMAT(
            IFNULL(dpor.RequiredDeliveryDate, do.estimatedArrivalDate),
            '%Y-%m-%d'
        ), '') AS deliveryDate,
        IFNULL(it.code, '') AS itemNo,
        IFNULL(it.name, '') AS itemName,
        IFNULL(uc.udfudesc, '') AS unitOfMeasure,
        IFNULL(dpor.deliveryNoteCode, '') AS dnNo,
        CAST(IFNULL(sp.id, 0) AS CHAR) AS customerId,
        IFNULL(sp.name, '') AS customerName,
        FORMAT(
            ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0
        ) AS qtyNumeric,
        FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS qty,
        '' AS truckNo,
        '' AS driver,
        IFNULL(do.code, '') AS deliveryOrderNo,
        IFNULL(po.code, '') AS fgPickOrderNo,
        IFNULL(po.code, '') AS stockReqNo,
        IFNULL(il.lotNo, '') AS lotNo,
        IFNULL(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
        FORMAT(ROUND(IFNULL(sol.qty, 0), 0), 0) AS stockOutQty,
        COALESCE(
            picker_user.name,
            modified_user.name,
            ''
        ) AS handler,
        COALESCE(
            picker_user.name,
            modified_user.name,
            ''
        ) AS pickedBy,
        GROUP_CONCAT(DISTINCT wh.code ORDER BY wh.code SEPARATOR ', ') AS storeLocation,
        '' AS pickRemark,
        FORMAT(
            ROUND(SUM(IFNULL(sol.qty, 0)) OVER (PARTITION BY it.code), 0), 0
        ) AS totalStockOutQty,
        0 AS stockSubCategory
    FROM do_pick_order_line_record dpolr
        LEFT JOIN do_pick_order_record dpor
            ON dpolr.record_id = dpor.id
           AND dpor.deleted = 0
           AND dpor.ticket_status = 'completed'
        INNER JOIN delivery_order do
            ON dpolr.do_order_id = do.id
           AND do.deleted = 0
        LEFT JOIN shop sp
            ON do.shopId = sp.id
           AND sp.deleted = 0
        LEFT JOIN delivery_order_line dol
            ON do.id = dol.deliveryOrderId
           AND dol.deleted = 0
        LEFT JOIN items it
            ON dol.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
        LEFT JOIN pick_order_line pol
            ON dpolr.pick_order_id = pol.poId
           AND pol.itemId = it.id
           AND pol.deleted = 0
        LEFT JOIN pick_order po
            ON pol.poId = po.id
           AND po.deleted = 0
               LEFT JOIN stock_out_line sol
            ON pol.id = sol.pickOrderLineId
           AND sol.itemId = it.id
           AND sol.deleted = 0
        LEFT JOIN stock_out so
            ON sol.stockOutId = so.id
           AND so.deleted = 0
           AND so.type = 'do'
        LEFT JOIN inventory_lot_line ill
            ON sol.inventoryLotLineId = ill.id
           AND ill.deleted = 0
        LEFT 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 user picker_user
            ON sol.handled_by = picker_user.id
           AND picker_user.deleted = 0
        LEFT JOIN user modified_user
            ON sol.modifiedBy = modified_user.staffNo
           AND modified_user.deleted = 0
           AND sol.handled_by IS NULL
    WHERE
        dpolr.deleted = 0
        $stockCategorySql
        $itemCodeSql
        $yearSql
        $lastOutDateStartSql
        $lastOutDateEndSql
        $handlerSql
        GROUP BY
        sol.id,
        dpor.RequiredDeliveryDate,
        do.estimatedArrivalDate,
        it.code,
        it.name,
        uc.udfudesc,
        dpor.deliveryNoteCode,
        sp.id,
        sp.name,
        sol.qty,
        picker_user.name,
        modified_user.name,
        po.code,
        do.code,
        il.lotNo,
        il.expiryDate
    ORDER BY
        it.code,
        deliveryDate,
        il.lotNo
""".trimIndent()

    val result = jdbcDao.queryForList(sql, args)
    
    // 打印查询结果
    println("=== Query Result (Total: ${result.size} rows) ===")
    result.take(50).forEachIndexed { index, row ->
        println("Row $index:")
        println("  deliveryDate: ${row["deliveryDate"]}")
        println("  itemNo: ${row["itemNo"]}")
        println("  itemName: ${row["itemName"]}")
        println("  qty: ${row["qty"]}")
        println("  qtyNumeric: ${row["qtyNumeric"]}")
        println("  deliveryOrderNo: ${row["deliveryOrderNo"]}")
        println("  dnNo: ${row["dnNo"]}")
        println("  fgPickOrderNo: ${row["fgPickOrderNo"]}")
        println("  pickedBy: ${row["pickedBy"]}")
        println("  storeLocation: ${row["storeLocation"]}")
        println("  ---")
    }
    if (result.size > 50) {
        println("... (showing first 50 rows, total ${result.size} rows)")
    }

    return result
}
    /**
     * Helper function to build SQL clause for comma-separated values.
     * Supports multiple values like "val1, val2, val3" and generates OR conditions with LIKE.
     */
    private fun buildMultiValueLikeClause(
        paramValue: String?,
        columnName: String,
        paramPrefix: String,
        args: MutableMap<String, Any>
    ): String {
        if (paramValue.isNullOrBlank()) return ""
        
        val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
        if (values.isEmpty()) return ""
        
        val conditions = values.mapIndexed { index, value ->
            val paramName = "${paramPrefix}_$index"
            args[paramName] = "%$value%"
            "$columnName LIKE :$paramName"
        }
        
        return "AND (${conditions.joinToString(" OR ")})"
    }

    /**
     * Helper function to build SQL clause for comma-separated values with exact match.
     * Supports multiple values like "val1, val2, val3" and generates OR conditions with =.
     */
    private fun buildMultiValueExactClause(
        paramValue: String?,
        columnName: String,
        paramPrefix: String,
        args: MutableMap<String, Any>
    ): String {
        if (paramValue.isNullOrBlank()) return ""
        
        val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
        if (values.isEmpty()) return ""
        
        val conditions = values.mapIndexed { index, value ->
            val paramName = "${paramPrefix}_$index"
            args[paramName] = value
            "$columnName = :$paramName"
        }
        
        return "AND (${conditions.joinToString(" OR ")})"
    }

    /**
     * Comma-separated tokens; each token matches **either** [codeColumn] or [nameColumn] (LIKE %token%).
     * Rows match if **any** token matches (OR across tokens).
     */
    private fun buildMultiValueCodeOrNameLikeClause(
        paramValue: String?,
        codeColumn: String,
        nameColumn: String,
        paramPrefix: String,
        args: MutableMap<String, Any>
    ): String {
        if (paramValue.isNullOrBlank()) return ""
        val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
        if (values.isEmpty()) return ""
        val tokenClauses = values.mapIndexed { index, value ->
            val p1 = "${paramPrefix}_${index}_c"
            val p2 = "${paramPrefix}_${index}_n"
            args[p1] = "%$value%"
            args[p2] = "%$value%"
            "($codeColumn LIKE :$p1 OR $nameColumn LIKE :$p2)"
        }
        return "AND (${tokenClauses.joinToString(" OR ")})"
    }

    /**
     * Comma-separated tokens; stock-in line matches if EXISTS a GRN log row whose grn_code matches **any** token (LIKE).
     */
    private fun buildMultiValueGrnExistsClause(
        paramValue: String?,
        paramPrefix: String,
        args: MutableMap<String, Any>
    ): String {
        if (paramValue.isNullOrBlank()) return ""
        val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() }
        if (values.isEmpty()) return ""
        val inner = values.mapIndexed { i, v ->
            val p = "${paramPrefix}_$i"
            args[p] = "%$v%"
            "g.grn_code LIKE :$p"
        }.joinToString(" OR ")
        return "AND EXISTS (SELECT 1 FROM m18_goods_receipt_note_log g WHERE g.stock_in_line_id = sil.id AND ($inner))"
    }

    /**
     * Supplier combo options for GRN report (code as value for filtering).
     */
    fun getGrnSupplierOptions(): List<Map<String, String>> {
        return shopRepository.findShopComboByTypeAndDeletedIsFalse(ShopType.SUPPLIER)
            .map { combo ->
                mapOf(
                    "label" to combo.label,
                    "value" to (combo.code ?: ""),
                )
            }
            .filter { it["value"]!!.isNotBlank() }
    }

    /**
     * Item code/name options for GRN report autocomplete (partial match).
     */
    fun getGrnItemOptions(q: String?): List<Map<String, String>> {
        val args = mutableMapOf<String, Any>()
        val sql = if (q.isNullOrBlank()) {
            """
            SELECT code, name FROM items WHERE deleted = false ORDER BY code LIMIT 200
            """.trimIndent()
        } else {
            args["q"] = "%${q.trim()}%"
            """
            SELECT code, name FROM items WHERE deleted = false
            AND (code LIKE :q OR name LIKE :q)
            ORDER BY code LIMIT 200
            """.trimIndent()
        }
        val rows = jdbcDao.queryForList(sql, args)
        return rows.map { row ->
            val code = (row["code"] ?: "").toString()
            val name = (row["name"] ?: "").toString()
            mapOf(
                "label" to if (name.isNotBlank()) "$code - $name" else code,
                "value" to code,
            )
        }
    }

    /**
     * Queries the database for Stock In Traceability Report data (入倉追蹤 PDF).
     * Joins stock_in_line, stock_in, items, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables.
     * Supports comma-separated values for stockCategory (items.type) and itemCode.
     * Date range [lastInDateStart, lastInDateEnd] filters on stock_in_line.productionDate (完成生產日期), same basis as 成品/半成品生產分析報告.
     */
    fun searchStockInTraceabilityReport(
        stockCategory: String?,
        itemCode: String?,
        lastInDateStart: String?,
        lastInDateEnd: String?
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()
        

        // If "All" is selected or contains "All", don't filter by stock category
        val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) {
            buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args)
        } else {
            ""
        }
        val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
        
        val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) {
            val formattedDate = lastInDateStart.replace("/", "-")
            args["lastInDateStart"] = formattedDate
            "AND sil.productionDate IS NOT NULL AND DATE(sil.productionDate) >= DATE(:lastInDateStart)"
        } else ""
        
        val lastInDateEndSql = if (!lastInDateEnd.isNullOrBlank()) {
            val formattedDate = lastInDateEnd.replace("/", "-")
            args["lastInDateEnd"] = formattedDate
            "AND sil.productionDate IS NOT NULL AND DATE(sil.productionDate) <= DATE(:lastInDateEnd)"
        } else ""

        val sql = """
        SELECT
            COALESCE(it.code, '') as itemNo,
            COALESCE(it.name, '') as itemName,
            COALESCE(uc.udfudesc, '') as unitOfMeasure,
            COALESCE(sil.dnNo, '') as dnNo,
            COALESCE(sil.lotNo, il.lotNo, '') as lotNo,
            COALESCE(DATE_FORMAT(COALESCE(sil.expiryDate, il.expiryDate), '%Y-%m-%d'), '') as expiryDate,
            CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN '0'
                 ELSE TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(pa_sil.putAwayQtySum, 0), 2)))
            END as stockInQty,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(sil.acceptedQty, 0), 2))) as iqcSampleQty,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(qr_agg.failQtySum, 0), 2))) as iqcDefectQty,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(
                CASE WHEN COALESCE(sil.acceptedQty, 0) > 0
                     THEN COALESCE(qr_agg.failQtySum, 0) / sil.acceptedQty * 100
                     ELSE 0
                END, 2))) as iqcDefectPercentage,
            CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN '不合格' ELSE '已接受' END as iqcResult,
            COALESCE(qr_agg.remarksConcat, '') as iqcRemarks,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(qr_agg.failQtySum, 0)) OVER (PARTITION BY it.id), 2))) as totalIqcDefectQty,
            COALESCE(wh.code, '') as storeLocation,
            COALESCE(sp_si.code, sp_po.code, '') as supplierID,
            COALESCE(sp_si.name, sp_po.name, '') as supplierName,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(pa_sil.putAwayQtySum, 0)) OVER (PARTITION BY it.id), 2))) as totalStockInQty,
            TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(SUM(COALESCE(sil.acceptedQty, 0)) OVER (PARTITION BY it.id), 2))) as totalIqcSampleQty
        FROM stock_in_line sil
        LEFT JOIN stock_in si ON sil.stockInId = si.id
        LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id
        LEFT JOIN items it ON sil.itemId = it.id
        LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = true
        LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
        LEFT JOIN inventory_lot il ON sil.inventoryLotId = il.id
        LEFT JOIN (
            SELECT inventoryLotId, SUM(COALESCE(inQty, 0)) AS putAwayQtySum
            FROM inventory_lot_line
            WHERE deleted = false
            GROUP BY inventoryLotId
        ) pa_sil ON pa_sil.inventoryLotId = sil.inventoryLotId
        LEFT JOIN inventory_lot_line ill ON il.id = ill.inventoryLotId
        LEFT JOIN warehouse wh ON ill.warehouseId = wh.id
        LEFT JOIN shop sp_si ON si.supplierId = sp_si.id
        LEFT JOIN shop sp_po ON po.supplierId = sp_po.id
        LEFT JOIN (
            SELECT
                qr.stockInLineId,
                SUM(COALESCE(qr.failQty, 0)) AS failQtySum,
                MAX(CASE WHEN qr.qcPassed = 0 THEN 1 ELSE 0 END) AS qcFailed,
                GROUP_CONCAT(
                    CASE WHEN qr.remarks IS NOT NULL AND TRIM(qr.remarks) != '' THEN qr.remarks END
                    SEPARATOR '; '
                ) AS remarksConcat
            FROM qc_result qr
            WHERE qr.deleted = 0
            GROUP BY qr.stockInLineId
        ) qr_agg ON qr_agg.stockInLineId = sil.id
        WHERE sil.deleted = false
        $stockCategorySql
        $itemCodeSql
        $lastInDateStartSql
        $lastInDateEndSql
        ORDER BY it.code, sil.lotNo
    """.trimIndent()

        return jdbcDao.queryForList(sql, args)
    }

    /**
     * GRN (Goods Received Note) report: stock-in lines with PO/delivery note.
     * Filters: receipt date range, item (code or name), supplier (code or name), PO code, GRN code (partial match, comma-separated OR).
     */
    fun searchGrnReport(
        receiptDateStart: String?,
        receiptDateEnd: String?,
        itemCode: String?,        
        supplier: String?,
        poCode: String?,
        grnCode: String?,
    ): List<Map<String, Any?>> {
        val args = mutableMapOf<String, Any>()
        val receiptDateStartSql = if (!receiptDateStart.isNullOrBlank()) {
            val formatted = receiptDateStart.replace("/", "-")
            args["receiptDateStart"] = formatted
            "AND DATE(sil.receiptDate) >= DATE(:receiptDateStart)"
        } else ""
        val receiptDateEndSql = if (!receiptDateEnd.isNullOrBlank()) {
            val formatted = receiptDateEnd.replace("/", "-")
            args["receiptDateEnd"] = formatted
            "AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)"
        } else ""
        val itemSql = buildMultiValueCodeOrNameLikeClause(itemCode, "it.code", "it.name", "grnItem", args)
        val supplierSql = buildMultiValueCodeOrNameLikeClause(supplier, "sp.code", "sp.name", "grnSupp", args)
        val poCodeSql = buildMultiValueLikeClause(poCode, "po.code", "grnPo", args)
        val grnExistsSql = buildMultiValueGrnExistsClause(grnCode, "grnG", args)

        val sql = """
            SELECT
                po.code AS poCode,
                CASE
                    WHEN sil.dnNo = 'DN00000' OR sil.dnNo IS NULL THEN ''
                    ELSE sil.dnNo
                END AS deliveryNoteNo,
                DATE_FORMAT(sil.receiptDate, '%Y-%m-%d') AS receiptDate,
                COALESCE(it.code, '') AS itemCode,
                COALESCE(it.name, '') AS itemName,
                COALESCE(sil.acceptedQty, 0) AS acceptedQty,
                COALESCE(sil.demandQty, 0) AS demandQty,
                COALESCE(uc_stock.udfudesc, uc_pol.udfudesc, '') AS uom,
                COALESCE(uc_pol.udfudesc, '') AS purchaseUomDesc,
                COALESCE(uc_stock.udfudesc, '') AS stockUomDesc,
                COALESCE(sil.productLotNo, '') AS productLotNo,
                DATE_FORMAT(sil.expiryDate, '%Y-%m-%d') AS expiryDate,
                COALESCE(sp.code, '') AS supplierCode,
                COALESCE(sp.name, '') AS supplier,
                COALESCE(sil.status, '') AS status,
                MAX(COALESCE(pol.up, 0)) AS unitPrice,
                MAX(ROUND(COALESCE(pol.up, 0) * COALESCE(sil.acceptedQty, 0), 2)) AS lineAmount,
                MAX(COALESCE(cur.code, '')) AS currencyCode,
                MAX(grn.grn_code) AS grnCode,
                MAX(grn.m18_record_id) AS grnId,
                MAX(po.m18CreatedUId) AS poM18CreatedUId
            FROM stock_in_line sil
            LEFT JOIN items it ON sil.itemId = it.id
            LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id
            LEFT JOIN currency cur ON po.currencyId = cur.id
            LEFT JOIN shop sp ON po.supplierId = sp.id
            LEFT JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id
            LEFT JOIN uom_conversion uc_pol ON pol.uomId = uc_pol.id
            LEFT JOIN item_uom iu_stock ON it.id = iu_stock.itemId AND iu_stock.stockUnit = true AND iu_stock.deleted = false
            LEFT JOIN uom_conversion uc_stock ON iu_stock.uomId = uc_stock.id
            LEFT JOIN m18_goods_receipt_note_log grn
                   ON grn.stock_in_line_id = sil.id
            WHERE sil.deleted = false
            AND sil.receiptDate IS NOT NULL
            AND sil.purchaseOrderId IS NOT NULL
            $receiptDateStartSql
            $receiptDateEndSql
            $itemSql
            $supplierSql
            $poCodeSql
            $grnExistsSql
            GROUP BY
                po.code,
                deliveryNoteNo,
                receiptDate,
                itemCode,
                itemName,
                acceptedQty,
                demandQty,
                uom,
                purchaseUomDesc,
                stockUomDesc,
                productLotNo,
                expiryDate,
                supplierCode,
                supplier,
                status
            ORDER BY sil.receiptDate, po.code, sil.id
        """.trimIndent()
        val rows = jdbcDao.queryForList(sql, args)
        return rows.map { row ->
            mapOf(
                "poCode" to row["poCode"],
                "deliveryNoteNo" to row["deliveryNoteNo"],
                "receiptDate" to row["receiptDate"],
                "itemCode" to row["itemCode"],
                "itemName" to row["itemName"],
                "acceptedQty" to (row["acceptedQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
                "receivedQty" to (row["acceptedQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
                "demandQty" to (row["demandQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
                "uom" to row["uom"],
                "purchaseUomDesc" to row["purchaseUomDesc"],
                "stockUomDesc" to row["stockUomDesc"],
                "productLotNo" to row["productLotNo"],
                "expiryDate" to row["expiryDate"],
                "supplierCode" to row["supplierCode"],
                "supplier" to row["supplier"],
                "status" to row["status"],
                "unitPrice" to (row["unitPrice"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
                "lineAmount" to (row["lineAmount"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
                "currencyCode" to row["currencyCode"],
                "grnCode" to row["grnCode"],
                "grnId" to row["grnId"],
                "poM18CreatorDisplay" to M18GrnRules.formatM18CreatedUidForReport(
                    when (val v = row["poM18CreatedUId"]) {
                        null -> null
                        is Number -> v.toLong()
                        is BigDecimal -> v.toLong()
                        else -> v.toString().toLongOrNull()
                    },
                ),
            )
        }
    }

    /**
     * GRN report sheet "已上架PO金額": totals grouped by [receiptDate] (calendar day of stock-in receipt),
     * plus currency and PO. Only lines with PO line status `completed`. Same filters as [searchGrnReport].
     */
    fun searchGrnListedPoAmounts(
        receiptDateStart: String?,
        receiptDateEnd: String?,
        itemCode: String?,
        supplier: String?,
        poCode: String?,
        grnCode: String?,
    ): Map<String, Any> {
        val args = mutableMapOf<String, Any>()
        val receiptDateStartSql = if (!receiptDateStart.isNullOrBlank()) {
            val formatted = receiptDateStart.replace("/", "-")
            args["receiptDateStart"] = formatted
            "AND DATE(sil.receiptDate) >= DATE(:receiptDateStart)"
        } else ""
        val receiptDateEndSql = if (!receiptDateEnd.isNullOrBlank()) {
            val formatted = receiptDateEnd.replace("/", "-")
            args["receiptDateEnd"] = formatted
            "AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)"
        } else ""
        val itemSql = buildMultiValueCodeOrNameLikeClause(itemCode, "it.code", "it.name", "grnListedPoItem", args)
        val supplierSql = buildMultiValueCodeOrNameLikeClause(supplier, "sp.code", "sp.name", "grnListedPoSupp", args)
        val poCodeSql = buildMultiValueLikeClause(poCode, "po.code", "grnListedPoPo", args)
        val grnExistsSql = buildMultiValueGrnExistsClause(grnCode, "grnListedPoG", args)

        val lineSubquery = """
            SELECT
                sil.id AS sil_id,
                sil.purchaseOrderId AS purchaseOrderId,
                DATE_FORMAT(sil.receiptDate, '%Y-%m-%d') AS receiptDate,
                ROUND(COALESCE(pol.up, 0) * COALESCE(sil.acceptedQty, 0), 2) AS line_amt
            FROM stock_in_line sil
            INNER JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id
            INNER JOIN purchase_order po ON sil.purchaseOrderId = po.id
            LEFT JOIN shop sp ON po.supplierId = sp.id
            LEFT JOIN items it ON sil.itemId = it.id
            WHERE sil.deleted = false
            AND sil.receiptDate IS NOT NULL
            AND sil.purchaseOrderId IS NOT NULL
            AND pol.status = 'completed'
            $receiptDateStartSql
            $receiptDateEndSql
            $itemSql
            $supplierSql
            $poCodeSql
            $grnExistsSql
        """.trimIndent()

        val currencySql = """
            SELECT
                s.receiptDate AS receiptDate,
                COALESCE(cur.code, '') AS currencyCode,
                SUM(s.line_amt) AS totalAmount
            FROM ( $lineSubquery ) s
            INNER JOIN purchase_order po ON s.purchaseOrderId = po.id
            LEFT JOIN currency cur ON po.currencyId = cur.id
            GROUP BY s.receiptDate, cur.code
            ORDER BY s.receiptDate, currencyCode
        """.trimIndent()

        val byPoSql = """
            SELECT
                s.receiptDate AS receiptDate,
                po.code AS poCode,
                COALESCE(cur.code, '') AS currencyCode,
                SUM(s.line_amt) AS totalAmount,
                GROUP_CONCAT(DISTINCT NULLIF(grn.grn_code, '') ORDER BY grn.grn_code SEPARATOR ', ') AS grnCodes
            FROM ( $lineSubquery ) s
            INNER JOIN purchase_order po ON s.purchaseOrderId = po.id
            LEFT JOIN currency cur ON po.currencyId = cur.id
            LEFT JOIN m18_goods_receipt_note_log grn ON grn.stock_in_line_id = s.sil_id
            GROUP BY s.receiptDate, po.id, po.code, cur.code
            ORDER BY s.receiptDate, currencyCode, poCode
        """.trimIndent()

        val currencyRows = jdbcDao.queryForList(currencySql, args).map { row ->
            mapOf(
                "receiptDate" to row["receiptDate"],
                "currencyCode" to row["currencyCode"],
                "totalAmount" to (row["totalAmount"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
            )
        }
        val poRows = jdbcDao.queryForList(byPoSql, args).map { row ->
            mapOf(
                "receiptDate" to row["receiptDate"],
                "poCode" to row["poCode"],
                "currencyCode" to row["currencyCode"],
                "totalAmount" to (row["totalAmount"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
                "grnCodes" to (row["grnCodes"] ?: ""),
            )
        }
        return mapOf(
            "currencyTotals" to currencyRows,
            "byPurchaseOrder" to poRows,
        )
    }

    /**
     * GRN preview for M18: show both stock qty (acceptedQty) and purchase qty (converted) for a specific receipt date.
     * This is a DRY-RUN preview only (does not call M18).
     * Excludes POs whose [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder.m18CreatedUId] is in [com.ffii.fpsms.m18.M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS] (same as live GRN posting).
     */
    fun searchGrnPreviewM18(receiptDate: String): List<Map<String, Any?>> {
        val formatted = receiptDate.replace("/", "-")
        val args = mutableMapOf<String, Any>("receiptDate" to formatted)
        val skipGrnM18CreatedUidInList = M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS.joinToString(", ")
        val sql = """
            SELECT
                sil.id AS stockInLineId,
                po.id AS purchaseOrderId,
                po.code AS poCode,
                pol.id AS purchaseOrderLineId,
                CASE
                    WHEN sil.dnNo = 'DN00000' OR sil.dnNo IS NULL THEN ''
                    ELSE sil.dnNo
                END AS deliveryNoteNo,
                DATE_FORMAT(sil.receiptDate, '%Y-%m-%d') AS receiptDate,
                it.id AS itemId,
                COALESCE(it.code, '') AS itemCode,
                COALESCE(it.name, '') AS itemName,
                COALESCE(sil.acceptedQty, 0) AS acceptedQty,
                COALESCE(uc_pol.udfudesc, '') AS purchaseUomDesc,
                COALESCE(uc_stock.udfudesc, '') AS stockUomDesc,
                COALESCE(sil.status, '') AS status
            FROM stock_in_line sil
            LEFT JOIN items it ON sil.itemId = it.id
            LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id
            LEFT JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id
            LEFT JOIN uom_conversion uc_pol ON pol.uomId = uc_pol.id
            LEFT JOIN item_uom iu_stock ON it.id = iu_stock.itemId AND iu_stock.stockUnit = true AND iu_stock.deleted = false
            LEFT JOIN uom_conversion uc_stock ON iu_stock.uomId = uc_stock.id
            WHERE sil.deleted = false
              AND sil.receiptDate IS NOT NULL
              AND DATE(sil.receiptDate) = DATE(:receiptDate)
              AND sil.purchaseOrderId IS NOT NULL
              AND sil.status = 'completed'
              AND (po.m18CreatedUId IS NULL OR po.m18CreatedUId NOT IN ($skipGrnM18CreatedUidInList))
            ORDER BY sil.purchaseOrderId, sil.purchaseOrderLineId, sil.id
        """.trimIndent()
        val rows = jdbcDao.queryForList(sql, args)
        return rows.map { row ->
            val itemId = (row["itemId"] as? Number)?.toLong()
            val acceptedQtyBd = when (val v = row["acceptedQty"]) {
                is BigDecimal -> v
                is Number -> BigDecimal(v.toString())
                is String -> v.toBigDecimalOrNull() ?: BigDecimal.ZERO
                else -> BigDecimal.ZERO
            }
            val purchaseQtyBd = if (itemId != null) itemUomService.convertStockQtyToPurchaseQty(itemId, acceptedQtyBd) else acceptedQtyBd
            mapOf(
                "receiptDate" to row["receiptDate"],
                "poCode" to row["poCode"],
                "deliveryNoteNo" to row["deliveryNoteNo"],
                "stockInLineId" to row["stockInLineId"],
                "purchaseOrderLineId" to row["purchaseOrderLineId"],
                "itemCode" to row["itemCode"],
                "itemName" to row["itemName"],
                "stockQty" to acceptedQtyBd.toDouble(),
                "purchaseQty" to purchaseQtyBd.toDouble(),
                "purchaseUomDesc" to row["purchaseUomDesc"],
                "stockUomDesc" to row["stockUomDesc"],
                "status" to row["status"],
            )
        }
    }

      /**
     * Queries the database for Stock Balance Report data (one summarized row per item).
     * Lots in scope: distinct [StockTakeRecord.lotId] for [stockTakeRoundId].
     * Opening balance: latest stock_ledger.balance from type TKE after round cutoff
     * (earliest stocktakerecord.stockTakeStartTime in the round, per item); 0 when no TKE.
     * Cumulative in/out and variance buckets: classified by [StockLedger.type] (TKE, MISS, BAD, ADJ, etc.).
     * Price per stock unit uses PO line and UOM ratios; total balance = sum of (in value - out value) per item.
     * 現存存貨 = totalCurrentBalance; avg unit price = total_balance / 現存存貨 (0 when current stock is 0).
     */
    fun searchStockBalanceReport(
        stockCategory: String?,
        itemCode: String?,
        balanceFilterStart: String?,
        balanceFilterEnd: String?,
        storeLocation: String?,
        lastInDateStart: String?,
        lastInDateEnd: String?,
        lastOutDateStart: String?,
        lastOutDateEnd: String?,
        stockTakeRoundId: Long,
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()
        args["stockTakeRoundId"] = stockTakeRoundId

        val stockCategorySql = buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args)
        val itemCodeSql = buildMultiValueLikeClause(itemCode, "sl.itemCode", "itemCode", args)
        val storeLocationSql = if (!storeLocation.isNullOrBlank()) {
            args["storeLocation"] = "%$storeLocation%"
            "AND agg.storeLocation LIKE :storeLocation"
        } else ""

        val sql = """
            WITH chosen_round AS (
                SELECT
                    CASE
                        WHEN :stockTakeRoundId IS NULL OR :stockTakeRoundId = 0 THEN (
                            SELECT st.stockTakeRoundId
                            FROM stock_take st
                            WHERE st.deleted = 0
                              AND st.stockTakeRoundId IS NOT NULL
                            ORDER BY st.planStart DESC
                            LIMIT 1
                        )
                        ELSE :stockTakeRoundId
                    END AS roundId
            ),
            round_time AS (
                SELECT
                    cr.roundId AS roundId,
                    MIN(st.planStart) AS roundStart
                FROM chosen_round cr
                INNER JOIN stock_take st ON st.stockTakeRoundId = cr.roundId AND st.deleted = 0
                GROUP BY cr.roundId
            ),
            next_round_time AS (
                SELECT
                    rt.roundId AS roundId,
                    COALESCE(
                        (
                            SELECT MIN(st2.planStart)
                            FROM stock_take st2
                            WHERE st2.deleted = 0
                              AND st2.planStart > rt.roundStart
                        ),
                        NOW()
                    ) AS nextRoundStart
                FROM round_time rt
            ),
            prev_round_time AS (
                SELECT
                    rt.roundId AS roundId,
                    COALESCE(
                        (
                            SELECT MAX(st0.planStart)
                            FROM stock_take st0
                            WHERE st0.deleted = 0
                              AND st0.planStart < rt.roundStart
                        ),
                        TIMESTAMP('1970-01-01 00:00:00')
                    ) AS prevRoundStart
                FROM round_time rt
            ),
            ledger_window AS (
                SELECT
                    sl.itemCode AS itemCode,
                    sl.itemId AS itemId,
                    sl.id AS slId,
                    COALESCE(sl.inQty, 0) AS inQty,
                    COALESCE(sl.outQty, 0) AS outQty,
                    COALESCE(sl.balance, 0) AS ledgerBalance,
                    UPPER(TRIM(COALESCE(sl.type, ''))) AS normType,
                    sl.date AS ledgerDate,
                    sl.created AS ledgerCreated,
                    COALESCE(lot_wh.storeLocation, '') AS storeLocation,
                    pol_in.id AS polInId,
                    pol_in.up AS polInUp,
                    pol_out.id AS polOutId,
                    pol_out.up AS polOutUp,
                    iu_stock.id AS iuStockId,
                    iu_stock.ratioN AS iuStockRatioN,
                    iu_stock.ratioD AS iuStockRatioD,
                    iu_purchase.id AS iuPurchaseId,
                    iu_purchase.ratioN AS iuPurchaseRatioN,
                    iu_purchase.ratioD AS iuPurchaseRatioD
                FROM stock_ledger sl
                    LEFT JOIN stock_in_line sil ON sl.stockInLineId = sil.id AND sil.deleted = 0
                    LEFT JOIN inventory_lot il_in ON sil.inventoryLotId = il_in.id AND il_in.deleted = 0
                    LEFT JOIN stock_out_line sol ON sl.stockOutLineId = sol.id AND sol.deleted = 0
                    LEFT JOIN inventory_lot_line ill_out ON sol.inventoryLotLineId = ill_out.id AND ill_out.deleted = 0
                    LEFT JOIN inventory_lot il_out ON ill_out.inventoryLotId = il_out.id AND il_out.deleted = 0
                    LEFT JOIN (
                        SELECT il.id AS lotId, MAX(wh.code) AS storeLocation
                        FROM inventory_lot il
                        LEFT 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
                        GROUP BY il.id
                    ) lot_wh ON lot_wh.lotId = COALESCE(il_in.id, il_out.id)
                    LEFT JOIN purchase_order_line pol_in ON sil.purchaseOrderLineId = pol_in.id AND pol_in.deleted = 0
                    LEFT JOIN stock_in_line sil_out ON il_out.stockInLineId = sil_out.id AND sil_out.deleted = 0
                    LEFT JOIN purchase_order_line pol_out ON sil_out.purchaseOrderLineId = pol_out.id AND pol_out.deleted = 0
                    LEFT JOIN item_uom iu_stock ON sl.itemId = iu_stock.itemId AND iu_stock.stockUnit = 1 AND iu_stock.deleted = 0
                    LEFT JOIN item_uom iu_purchase ON sl.itemId = iu_purchase.itemId AND iu_purchase.purchaseUnit = 1 AND iu_purchase.deleted = 0
                    INNER JOIN round_time rt ON 1 = 1
                    INNER JOIN next_round_time nrt ON 1 = 1
                WHERE sl.deleted = 0
                  AND sl.itemCode IS NOT NULL AND sl.itemCode <> ''
                  AND sl.created >= rt.roundStart
                  AND sl.created < nrt.nextRoundStart
                  $itemCodeSql
            ),
            latest_tke_in_window_per_item AS (
                SELECT
                    ranked.itemId,
                    ranked.tkeCreated,
                    ranked.tkeId,
                    ranked.tkeBalance
                FROM (
                    SELECT
                        lw.itemId,
                        lw.ledgerCreated AS tkeCreated,
                        lw.slId AS tkeId,
                        lw.ledgerBalance AS tkeBalance,
                        ROW_NUMBER() OVER (
                            PARTITION BY lw.itemId
                            ORDER BY lw.ledgerCreated DESC, lw.slId DESC
                        ) AS rn
                    FROM ledger_window lw
                    WHERE lw.normType = 'TKE'
                ) ranked
                WHERE ranked.rn = 1
            ),
            prev_window_cost_base AS (
                SELECT
                    sl.itemId,
                    sl.itemCode,
                    sl.created AS ledgerCreated,
                    sl.id AS slId,
                    (
                        COALESCE(pol_in.up, 0)
                        * (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
                        / (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0))
                    ) AS stockUnitCost
                FROM stock_ledger sl
                    LEFT JOIN stock_in_line sil ON sl.stockInLineId = sil.id AND sil.deleted = 0
                    LEFT JOIN purchase_order_line pol_in ON sil.purchaseOrderLineId = pol_in.id AND pol_in.deleted = 0
                    LEFT JOIN item_uom iu_stock ON sl.itemId = iu_stock.itemId AND iu_stock.stockUnit = 1 AND iu_stock.deleted = 0
                    LEFT JOIN item_uom iu_purchase ON sl.itemId = iu_purchase.itemId AND iu_purchase.purchaseUnit = 1 AND iu_purchase.deleted = 0
                    INNER JOIN round_time rt ON 1 = 1
                    INNER JOIN prev_round_time prt ON 1 = 1
                WHERE sl.deleted = 0
                  AND sl.itemCode IS NOT NULL AND sl.itemCode <> ''
                  AND sl.created >= prt.prevRoundStart
                  AND sl.created < rt.roundStart
                  AND COALESCE(sl.inQty, 0) > 0
                  AND pol_in.id IS NOT NULL
                  AND iu_stock.id IS NOT NULL
                  AND iu_purchase.id IS NOT NULL
                  AND COALESCE(iu_purchase.ratioN, 0) > 0
                  AND UPPER(TRIM(COALESCE(sl.type, ''))) NOT IN ('TKE', 'OPEN', 'ADJ')
            ),
            prev_window_cost_per_item AS (
                SELECT
                    ranked.itemId,
                    ranked.stockUnitCost
                FROM (
                    SELECT
                        b.itemId,
                        b.stockUnitCost,
                        ROW_NUMBER() OVER (
                            PARTITION BY b.itemId
                            ORDER BY b.ledgerCreated DESC, b.slId DESC
                        ) AS rn
                    FROM prev_window_cost_base b
                    WHERE COALESCE(b.stockUnitCost, 0) > 0
                ) ranked
                WHERE ranked.rn = 1
            ),
            ledger_flagged AS (
                SELECT
                    lw.*,
                    COALESCE(lt.tkeBalance, 0) AS openingBalancePerItem,
                    COALESCE(pwc.stockUnitCost, 0) AS prevWindowStockUnitCost,
                    CASE
                        WHEN lt.tkeCreated IS NULL THEN 1
                        WHEN lw.ledgerCreated > lt.tkeCreated THEN 1
                        WHEN lw.ledgerCreated = lt.tkeCreated AND lw.slId > lt.tkeId THEN 1
                        ELSE 0
                    END AS isMovementRow
                FROM ledger_window lw
                LEFT JOIN latest_tke_in_window_per_item lt ON lt.itemId = lw.itemId
                LEFT JOIN prev_window_cost_per_item pwc ON pwc.itemId = lw.itemId
            )
            SELECT
                '' as stockSubCategory,
                COALESCE(item_agg.itemNo, '') as itemNo,
                COALESCE(item_agg.itemName, '') as itemName,
                COALESCE(item_agg.unitOfMeasure, '') as unitOfMeasure,
                '' as lotNo,
                '' as expiryDate,
                '' as openingBalance,
                '' as cumStockIn,
                '' as cumStockOut,
                '' as currentBalance,
                '' as reOrderQty,
                COALESCE(item_agg.storeLocation, '') as storeLocation,
                COALESCE(DATE_FORMAT(item_agg.lastInDate, '%Y-%m-%d'), '') as lastInDate,
                COALESCE(DATE_FORMAT(item_agg.lastOutDate, '%Y-%m-%d'), '') as lastOutDate,
                CASE WHEN COALESCE(item_agg.totalOpeningBalance, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalOpeningBalance, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalOpeningBalance, 0), 0) END as totalOpeningBalance,
                CASE WHEN COALESCE(item_agg.totalCumStockIn, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalCumStockIn, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalCumStockIn, 0), 0) END as totalCumStockIn,
                CASE WHEN COALESCE(item_agg.totalCumStockOut, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalCumStockOut, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalCumStockOut, 0), 0) END as totalCumStockOut,
                CASE WHEN COALESCE(item_agg.totalCurrentBalance, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalCurrentBalance, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalCurrentBalance, 0), 0) END as totalCurrentBalance,
                '' as misInputAndLost,
                '' as defectiveGoods,
                '' as variance,
                CASE WHEN COALESCE(item_agg.totalMisInputAndLost, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalMisInputAndLost, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalMisInputAndLost, 0), 0) END as totalMisInputAndLost,
                CASE WHEN COALESCE(item_agg.totalVariance, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalVariance, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalVariance, 0), 0) END as totalVariance,
                CASE WHEN COALESCE(item_agg.totalDefectiveGoods, 0) < 0 THEN CONCAT('(', FORMAT(-item_agg.totalDefectiveGoods, 0), ')') ELSE FORMAT(COALESCE(item_agg.totalDefectiveGoods, 0), 0) END as totalDefectiveGoods,
                CASE
                    WHEN COALESCE(item_agg.totalCurrentBalance, 0) > 0
                    THEN FORMAT(ROUND((COALESCE(item_agg.total_in_value, 0) - COALESCE(item_agg.total_out_value, 0)) / item_agg.totalCurrentBalance, 2), 2)
                    ELSE '0.00'
                END as avgUnitPrice,
                CASE
                    WHEN COALESCE(item_agg.totalCurrentBalance, 0) > 0
                    THEN FORMAT(ROUND(item_agg.totalCurrentBalance * (COALESCE(item_agg.total_in_value, 0) - COALESCE(item_agg.total_out_value, 0)) / item_agg.totalCurrentBalance, 2), 2)
                    ELSE '0.00'
                END as totalStockBalance
            FROM (
                SELECT
                    agg.itemCode AS itemNo,
                    agg.itemId,
                    it.name AS itemName,
                    COALESCE(uc.udfudesc, uc.code, '') AS unitOfMeasure,
                    SUM(agg.openingBalance) AS totalOpeningBalance,
                    SUM(agg.cumStockIn) AS totalCumStockIn,
                    SUM(agg.cumStockOut) AS totalCumStockOut,
                    SUM(agg.currentBalance) AS totalCurrentBalance,
                    SUM(agg.cumStockOutMiss) AS totalMisInputAndLost,
                    SUM(agg.cumStockOutAdjStockTake) AS totalVariance,
                    SUM(agg.cumStockOutBad) AS totalDefectiveGoods,
                    MAX(agg.storeLocation) AS storeLocation,
                    MAX(agg.lastInDate) AS lastInDate,
                    MAX(agg.lastOutDate) AS lastOutDate,
                    SUM(agg.total_in_value) AS total_in_value,
                    SUM(agg.total_out_value) AS total_out_value
                FROM (
                    SELECT
                        lf.itemCode,
                        lf.itemId,
                        MAX(lf.openingBalancePerItem) AS openingBalance,
                        SUM(
                            CASE
                                WHEN lf.isMovementRow = 1
                                     AND lf.normType NOT IN ('OPEN', 'TKE')
                                    THEN COALESCE(lf.inQty, 0)
                                ELSE 0
                            END
                        ) AS cumStockIn,
                        SUM(
                            CASE
                                WHEN COALESCE(lf.outQty, 0) > 0
                                     AND lf.isMovementRow = 1
                                     AND lf.normType NOT IN (
                                         'OPEN', 'MISS', 'BAD', 'TKE', 'ADJ', 'STOCKTAKE'
                                     )
                                    THEN COALESCE(lf.outQty, 0)
                                ELSE 0
                            END
                        ) AS cumStockOut,
                        MAX(lf.openingBalancePerItem) + SUM(
                            CASE
                                WHEN lf.isMovementRow = 1
                                    THEN COALESCE(lf.inQty, 0) - COALESCE(lf.outQty, 0)
                                ELSE 0
                            END
                        ) AS currentBalance,
                        MAX(CASE WHEN lf.isMovementRow = 1 AND COALESCE(lf.inQty, 0) > 0 THEN lf.ledgerDate END) AS lastInDate,
                        MAX(CASE WHEN lf.isMovementRow = 1 AND COALESCE(lf.outQty, 0) > 0 THEN lf.ledgerDate END) AS lastOutDate,
                        MAX(lf.storeLocation) AS storeLocation,
                        SUM(
                            CASE
                                WHEN lf.isMovementRow = 1
                                     AND COALESCE(lf.outQty, 0) > 0
                                     AND lf.normType = 'MISS'
                                    THEN COALESCE(lf.outQty, 0)
                                ELSE 0
                            END
                        ) AS cumStockOutMiss,
                        SUM(
                            CASE
                                WHEN lf.isMovementRow = 1
                                     AND COALESCE(lf.outQty, 0) > 0
                                     AND lf.normType = 'BAD'
                                    THEN COALESCE(lf.outQty, 0)
                                ELSE 0
                            END
                        ) AS cumStockOutBad,
                        SUM(
                            CASE
                                WHEN lf.isMovementRow = 1
                                     AND COALESCE(lf.outQty, 0) > 0
                                     AND lf.normType IN ('TKE', 'ADJ', 'STOCKTAKE')
                                    THEN COALESCE(lf.outQty, 0)
                                ELSE 0
                            END
                        ) AS cumStockOutAdjStockTake,
                        SUM(
                            CASE
                                WHEN lf.isMovementRow = 1
                                    THEN
                                        COALESCE(lf.inQty, 0) * CASE
                                            WHEN lf.normType = 'TKE' THEN COALESCE(lf.prevWindowStockUnitCost, 0)
                                            WHEN lf.polInId IS NOT NULL
                                                 AND lf.iuStockId IS NOT NULL
                                                 AND lf.iuPurchaseId IS NOT NULL
                                                 AND COALESCE(lf.iuPurchaseRatioN, 0) > 0
                                                THEN COALESCE(lf.polInUp, 0)
                                                     * (lf.iuStockRatioN / NULLIF(lf.iuStockRatioD, 0))
                                                     / (lf.iuPurchaseRatioN / NULLIF(lf.iuPurchaseRatioD, 0))
                                            ELSE 0
                                        END
                                ELSE 0
                            END
                        ) AS total_in_value,
                        SUM(
                            CASE
                                WHEN lf.isMovementRow = 1
                                    THEN
                                        COALESCE(lf.outQty, 0) * CASE
                                            WHEN lf.normType = 'TKE' THEN COALESCE(lf.prevWindowStockUnitCost, 0)
                                            WHEN lf.polOutId IS NOT NULL
                                                 AND lf.iuStockId IS NOT NULL
                                                 AND lf.iuPurchaseId IS NOT NULL
                                                 AND COALESCE(lf.iuPurchaseRatioN, 0) > 0
                                                THEN COALESCE(lf.polOutUp, 0)
                                                     * (lf.iuStockRatioN / NULLIF(lf.iuStockRatioD, 0))
                                                     / (lf.iuPurchaseRatioN / NULLIF(lf.iuPurchaseRatioD, 0))
                                            ELSE 0
                                        END
                                ELSE 0
                            END
                        ) AS total_out_value
                    FROM ledger_flagged lf
                    GROUP BY lf.itemCode, lf.itemId
                ) agg
                LEFT JOIN items it ON agg.itemId = it.id AND it.deleted = 0
                LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = 1 AND iu.deleted = 0
                LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
                WHERE 1 = 1
                $stockCategorySql
                $storeLocationSql
                GROUP BY agg.itemCode, agg.itemId, it.id, it.code, it.name, uc.udfudesc, uc.code
                HAVING 1=1
            ) item_agg
            WHERE 1=1
        """.trimIndent()

        val havingConditions = mutableListOf<String>()
        val currentBalanceExpr = "COALESCE(totalCurrentBalance, 0)"

        if (!balanceFilterStart.isNullOrBlank()) {
            args["balanceFilterStart"] = balanceFilterStart.toDoubleOrNull() ?: 0.0
            havingConditions.add("$currentBalanceExpr >= :balanceFilterStart")
        }
        if (!balanceFilterEnd.isNullOrBlank()) {
            args["balanceFilterEnd"] = balanceFilterEnd.toDoubleOrNull() ?: 0.0
            havingConditions.add("$currentBalanceExpr <= :balanceFilterEnd")
        }
        if (!lastInDateStart.isNullOrBlank()) {
            val formattedDate = lastInDateStart.replace("/", "-")
            args["lastInDateStart"] = formattedDate
            havingConditions.add("(lastInDate IS NOT NULL AND DATE(lastInDate) >= DATE(:lastInDateStart))")
        }
        if (!lastInDateEnd.isNullOrBlank()) {
            val formattedDate = lastInDateEnd.replace("/", "-")
            args["lastInDateEnd"] = formattedDate
            havingConditions.add("(lastInDate IS NOT NULL AND DATE(lastInDate) <= DATE(:lastInDateEnd))")
        }
        if (!lastOutDateStart.isNullOrBlank()) {
            val formattedDate = lastOutDateStart.replace("/", "-")
            args["lastOutDateStart"] = formattedDate
            havingConditions.add("(lastOutDate IS NOT NULL AND DATE(lastOutDate) >= DATE(:lastOutDateStart))")
        }
        if (!lastOutDateEnd.isNullOrBlank()) {
            val formattedDate = lastOutDateEnd.replace("/", "-")
            args["lastOutDateEnd"] = formattedDate
            havingConditions.add("(lastOutDate IS NOT NULL AND DATE(lastOutDate) <= DATE(:lastOutDateEnd))")
        }

        val finalSql = if (havingConditions.isNotEmpty()) {
            sql.replace("HAVING 1=1", "HAVING ${havingConditions.joinToString(" AND ")}")
        } else {
            sql.replace("GROUP BY agg.itemCode, agg.itemId, it.id, it.code, it.name, uc.udfudesc, uc.code\n                HAVING 1=1", "GROUP BY agg.itemCode, agg.itemId, it.id, it.code, it.name, uc.udfudesc, uc.code")
        }
        return jdbcDao.queryForList("$finalSql ORDER BY itemNo", args)
    }

    /**
     * Stock Balance Report (date-driven).
     *
     * - 期初存量: stockDate - 1 的最後一筆 stock_ledger.balance (以 sl.date, sl.id 排序)
     * - 現存存貨: stockDate 的最後一筆 stock_ledger.balance (以 sl.date, sl.id 排序)
     * - 單位均價: 依 item 判斷是否 BOM
     *   - BOM: 用 delivery_order_line (prefer up, else price/qty, else price) 算加權平均
     *   - 非 BOM: 用 purchase_order_line (prefer up, else price/qty, else price) 算加權平均
     * - 庫存總價值: 單位均價 * 現存存貨
     */
    fun searchStockBalanceReportByDate(
        stockCategory: String?,
        itemCode: String?,
        stockDate: String,
        balanceFilterStart: String?,
        balanceFilterEnd: String?,
        storeLocation: String?,
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()
        val formattedStockDate = stockDate.replace("/", "-")
        args["stockDate"] = formattedStockDate

        val stockCategorySql = buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args)
        val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
        val storeLocationSql = if (!storeLocation.isNullOrBlank()) {
            args["storeLocation"] = "%$storeLocation%"
            "AND COALESCE(store_location.storeLocation, '') LIKE :storeLocation"
        } else ""

        val baseSql = """
            WITH params AS (
                SELECT
                    DATE(:stockDate) AS d0,
                    DATE_SUB(DATE(:stockDate), INTERVAL 1 DAY) AS d1
            ),
            ledger_item_ids AS (
                SELECT DISTINCT sl.itemId
                FROM stock_ledger sl
                    INNER JOIN params p ON 1=1
                WHERE sl.deleted = 0
                  AND sl.itemId IS NOT NULL
                  AND DATE(sl.date) <= p.d0
            ),
            item_scope AS (
                SELECT
                    it.id AS itemId,
                    it.code AS itemNo,
                    it.name AS itemName,
                    it.type AS itemType,
                    COALESCE(CAST(NULLIF(it.AverageUnitPrice, '') AS DECIMAL(14, 4)), 0) AS avgUnitPriceRaw
                FROM items it
                    INNER JOIN ledger_item_ids li ON li.itemId = it.id
                WHERE it.deleted = 0
                  AND it.code IS NOT NULL AND it.code <> ''
                  $itemCodeSql
                  $stockCategorySql
            ),
            store_location AS (
                SELECT
                    sl.itemId,
                    MAX(wh.code) AS storeLocation
                FROM stock_ledger sl
                    LEFT JOIN stock_in_line sil ON sl.stockInLineId = sil.id AND sil.deleted = 0
                    LEFT JOIN inventory_lot il_in ON sil.inventoryLotId = il_in.id AND il_in.deleted = 0
                    LEFT JOIN stock_out_line sol ON sl.stockOutLineId = sol.id AND sol.deleted = 0
                    LEFT JOIN inventory_lot_line ill_out ON sol.inventoryLotLineId = ill_out.id AND ill_out.deleted = 0
                    LEFT JOIN inventory_lot il_out ON ill_out.inventoryLotId = il_out.id AND il_out.deleted = 0
                    LEFT JOIN inventory_lot_line ill_any ON ill_any.inventoryLotId = COALESCE(il_in.id, il_out.id) AND ill_any.deleted = 0
                    LEFT JOIN warehouse wh ON ill_any.warehouseId = wh.id AND wh.deleted = 0
                WHERE sl.deleted = 0
                GROUP BY sl.itemId
            ),
            opening_ranked AS (
                SELECT
                    sl.itemId,
                    COALESCE(sl.balance, 0) AS openingBalance,
                    ROW_NUMBER() OVER (PARTITION BY sl.itemId ORDER BY sl.date DESC, sl.id DESC) AS rn
                FROM stock_ledger sl
                    INNER JOIN params p ON 1=1
                WHERE sl.deleted = 0
                  AND sl.itemId IS NOT NULL
                  AND DATE(sl.date) <= p.d1
            ),
            opening_per_item AS (
                SELECT itemId, openingBalance
                FROM opening_ranked
                WHERE rn = 1
            ),
            current_ranked AS (
                SELECT
                    sl.itemId,
                    COALESCE(sl.balance, 0) AS currentBalance,
                    ROW_NUMBER() OVER (PARTITION BY sl.itemId ORDER BY sl.date DESC, sl.id DESC) AS rn
                FROM stock_ledger sl
                    INNER JOIN params p ON 1=1
                WHERE sl.deleted = 0
                  AND sl.itemId IS NOT NULL
                  AND DATE(sl.date) <= p.d0
            ),
            current_per_item AS (
                SELECT itemId, currentBalance
                FROM current_ranked
                WHERE rn = 1
            ),
            last_in_out AS (
                SELECT
                    sl.itemId,
                    MAX(CASE WHEN COALESCE(sl.inQty, 0) > 0 THEN sl.date END) AS lastInDate,
                    MAX(CASE WHEN COALESCE(sl.outQty, 0) > 0 THEN sl.date END) AS lastOutDate
                FROM stock_ledger sl
                    INNER JOIN params p ON 1=1
                WHERE sl.deleted = 0
                  AND sl.itemId IS NOT NULL
                  AND DATE(sl.date) <= p.d0
                GROUP BY sl.itemId
            ),
            ledger_moves AS (
                SELECT
                    sl.itemId,
                    SUM(
                        CASE
                            WHEN UPPER(TRIM(COALESCE(sl.type, ''))) IN ('NOR', 'ADJ')
                                 AND COALESCE(sl.inQty, 0) > 0
                                THEN COALESCE(sl.inQty, 0)
                            ELSE 0
                        END
                    ) AS cumStockIn,
                    SUM(
                        CASE
                            WHEN UPPER(TRIM(COALESCE(sl.type, ''))) IN ('NOR', 'ADJ')
                                 AND COALESCE(sl.outQty, 0) > 0
                                THEN COALESCE(sl.outQty, 0)
                            ELSE 0
                        END
                    ) AS cumStockOut,
                    SUM(
                        CASE
                            WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'MISS'
                                 AND COALESCE(sl.outQty, 0) > 0
                                THEN COALESCE(sl.outQty, 0)
                            ELSE 0
                        END
                    ) AS misInputAndLost,
                    SUM(
                        CASE
                            WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'BAD'
                                 AND COALESCE(sl.outQty, 0) > 0
                                THEN COALESCE(sl.outQty, 0)
                            ELSE 0
                        END
                    ) AS defectiveGoods,
                    SUM(
                        CASE
                            WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'TKE'
                                THEN COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0)
                            ELSE 0
                        END
                    ) AS variance
                FROM stock_ledger sl
                    INNER JOIN params p ON 1=1
                WHERE sl.deleted = 0
                  AND sl.itemId IS NOT NULL
                  AND DATE(sl.date) > p.d1
                  AND DATE(sl.date) <= p.d0
                GROUP BY sl.itemId
            )
            SELECT
                '' AS stockSubCategory,
                COALESCE(s.itemNo, '') AS itemNo,
                COALESCE(s.itemName, '') AS itemName,
                COALESCE(uc.udfudesc, uc.code, '') AS unitOfMeasure,
                '' AS lotNo,
                '' AS expiryDate,
                '' AS openingBalance,
                '' AS cumStockIn,
                '' AS cumStockOut,
                '' AS currentBalance,
                '' AS reOrderQty,
                COALESCE(store_location.storeLocation, '') AS storeLocation,
                COALESCE(DATE_FORMAT(lio.lastInDate, '%Y-%m-%d'), '') AS lastInDate,
                COALESCE(DATE_FORMAT(lio.lastOutDate, '%Y-%m-%d'), '') AS lastOutDate,
                COALESCE(op.openingBalance, 0) AS openingBalanceRaw,
                COALESCE(cp.currentBalance, 0) AS currentBalanceRaw,
                CASE WHEN COALESCE(op.openingBalance, 0) < 0 THEN CONCAT('(', FORMAT(-op.openingBalance, 0), ')') ELSE FORMAT(COALESCE(op.openingBalance, 0), 0) END AS totalOpeningBalance,
                CASE WHEN COALESCE(lm.cumStockIn, 0) < 0 THEN CONCAT('(', FORMAT(-lm.cumStockIn, 0), ')') ELSE FORMAT(COALESCE(lm.cumStockIn, 0), 0) END AS totalCumStockIn,
                CASE WHEN COALESCE(lm.cumStockOut, 0) < 0 THEN CONCAT('(', FORMAT(-lm.cumStockOut, 0), ')') ELSE FORMAT(COALESCE(lm.cumStockOut, 0), 0) END AS totalCumStockOut,
                CASE WHEN COALESCE(cp.currentBalance, 0) < 0 THEN CONCAT('(', FORMAT(-cp.currentBalance, 0), ')') ELSE FORMAT(COALESCE(cp.currentBalance, 0), 0) END AS totalCurrentBalance,
                '' AS misInputAndLost,
                '' AS defectiveGoods,
                '' AS variance,
                CASE WHEN COALESCE(lm.misInputAndLost, 0) < 0 THEN CONCAT('(', FORMAT(-lm.misInputAndLost, 0), ')') ELSE FORMAT(COALESCE(lm.misInputAndLost, 0), 0) END AS totalMisInputAndLost,
                CASE WHEN COALESCE(lm.variance, 0) < 0 THEN CONCAT('(', FORMAT(-lm.variance, 0), ')') ELSE FORMAT(COALESCE(lm.variance, 0), 0) END AS totalVariance,
                CASE WHEN COALESCE(lm.defectiveGoods, 0) < 0 THEN CONCAT('(', FORMAT(-lm.defectiveGoods, 0), ')') ELSE FORMAT(COALESCE(lm.defectiveGoods, 0), 0) END AS totalDefectiveGoods,
                FORMAT(ROUND(COALESCE(s.avgUnitPriceRaw, 0), 2), 2) AS avgUnitPrice,
                FORMAT(ROUND(COALESCE(s.avgUnitPriceRaw, 0) * COALESCE(cp.currentBalance, 0), 2), 2) AS totalStockBalance
            FROM item_scope s
                LEFT JOIN opening_per_item op ON op.itemId = s.itemId
                LEFT JOIN current_per_item cp ON cp.itemId = s.itemId
                LEFT JOIN last_in_out lio ON lio.itemId = s.itemId
                LEFT JOIN ledger_moves lm ON lm.itemId = s.itemId
                LEFT JOIN item_uom iu ON iu.itemId = s.itemId AND iu.stockUnit = 1 AND iu.deleted = 0
                LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
                LEFT JOIN store_location ON store_location.itemId = s.itemId
            WHERE 1=1
              $storeLocationSql
        """.trimIndent()

        val filters = mutableListOf<String>()
        if (!balanceFilterStart.isNullOrBlank()) {
            args["balanceFilterStart"] = balanceFilterStart.toDoubleOrNull() ?: 0.0
            filters.add("COALESCE(currentBalanceRaw, 0) >= :balanceFilterStart")
        }
        if (!balanceFilterEnd.isNullOrBlank()) {
            args["balanceFilterEnd"] = balanceFilterEnd.toDoubleOrNull() ?: 0.0
            filters.add("COALESCE(currentBalanceRaw, 0) <= :balanceFilterEnd")
        }

        val finalSql =
            if (filters.isEmpty()) {
                // no numeric filter: just order by itemNo
                baseSql
            } else {
                // wrap to filter on raw numeric current balance
                """
                SELECT * FROM (
                    $baseSql
                ) base
                WHERE ${filters.joinToString(" AND ")}
                """.trimIndent()
            }

        return jdbcDao.queryForList("$finalSql ORDER BY itemNo", args)
    }
    /**
     * Compiles and fills a Jasper Report, then exports to Excel (.xlsx). Same layout/columns as the report template.
     */
    fun createExcelResponse(templatePath: String, params: Map<String, Any>, dataList: List<Map<String, Any>>): ByteArray {
        val stream = this::class.java.getResourceAsStream(templatePath)
            ?: throw RuntimeException("Report template not found: $templatePath")
        val jasperReport = JasperCompileManager.compileReport(stream)
        val dataSource = JRMapCollectionDataSource(dataList)
        val jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource)
        val out = ByteArrayOutputStream()
        val exporter = JRXlsxExporter()
        exporter.setExporterInput(SimpleExporterInput(jasperPrint))
        exporter.setExporterOutput(SimpleOutputStreamExporterOutput(out))
        exporter.exportReport()
        return out.toByteArray()
    }

    /**
     * Compiles and fills a Jasper Report, returning the PDF as a ByteArray.
     */
    fun createPdfResponse(templatePath: String, params: Map<String, Any>, dataList: List<Map<String, Any>>): ByteArray {
        val stream = this::class.java.getResourceAsStream(templatePath)
            ?: throw RuntimeException("Report template not found: $templatePath")
            
        val jasperReport = JasperCompileManager.compileReport(stream)
        
        val dataSource = JRMapCollectionDataSource(dataList)
        
        val jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource)
        return JasperExportManager.exportReportToPdf(jasperPrint)
    }
    
}