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

import com.ffii.core.support.JdbcDao
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.format.DateTimeFormatter
@Service
open class StockTakeVarianceReportService(
    private val jdbcDao: JdbcDao,
) {

    companion object {
        private const val ORIGIN_STOCK_IN_JOIN_SQL = """
  LEFT JOIN stock_in_line origin_sil
    ON origin_sil.id = il.stockInLineId AND origin_sil.deleted = 0
  LEFT JOIN purchase_order po
    ON po.id = origin_sil.purchaseOrderId AND po.deleted = 0
"""

        private const val LOT_ROOT_ORIGIN_CTE_SQL = """
lot_trace AS (
  SELECT
    il.id AS lotId,
    il.stockInLineId AS silId,
    0 AS depth
  FROM inventory_lot il
  WHERE il.deleted = 0
    AND il.stockInLineId IS NOT NULL
  UNION ALL
  SELECT
    lt.lotId AS lotId,
    il_src.stockInLineId AS silId,
    lt.depth + 1 AS depth
  FROM lot_trace lt
  INNER JOIN stock_in_line sil
    ON sil.id = lt.silId
   AND sil.deleted = 0
  INNER JOIN stock_transfer_record tr
    ON tr.id = sil.stockTransferId
   AND tr.deleted = 0
  INNER JOIN stock_out_line sol
    ON sol.id = tr.stockOutLineId
   AND sol.deleted = 0
  INNER JOIN inventory_lot_line ill_src
    ON ill_src.id = sol.inventoryLotLineId
   AND ill_src.deleted = 0
  INNER JOIN inventory_lot il_src
    ON il_src.id = ill_src.inventoryLotId
   AND il_src.deleted = 0
  WHERE lt.depth < 8
    AND il_src.stockInLineId IS NOT NULL
    AND (
      UPPER(TRIM(COALESCE(sil.type, ''))) = 'TRF'
      OR sil.stockTransferId IS NOT NULL
    )
),
lot_root_origin AS (
  SELECT
    lotId,
    silId AS rootSilId
  FROM (
    SELECT
      lotId,
      silId,
      ROW_NUMBER() OVER (PARTITION BY lotId ORDER BY depth DESC) AS rn
    FROM lot_trace
  ) t
  WHERE t.rn = 1
)"""

        private const val ROOT_STOCK_IN_JOIN_SQL = """
  LEFT JOIN lot_root_origin lro
    ON lro.lotId = il.id
  LEFT JOIN stock_in_line root_sil
    ON root_sil.id = lro.rootSilId AND root_sil.deleted = 0
  LEFT JOIN purchase_order root_po
    ON root_po.id = root_sil.purchaseOrderId AND root_po.deleted = 0
"""

        private const val LOT_ROOT_CATEGORY_SQL = """
    CASE
      WHEN root_sil.jobOrderId IS NOT NULL THEN '工廠生產'
      WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN' THEN '期初存貨'
      WHEN UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ' THEN '倉存調整'
      WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP' THEN 'PP'
      WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF' THEN 'PF'
      WHEN UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'TOA' THEN 'TOA'
      WHEN root_sil.purchaseOrderId IS NOT NULL THEN '採購入倉'
      ELSE '其他入倉'
    END"""

        private const val LOT_TYPE_DISPLAY_SQL = """
    CASE
      WHEN UPPER(TRIM(COALESCE(origin_sil.type, ''))) = 'TRF'
        OR origin_sil.stockTransferId IS NOT NULL
      THEN CONCAT('轉倉（原：', $LOT_ROOT_CATEGORY_SQL, '）')
      ELSE $LOT_ROOT_CATEGORY_SQL
    END"""
    }

    /**
     * Stock Take Variance 報表查詢
     *
     * 條件設計與 Stock Ledger 報表類似：
     * - stockCategory  : items.type，可逗號分隔（精確匹配）
     * - itemCode      : items.code，可逗號分隔（LIKE）
     * - storeLocation : warehouse.code，模糊匹配
     * - lastInDate*   : 依 inventory_lot.stockInDate 過濾
     *
     * 資料來源以 stocktakerecord 為主，欄位對應 `StockTakeVarianceReport.jrxml`：
     * - currentBookBalance  : bookQty
     * - stockTakeQty        : 以 approverStockTakeQty > pickerSecondStockTakeQty > pickerFirstStockTakeQty 為準
     * - variance            : stockTakeQty - bookQty
     * - variancePercentage  : variance / bookQty（%），bookQty 為 0 則輸出空字串
     *
     * 期初 / 累計存入 / 累計存出（openingBalance / cumStockIn / cumStockOut）：
     * - 開始存貨 openingBalance：僅統計 stock_ledger.type = 'OPEN' 的 inQty - outQty
     * - 累計存入量 cumStockIn：僅包含 type IN ('NOR', 'Miss', 'Bad') 的 inQty
     * - 累計存出量 cumStockOut：僅包含 type IN ('NOR', 'Miss', 'Bad') 的 outQty
     * - 調整 Adj：不計入 cumStockIn / cumStockOut，但因為 ledger.balance 已含 Adj，
     *   currentBookBalance 直接取該 item 最後一筆的 balance。
     *
     * totalOpeningBalance / totalCumStockIn / totalCumStockOut 目前先維持為空字串；
     * totalCurrentBalance 則為各 item 的 currentBookBalance 加總（但在本報表中只用到明細）。
     */
    fun searchStockTakeVarianceReport(
        stockCategory: String?,
        itemCode: String?,
        storeLocation: String?,
        stockTakeDateStart: String?,
        stockTakeDateEnd: String?,
        type: String? = null,
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()
    
        // 基本過濾一樣
        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 wh.code LIKE :storeLocation"
        } else {
            ""
        }
        val lotTypeFilterSql = buildLotTypeFilterClause(type)
    
        // 1) toDate：有填用傳入，沒填用今天
        val toDate = (stockTakeDateEnd?.replace("/", "-")
            ?: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
    
        // 2) fromDate：有填用傳入，沒填查 stock_ledger 最早日期
        val fromDate = if (!stockTakeDateStart.isNullOrBlank()) {
            stockTakeDateStart.replace("/", "-")
        } else {
            val minDateSql = """
                SELECT DATE_FORMAT(MIN(sl.date), '%Y-%m-%d') AS firstDate
                FROM stock_ledger sl
                WHERE sl.deleted = 0
                  AND sl.itemCode IS NOT NULL
                  AND sl.itemCode <> ''
            """.trimIndent()
    
            val minDateRow = jdbcDao.queryForList(minDateSql, emptyMap<String, Any>()).firstOrNull()
            (minDateRow?.get("firstDate") as? String) ?: toDate
        }
    
        args["fromDate"] = fromDate
        args["toDate"] = toDate
    
        val sql = """
WITH RECURSIVE
$LOT_ROOT_ORIGIN_CTE_SQL,
latest_str AS (
  SELECT
    s1.lotId,
    s1.warehouseId,
    s1.bookQty,
    s1.varianceQty,
    s1.approverStockTakeQty,
    s1.date,
    s1.id
  FROM stocktakerecord s1
  WHERE s1.deleted = 0
    AND s1.status = 'completed'
    AND s1.date >= :fromDate
    AND s1.date <= :toDate
    AND NOT EXISTS (
      SELECT 1
      FROM stocktakerecord s2
      WHERE s2.deleted = 0
        AND s2.status = 'completed'
        AND s2.date >= :fromDate
        AND s2.date <= :toDate
        AND s2.lotId = s1.lotId
        AND s2.warehouseId = s1.warehouseId
        AND (
          s2.date > s1.date
          OR (s2.date = s1.date AND s2.id > s1.id)
        )
    )
),
in_agg AS (
  SELECT
    ill.id AS inventoryLotLineId,
    SUM(CASE WHEN DATE(sil.receiptDate) < :fromDate THEN
        CASE WHEN sil.purchaseOrderLineId IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0)
             WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
             ELSE COALESCE(sil.acceptedQty, 0)
        END
        ELSE 0 END) AS inBefore,
    SUM(CASE WHEN DATE(sil.receiptDate) BETWEEN :fromDate AND :toDate THEN
        CASE WHEN sil.purchaseOrderLineId IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0)
             WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
             ELSE COALESCE(sil.acceptedQty, 0)
        END
        ELSE 0 END) AS inDuring,
    MAX(CASE WHEN sil.receiptDate IS NOT NULL THEN DATE(sil.receiptDate) END) AS lastInDate
  FROM inventory_lot_line ill
  INNER JOIN inventory_lot il
    ON ill.inventoryLotId = il.id
   AND il.deleted = 0
  INNER JOIN items it
    ON il.itemId = it.id
   AND it.deleted = 0
  LEFT JOIN stock_in_line sil
    ON sil.inventoryLotLineId = ill.id
   AND sil.deleted = 0
   AND sil.status = 'completed'
  LEFT JOIN item_uom iu_purchase
    ON it.id = iu_purchase.itemId
   AND iu_purchase.purchaseUnit = 1
   AND iu_purchase.deleted = 0
  LEFT JOIN item_uom iu_stock
    ON it.id = iu_stock.itemId
   AND iu_stock.stockUnit = 1
   AND iu_stock.deleted = 0
  WHERE ill.deleted = 0
  GROUP BY ill.id
),
out_agg AS (
  SELECT
    ill.id AS inventoryLotLineId,
    SUM(CASE WHEN DATE(sol.endTime) < :fromDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outBefore,
    SUM(CASE WHEN DATE(sol.endTime) BETWEEN :fromDate AND :toDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outDuring,
    MAX(CASE WHEN sol.endTime IS NOT NULL THEN DATE(sol.endTime) END) AS lastOutDate
  FROM inventory_lot_line ill
  LEFT JOIN stock_out_line sol
    ON sol.inventoryLotLineId = ill.id
   AND sol.deleted = 0
   AND sol.status = 'completed'
  WHERE ill.deleted = 0
  GROUP BY ill.id
),
in_out AS (
  SELECT
    i.inventoryLotLineId,
    COALESCE(i.inBefore, 0)  AS inBefore,
    COALESCE(o.outBefore, 0) AS outBefore,
    COALESCE(i.inDuring, 0)  AS inDuring,
    COALESCE(o.outDuring, 0) AS outDuring,
    i.lastInDate,
    o.lastOutDate
  FROM in_agg i
  LEFT JOIN out_agg o ON o.inventoryLotLineId = i.inventoryLotLineId
),
data AS (
  SELECT
    it.type                                        AS stockSubCategory,
    it.code                                        AS itemNo,
    it.name                                        AS itemName,
    uc.udfudesc                                    AS unitOfMeasure,

    il.lotNo                                       AS lotNo,
    COALESCE(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
    wh.code                                        AS storeLocation,

    /* 數值先算出來，外層再 FORMAT 成字串，方便做 totals */
    (COALESCE(io.inBefore, 0) - COALESCE(io.outBefore, 0))                                    AS openingQty,
    COALESCE(io.inDuring, 0)                                                                  AS inQty,
    COALESCE(io.outDuring, 0)                                                                 AS outQty,
    ((COALESCE(io.inBefore, 0) - COALESCE(io.outBefore, 0)) + COALESCE(io.inDuring, 0) - COALESCE(io.outDuring, 0)) AS currentQty,

    io.lastInDate                                                                              AS lastInDateRaw,
    io.lastOutDate                                                                             AS lastOutDateRaw,

    ls.bookQty                                                                                 AS stkBookQty,
    ls.approverStockTakeQty                                                                    AS stkApproverQty,
    ls.varianceQty                                                                             AS stkVarianceQty,
    ls.date                                                                                    AS stockTakeDateRaw,
    $LOT_TYPE_DISPLAY_SQL                                                                       AS lotTypeRaw
  FROM inventory_lot_line ill
  INNER JOIN inventory_lot il
    ON ill.inventoryLotId = il.id
   AND il.deleted = 0
  INNER JOIN items it
    ON il.itemId = it.id
   AND it.deleted = 0
  INNER JOIN warehouse wh
    ON ill.warehouseId = wh.id
   AND wh.deleted = 0
  $ORIGIN_STOCK_IN_JOIN_SQL
  $ROOT_STOCK_IN_JOIN_SQL

  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

  LEFT JOIN in_out io
    ON io.inventoryLotLineId = ill.id

  LEFT JOIN latest_str ls
    ON ls.lotId = il.id
   AND ls.warehouseId = wh.id

  WHERE ill.deleted = 0
    $stockCategorySql
    $itemCodeSql
    $storeLocationSql
    $lotTypeFilterSql
)

SELECT
  stockSubCategory,
  itemNo,
  itemName,
  unitOfMeasure,
  lotNo,
  expiryDate,
  storeLocation,

  /* 期初/累計/現存：jrxml 欄位全是 String；負數括號顯示，無小數 */
  CASE WHEN COALESCE(openingQty, 0) < 0 THEN CONCAT('(', FORMAT(-openingQty, 0), ')') ELSE FORMAT(COALESCE(openingQty, 0), 0) END AS openingBalance,
  CASE WHEN COALESCE(inQty, 0) < 0 THEN CONCAT('(', FORMAT(-inQty, 0), ')') ELSE FORMAT(COALESCE(inQty, 0), 0) END AS cumStockIn,
  CASE WHEN COALESCE(outQty, 0) < 0 THEN CONCAT('(', FORMAT(-outQty, 0), ')') ELSE FORMAT(COALESCE(outQty, 0), 0) END AS cumStockOut,
  CASE WHEN COALESCE(currentQty, 0) < 0 THEN CONCAT('(', FORMAT(-currentQty, 0), ')') ELSE FORMAT(COALESCE(currentQty, 0), 0) END AS currentBookBalance,

  COALESCE(DATE_FORMAT(lastInDateRaw, '%Y-%m-%d'), '')  AS lastInDate,
  COALESCE(DATE_FORMAT(lastOutDateRaw, '%Y-%m-%d'), '') AS lastOutDate,
  COALESCE(DATE_FORMAT(stockTakeDateRaw, '%Y-%m-%d'), '') AS stockTakeDate,
  /* 取貨量 = approverStockTakeQty（無盤點紀錄顯示 0） */
  CASE
    WHEN stkApproverQty IS NULL THEN '0'
    WHEN COALESCE(stkApproverQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkApproverQty, 0), ')')
    ELSE FORMAT(COALESCE(stkApproverQty, 0), 0)
  END AS stockTakeQty,

  /* 超撿量 = varianceQty（無盤點紀錄顯示 0） */
  CASE
    WHEN stkVarianceQty IS NULL THEN '0'
    WHEN COALESCE(stkVarianceQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkVarianceQty, 0), ')')
    ELSE FORMAT(COALESCE(stkVarianceQty, 0), 0)
  END AS variance,

  /* 盤盈虧百分比（無盤點或 bookQty=0 顯示 0%） */
  CASE
    WHEN stkVarianceQty IS NULL THEN '0%'
    WHEN COALESCE(stkBookQty, 0) = 0 THEN '0%'
    WHEN (COALESCE(stkVarianceQty, 0) / stkBookQty) * 100 < 0 THEN CONCAT('(', FORMAT(-(COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%)')
    ELSE CONCAT(FORMAT((COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%')
  END AS variancePercentage,

  /* item totals（footer 用）；負數括號顯示，無小數 */
  CASE WHEN SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalOpeningBalance,
  CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn,
  CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut,
  CASE WHEN SUM(COALESCE(currentQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(currentQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(currentQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance,
  CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty,
  lotTypeRaw AS type

FROM data
ORDER BY
  itemNo,
  lotNo,
  storeLocation
""".trimIndent()
        val result = jdbcDao.queryForList(sql, args)
       
        return result
    }

    /**
     * V2：依盤點輪次 + 可選物料編號；輸出該輪所有 `stocktakerecord`（`deleted=0`，不限 `status`；未建立盤點紀錄的批號不列出）。
     * 期初／累計區間取該輪紀錄之 MIN(`date`)～MAX(`date`)（與 V1 之 in/out 聚合邏輯一致，但以 rb 帶入）。
     * 「審核時間」欄位：`approverTime` 之本地日期時間字串；無則退回盤點日 `date`。
     */
    fun searchStockTakeVarianceReportV2(
        stockTakeRoundId: Long,
        itemCode: String?,
        storeId: String?,
        status: String?,
        type: String? = null,
    ): List<Map<String, Any>> {
        val countSql = """
            SELECT COUNT(*) AS c FROM stocktakerecord s
            WHERE s.deleted = 0
              AND s.stockTakeRoundId = :stockTakeRoundId
        """.trimIndent()
        val cntRow = jdbcDao.queryForList(
            countSql,
            mapOf("stockTakeRoundId" to stockTakeRoundId)
        ).firstOrNull()
        val cnt = (cntRow?.get("c") as? Number)?.toLong() ?: 0L
        if (cnt == 0L) return emptyList()

        val args = mutableMapOf<String, Any>()
        args["stockTakeRoundId"] = stockTakeRoundId

        val statusNormalized = status?.trim()?.lowercase().orEmpty()
        // status 映射规则：
        // - All/null：不过滤
        // - pending：包含 pending/pass/notMatch
        // - completed：只看 completed
        val statusLatestSql = when (statusNormalized) {
            "pending" -> """
                AND str.status IN ('pending', 'pass', 'notMatch')
            """.trimIndent()
            "completed" -> """
                AND str.status = 'completed'
            """.trimIndent()
            else -> ""
        }

        val storeIdSql = run {
            val normalized = storeId?.trim()
            if (normalized.isNullOrBlank() || normalized.equals("all", ignoreCase = true)) {
                ""
            } else {
                args["storeId"] = normalized
                // DB 里 store_id 可能是 "2/F" 或 "2F"；用 REPLACE 去斜線做匹配
                """
                AND REPLACE(COALESCE(wh.store_id, ''), '/', '') = REPLACE(:storeId, '/', '')
                """.trimIndent()
            }
        }
        val itemCodeSql = buildMultiValueLikeClause(
            itemCode,
            "it.code",
            "itemCode",
            args
        )
        val lotTypeFilterSql = buildLotTypeFilterClause(type)

        val sql = """
WITH RECURSIVE
$LOT_ROOT_ORIGIN_CTE_SQL,
rb AS (
  SELECT
    COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate,
    COALESCE(MAX(s.date), CURRENT_DATE) AS toDate
  FROM stocktakerecord s
  WHERE s.deleted = 0
    AND s.stockTakeRoundId = :stockTakeRoundId
),
latest_str AS (
  SELECT
    str.lotId,
    str.warehouseId,
    str.bookQty,
    str.varianceQty,
    str.approverStockTakeQty,
    str.date AS strDate,
    str.id,
    str.approverTime,
    str.status AS stockTakeRecordStatus
  FROM stocktakerecord str
  WHERE str.deleted = 0
    AND str.stockTakeRoundId = :stockTakeRoundId
$statusLatestSql
),
in_agg AS (
  SELECT
    ill.id AS inventoryLotLineId,
    SUM(CASE WHEN DATE(sil.receiptDate) < rb.fromDate THEN
        CASE WHEN sil.purchaseOrderLineId IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0)
             WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
             ELSE COALESCE(sil.acceptedQty, 0)
        END
        ELSE 0 END) AS inBefore,
    SUM(CASE WHEN DATE(sil.receiptDate) BETWEEN rb.fromDate AND rb.toDate THEN
        CASE WHEN sil.purchaseOrderLineId IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0)
             WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
             ELSE COALESCE(sil.acceptedQty, 0)
        END
        ELSE 0 END) AS inDuring,
    MAX(CASE WHEN sil.receiptDate IS NOT NULL THEN DATE(sil.receiptDate) END) AS lastInDate
  FROM inventory_lot_line ill
  CROSS JOIN rb
  INNER JOIN inventory_lot il
    ON ill.inventoryLotId = il.id
   AND il.deleted = 0
  INNER JOIN items it
    ON il.itemId = it.id
   AND it.deleted = 0
  LEFT JOIN stock_in_line sil
    ON sil.inventoryLotLineId = ill.id
   AND sil.deleted = 0
   AND sil.status = 'completed'
  LEFT JOIN item_uom iu_purchase
    ON it.id = iu_purchase.itemId
   AND iu_purchase.purchaseUnit = 1
   AND iu_purchase.deleted = 0
  LEFT JOIN item_uom iu_stock
    ON it.id = iu_stock.itemId
   AND iu_stock.stockUnit = 1
   AND iu_stock.deleted = 0
  WHERE ill.deleted = 0
  GROUP BY ill.id
),
out_agg AS (
  SELECT
    ill.id AS inventoryLotLineId,
    SUM(CASE WHEN DATE(sol.endTime) < rb.fromDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outBefore,
    SUM(CASE WHEN DATE(sol.endTime) BETWEEN rb.fromDate AND rb.toDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outDuring,
    MAX(CASE WHEN sol.endTime IS NOT NULL THEN DATE(sol.endTime) END) AS lastOutDate
  FROM inventory_lot_line ill
  CROSS JOIN rb
  LEFT JOIN stock_out_line sol
    ON sol.inventoryLotLineId = ill.id
   AND sol.deleted = 0
   AND sol.status = 'completed'
  WHERE ill.deleted = 0
  GROUP BY ill.id
),
in_out AS (
  SELECT
    i.inventoryLotLineId,
    COALESCE(i.inBefore, 0)  AS inBefore,
    COALESCE(o.outBefore, 0) AS outBefore,
    COALESCE(i.inDuring, 0)  AS inDuring,
    COALESCE(o.outDuring, 0) AS outDuring,
    i.lastInDate,
    o.lastOutDate
  FROM in_agg i
  LEFT JOIN out_agg o ON o.inventoryLotLineId = i.inventoryLotLineId
),
data AS (
  SELECT
    it.type                                        AS stockSubCategory,
    it.code                                        AS itemNo,
    it.name                                        AS itemName,
    uc.udfudesc                                    AS unitOfMeasure,

    il.lotNo                                       AS lotNo,
    COALESCE(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
    wh.code                                        AS storeLocation,

    (COALESCE(io.inBefore, 0) - COALESCE(io.outBefore, 0))                                    AS openingQty,
    COALESCE(io.inDuring, 0)                                                                  AS inQty,
    COALESCE(io.outDuring, 0)                                                                 AS outQty,
    ((COALESCE(io.inBefore, 0) - COALESCE(io.outBefore, 0)) + COALESCE(io.inDuring, 0) - COALESCE(io.outDuring, 0)) AS currentQty,

    io.lastInDate                                                                              AS lastInDateRaw,
    io.lastOutDate                                                                             AS lastOutDateRaw,

    ls.bookQty                                                                                 AS stkBookQty,
    ls.approverStockTakeQty                                                                    AS stkApproverQty,
    ls.varianceQty                                                                             AS stkVarianceQty,
    ls.strDate                                                                                 AS stockTakeDateRaw,
    ls.approverTime                                                                            AS approvalDateTimeRaw,
    ls.stockTakeRecordStatus                                                                   AS stockTakeRecordStatus,
    $LOT_TYPE_DISPLAY_SQL                                                                      AS lotTypeRaw
  FROM latest_str ls
  INNER JOIN inventory_lot il
    ON ls.lotId = il.id
   AND il.deleted = 0
  INNER JOIN inventory_lot_line ill
    ON ill.inventoryLotId = il.id
   AND ill.warehouseId = ls.warehouseId
   AND ill.deleted = 0
  INNER JOIN items it
    ON il.itemId = it.id
   AND it.deleted = 0
  INNER JOIN warehouse wh
    ON wh.id = ls.warehouseId
   AND wh.deleted = 0
  $ORIGIN_STOCK_IN_JOIN_SQL
  $ROOT_STOCK_IN_JOIN_SQL

  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

  LEFT JOIN in_out io
    ON io.inventoryLotLineId = ill.id

  WHERE 1=1
    $itemCodeSql
    $storeIdSql
    $lotTypeFilterSql
)

SELECT
  stockSubCategory,
  itemNo,
  itemName,
  unitOfMeasure,
  lotNo,
  expiryDate,
  storeLocation,

  CASE WHEN COALESCE(openingQty, 0) < 0 THEN CONCAT('(', FORMAT(-openingQty, 0), ')') ELSE FORMAT(COALESCE(openingQty, 0), 0) END AS openingBalance,
  CASE WHEN COALESCE(inQty, 0) < 0 THEN CONCAT('(', FORMAT(-inQty, 0), ')') ELSE FORMAT(COALESCE(inQty, 0), 0) END AS cumStockIn,
  CASE WHEN COALESCE(outQty, 0) < 0 THEN CONCAT('(', FORMAT(-outQty, 0), ')') ELSE FORMAT(COALESCE(outQty, 0), 0) END AS cumStockOut,
  CASE WHEN COALESCE(stkBookQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkBookQty, 0), ')') ELSE FORMAT(COALESCE(stkBookQty, 0), 0) END AS currentBookBalance,

  COALESCE(DATE_FORMAT(lastInDateRaw, '%Y-%m-%d'), '')  AS lastInDate,
  COALESCE(DATE_FORMAT(lastOutDateRaw, '%Y-%m-%d'), '') AS lastOutDate,
  COALESCE(
    DATE_FORMAT(approvalDateTimeRaw, '%Y-%m-%d %H:%i:%s'),
    COALESCE(DATE_FORMAT(stockTakeDateRaw, '%Y-%m-%d'), '')
  ) AS stockTakeDate,

  CASE
    WHEN stkApproverQty IS NULL THEN '0'
    WHEN COALESCE(stkApproverQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkApproverQty, 0), ')')
    ELSE FORMAT(COALESCE(stkApproverQty, 0), 0)
  END AS stockTakeQty,

  CASE
    WHEN COALESCE(stockTakeRecordStatus, '') <> 'completed' THEN '0'
    WHEN stkVarianceQty IS NULL THEN '0'
    WHEN COALESCE(stkVarianceQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkVarianceQty, 0), ')')
    ELSE FORMAT(COALESCE(stkVarianceQty, 0), 0)
  END AS variance,

  CASE
    WHEN COALESCE(stockTakeRecordStatus, '') <> 'completed' THEN '0%'
    WHEN stkVarianceQty IS NULL THEN '0%'
    WHEN COALESCE(stkBookQty, 0) = 0 THEN '0%'
    WHEN (COALESCE(stkVarianceQty, 0) / stkBookQty) * 100 < 0 THEN CONCAT('(', FORMAT(-(COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%)')
    ELSE CONCAT(FORMAT((COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%')
  END AS variancePercentage,

  CASE WHEN SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalOpeningBalance,
  CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn,
  CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut,
  CASE WHEN SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance,
  CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty,
  lotTypeRaw AS type

FROM data
ORDER BY
  itemNo,
  lotNo,
  storeLocation
""".trimIndent()

        return jdbcDao.queryForList(sql, args)
    }

    /**
     * V2 Overall：每個 (lotId, warehouseId) 取 **status=completed** 且 **stockTakeRoundId 最大** 之一筆盤點紀錄
     * （同輪多筆時取 id 較大者）。期初／累計區間依該筆所屬輪次之 MIN～MAX(`date`）。
     * @param limitedToRoundIds null＝全系統所有輪次；非 null＝僅在指定輪次內取最新已完成
     */
    fun searchStockTakeVarianceReportV2Overall(
        itemCode: String?,
        storeId: String?,
        limitedToRoundIds: List<Long>? = null,
        type: String? = null,
    ): List<Map<String, Any>> {
        val args = mutableMapOf<String, Any>()

        val roundIdsFilterSql = if (limitedToRoundIds.isNullOrEmpty()) {
            ""
        } else {
            val inClause = limitedToRoundIds.mapIndexed { index, id ->
                val key = "overallRoundId_$index"
                args[key] = id
                ":$key"
            }.joinToString(", ")
            "AND s.stockTakeRoundId IN ($inClause)"
        }

        val storeIdSql = run {
            val normalized = storeId?.trim()
            if (normalized.isNullOrBlank() || normalized.equals("all", ignoreCase = true)) {
                ""
            } else {
                args["storeId"] = normalized
                """
                AND REPLACE(COALESCE(wh.store_id, ''), '/', '') = REPLACE(:storeId, '/', '')
                """.trimIndent()
            }
        }
        val itemCodeSql = buildMultiValueLikeClause(
            itemCode,
            "it.code",
            "itemCode",
            args
        )
        val lotTypeFilterSql = buildLotTypeFilterClause(type)

        val sql = """
WITH RECURSIVE
$LOT_ROOT_ORIGIN_CTE_SQL,
round_bounds AS (
  SELECT
    s.stockTakeRoundId,
    COALESCE(MIN(s.date), CURRENT_DATE) AS fromDate,
    COALESCE(MAX(s.date), CURRENT_DATE) AS toDate
  FROM stocktakerecord s
  WHERE s.deleted = 0
    $roundIdsFilterSql
  GROUP BY s.stockTakeRoundId
),
latest_str AS (
  SELECT
    str.lotId,
    str.warehouseId,
    str.stockTakeRoundId,
    str.bookQty,
    str.varianceQty,
    str.approverStockTakeQty,
    str.date AS strDate,
    str.id,
    str.approverTime,
    str.status AS stockTakeRecordStatus
  FROM stocktakerecord str
  INNER JOIN (
    SELECT
      s.lotId,
      s.warehouseId,
      MAX(s.stockTakeRoundId) AS maxRound
    FROM stocktakerecord s
    WHERE s.deleted = 0
      AND s.status = 'completed'
      $roundIdsFilterSql
    GROUP BY s.lotId, s.warehouseId
  ) mx ON mx.lotId = str.lotId
      AND mx.warehouseId = str.warehouseId
      AND mx.maxRound = str.stockTakeRoundId
  WHERE str.deleted = 0
    AND str.status = 'completed'
    AND NOT EXISTS (
      SELECT 1
      FROM stocktakerecord str2
      WHERE str2.deleted = 0
        AND str2.status = 'completed'
        AND str2.lotId = str.lotId
        AND str2.warehouseId = str.warehouseId
        AND str2.stockTakeRoundId = str.stockTakeRoundId
        AND str2.id > str.id
    )
),
line_bounds AS (
  SELECT
    ill.id AS inventoryLotLineId,
    ls.lotId,
    ls.warehouseId,
    rb.fromDate,
    rb.toDate,
    ls.bookQty,
    ls.varianceQty,
    ls.approverStockTakeQty,
    ls.strDate,
    ls.approverTime,
    ls.stockTakeRecordStatus
  FROM latest_str ls
  INNER JOIN round_bounds rb ON rb.stockTakeRoundId = ls.stockTakeRoundId
  INNER JOIN inventory_lot il ON ls.lotId = il.id AND il.deleted = 0
  INNER JOIN inventory_lot_line ill ON ill.inventoryLotId = il.id
    AND ill.warehouseId = ls.warehouseId
    AND ill.deleted = 0
),
in_agg AS (
  SELECT
    lb.inventoryLotLineId,
    SUM(CASE WHEN DATE(sil.receiptDate) < lb.fromDate THEN
        CASE WHEN sil.purchaseOrderLineId IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0)
             WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
             ELSE COALESCE(sil.acceptedQty, 0)
        END
        ELSE 0 END) AS inBefore,
    SUM(CASE WHEN DATE(sil.receiptDate) BETWEEN lb.fromDate AND lb.toDate THEN
        CASE WHEN sil.purchaseOrderLineId IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0)
             WHEN iu_purchase.id IS NOT NULL AND iu_stock.id IS NOT NULL
             THEN COALESCE(sil.acceptedQty, 0) * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu_stock.ratioN / NULLIF(iu_stock.ratioD, 0))
             ELSE COALESCE(sil.acceptedQty, 0)
        END
        ELSE 0 END) AS inDuring,
    MAX(CASE WHEN sil.receiptDate IS NOT NULL THEN DATE(sil.receiptDate) END) AS lastInDate
  FROM line_bounds lb
  INNER JOIN inventory_lot_line ill ON ill.id = lb.inventoryLotLineId
  INNER JOIN inventory_lot il ON ill.inventoryLotId = il.id AND il.deleted = 0
  INNER JOIN items it ON il.itemId = it.id AND it.deleted = 0
  LEFT JOIN stock_in_line sil
    ON sil.inventoryLotLineId = ill.id
   AND sil.deleted = 0
   AND sil.status = 'completed'
  LEFT JOIN item_uom iu_purchase
    ON it.id = iu_purchase.itemId
   AND iu_purchase.purchaseUnit = 1
   AND iu_purchase.deleted = 0
  LEFT JOIN item_uom iu_stock
    ON it.id = iu_stock.itemId
   AND iu_stock.stockUnit = 1
   AND iu_stock.deleted = 0
  WHERE ill.deleted = 0
  GROUP BY lb.inventoryLotLineId
),
out_agg AS (
  SELECT
    lb.inventoryLotLineId,
    SUM(CASE WHEN DATE(sol.endTime) < lb.fromDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outBefore,
    SUM(CASE WHEN DATE(sol.endTime) BETWEEN lb.fromDate AND lb.toDate THEN COALESCE(sol.qty, 0) ELSE 0 END) AS outDuring,
    MAX(CASE WHEN sol.endTime IS NOT NULL THEN DATE(sol.endTime) END) AS lastOutDate
  FROM line_bounds lb
  INNER JOIN inventory_lot_line ill ON ill.id = lb.inventoryLotLineId
  LEFT JOIN stock_out_line sol
    ON sol.inventoryLotLineId = ill.id
   AND sol.deleted = 0
   AND sol.status = 'completed'
  WHERE ill.deleted = 0
  GROUP BY lb.inventoryLotLineId
),
in_out AS (
  SELECT
    i.inventoryLotLineId,
    COALESCE(i.inBefore, 0)  AS inBefore,
    COALESCE(o.outBefore, 0) AS outBefore,
    COALESCE(i.inDuring, 0)  AS inDuring,
    COALESCE(o.outDuring, 0) AS outDuring,
    i.lastInDate,
    o.lastOutDate
  FROM in_agg i
  LEFT JOIN out_agg o ON o.inventoryLotLineId = i.inventoryLotLineId
),
data AS (
  SELECT
    it.type                                        AS stockSubCategory,
    it.code                                        AS itemNo,
    it.name                                        AS itemName,
    uc.udfudesc                                    AS unitOfMeasure,

    il.lotNo                                       AS lotNo,
    COALESCE(DATE_FORMAT(il.expiryDate, '%Y-%m-%d'), '') AS expiryDate,
    wh.code                                        AS storeLocation,

    (COALESCE(io.inBefore, 0) - COALESCE(io.outBefore, 0))                                    AS openingQty,
    COALESCE(io.inDuring, 0)                                                                  AS inQty,
    COALESCE(io.outDuring, 0)                                                                 AS outQty,
    ((COALESCE(io.inBefore, 0) - COALESCE(io.outBefore, 0)) + COALESCE(io.inDuring, 0) - COALESCE(io.outDuring, 0)) AS currentQty,

    io.lastInDate                                                                              AS lastInDateRaw,
    io.lastOutDate                                                                             AS lastOutDateRaw,

    lb.bookQty                                                                                 AS stkBookQty,
    lb.approverStockTakeQty                                                                    AS stkApproverQty,
    lb.varianceQty                                                                             AS stkVarianceQty,
    lb.strDate                                                                                 AS stockTakeDateRaw,
    lb.approverTime                                                                            AS approvalDateTimeRaw,
    lb.stockTakeRecordStatus                                                                   AS stockTakeRecordStatus,
    $LOT_TYPE_DISPLAY_SQL                                                                      AS lotTypeRaw
  FROM line_bounds lb
  INNER JOIN inventory_lot_line ill ON ill.id = lb.inventoryLotLineId
  INNER JOIN inventory_lot il ON ill.inventoryLotId = il.id AND il.deleted = 0
  INNER JOIN items it ON il.itemId = it.id AND it.deleted = 0
  INNER JOIN warehouse wh ON wh.id = lb.warehouseId AND wh.deleted = 0
  $ORIGIN_STOCK_IN_JOIN_SQL
  $ROOT_STOCK_IN_JOIN_SQL

  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

  LEFT JOIN in_out io
    ON io.inventoryLotLineId = ill.id

  WHERE 1=1
    $itemCodeSql
    $storeIdSql
    $lotTypeFilterSql
)

SELECT
  stockSubCategory,
  itemNo,
  itemName,
  unitOfMeasure,
  lotNo,
  expiryDate,
  storeLocation,

  CASE WHEN COALESCE(openingQty, 0) < 0 THEN CONCAT('(', FORMAT(-openingQty, 0), ')') ELSE FORMAT(COALESCE(openingQty, 0), 0) END AS openingBalance,
  CASE WHEN COALESCE(inQty, 0) < 0 THEN CONCAT('(', FORMAT(-inQty, 0), ')') ELSE FORMAT(COALESCE(inQty, 0), 0) END AS cumStockIn,
  CASE WHEN COALESCE(outQty, 0) < 0 THEN CONCAT('(', FORMAT(-outQty, 0), ')') ELSE FORMAT(COALESCE(outQty, 0), 0) END AS cumStockOut,
  CASE WHEN COALESCE(stkBookQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkBookQty, 0), ')') ELSE FORMAT(COALESCE(stkBookQty, 0), 0) END AS currentBookBalance,

  COALESCE(DATE_FORMAT(lastInDateRaw, '%Y-%m-%d'), '')  AS lastInDate,
  COALESCE(DATE_FORMAT(lastOutDateRaw, '%Y-%m-%d'), '') AS lastOutDate,
  COALESCE(
    DATE_FORMAT(approvalDateTimeRaw, '%Y-%m-%d %H:%i:%s'),
    COALESCE(DATE_FORMAT(stockTakeDateRaw, '%Y-%m-%d'), '')
  ) AS stockTakeDate,

  CASE
    WHEN stkApproverQty IS NULL THEN '0'
    WHEN COALESCE(stkApproverQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkApproverQty, 0), ')')
    ELSE FORMAT(COALESCE(stkApproverQty, 0), 0)
  END AS stockTakeQty,

  CASE
    WHEN COALESCE(stockTakeRecordStatus, '') <> 'completed' THEN '0'
    WHEN stkVarianceQty IS NULL THEN '0'
    WHEN COALESCE(stkVarianceQty, 0) < 0 THEN CONCAT('(', FORMAT(-stkVarianceQty, 0), ')')
    ELSE FORMAT(COALESCE(stkVarianceQty, 0), 0)
  END AS variance,

  CASE
    WHEN COALESCE(stockTakeRecordStatus, '') <> 'completed' THEN '0%'
    WHEN stkVarianceQty IS NULL THEN '0%'
    WHEN COALESCE(stkBookQty, 0) = 0 THEN '0%'
    WHEN (COALESCE(stkVarianceQty, 0) / stkBookQty) * 100 < 0 THEN CONCAT('(', FORMAT(-(COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%)')
    ELSE CONCAT(FORMAT((COALESCE(stkVarianceQty, 0) / stkBookQty) * 100, 0), '%')
  END AS variancePercentage,

  CASE WHEN SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(openingQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalOpeningBalance,
  CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockIn,
  CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCumStockOut,
  CASE WHEN SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkBookQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalCurrentBalance,
  CASE WHEN SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0), ')') ELSE FORMAT(SUM(COALESCE(stkApproverQty, 0)) OVER (PARTITION BY itemNo), 0) END AS totalStockTakeQty,
  lotTypeRaw AS type

FROM data
ORDER BY
  itemNo,
  lotNo,
  storeLocation
""".trimIndent()

        return jdbcDao.queryForList(sql, args)
    }

    /** Overall 模式報表副標題（全系統輪次，舊 API 相容） */
    fun getStockTakeVarianceOverallCaption(): String =
        "全輪次（各批號／倉別最新已完成盤點）"

    /** 多選輪次 Overall 模式報表副標題 */
    fun getStockTakeVarianceMultiRoundCaption(stockTakeRoundIds: List<Long>): String {
        if (stockTakeRoundIds.isEmpty()) return getStockTakeVarianceOverallCaption()
        val parts = stockTakeRoundIds.map { getStockTakeRoundCaption(it) }
        return "已選輪次（各批號／倉別最新已完成盤點）：${parts.joinToString("；")}"
    }

    /** 報表表頭：盤點輪次說明（與 /report/stock-take-rounds 選項格式一致） */
    fun getStockTakeRoundCaption(stockTakeRoundId: Long): String {
        val sql = """
            SELECT
              CONCAT(
                CASE
                  WHEN NULLIF(TRIM(MAX(st.stockTakeRoundName)), '') IS NULL
                  THEN CONCAT('盤點輪次', CAST(st.stockTakeRoundId AS CHAR))
                  ELSE TRIM(MAX(st.stockTakeRoundName))
                END,
                ' — ',
                DATE_FORMAT(MIN(st.planStart), '%Y-%m-%d')
              ) AS cap
            FROM stock_take st
            WHERE st.deleted = 0
              AND st.stockTakeRoundId = :stockTakeRoundId
            GROUP BY st.stockTakeRoundId
        """.trimIndent()
        val row = jdbcDao.queryForList(
            sql,
            mapOf("stockTakeRoundId" to stockTakeRoundId)
        ).firstOrNull()
        val cap = row?.get("cap") as? String
        return if (!cap.isNullOrBlank()) cap else "盤點輪次$stockTakeRoundId"
    }

    /**
     * 類型篩選：依「轉倉追溯後」之根來源類型過濾。
     *
     * 需與 [ORIGIN_STOCK_IN_JOIN_SQL] + [ROOT_STOCK_IN_JOIN_SQL] 一併使用：
     * - 顯示用 origin_sil（判斷是否轉倉）
     * - 篩選用 root_sil / root_po（根來源類型）
     */
    private fun buildLotTypeFilterClause(type: String?): String {
        val normalized = type?.trim().orEmpty()
        if (normalized.isBlank() || normalized.equals("all", ignoreCase = true)) return ""
        return when (normalized) {
            "PP" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PP'"
            "PF" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'PF'"
            "TOA" -> "AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) = 'TOA'"
            "工廠生產" -> "AND root_sil.jobOrderId IS NOT NULL"
            "倉存調整" -> "AND UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'ADJ'"
            "期初存貨" -> "AND UPPER(TRIM(COALESCE(root_sil.type, ''))) = 'OPEN'"
            "採購入倉" -> """
                AND root_sil.purchaseOrderId IS NOT NULL
                AND UPPER(LEFT(TRIM(COALESCE(root_po.code, '')), 2)) NOT IN ('PP', 'PF', 'TOA')
            """.trimIndent()
            "其他入倉" -> """
                AND (root_sil.purchaseOrderId IS NULL OR root_sil.purchaseOrderId = 0)
                AND root_sil.jobOrderId IS NULL
                AND UPPER(TRIM(COALESCE(root_sil.type, ''))) NOT IN ('OPEN', 'ADJ')
            """.trimIndent()
            else -> ""
        }
    }

    /** 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 ")})"
    }

    /** = 多值工具方法 */
    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 ")})"
    }
}

