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 StockLedgerReportService(
    private val jdbcDao: JdbcDao,
) {

    /**
     * Stock Ledger 報表查詢
     *
     * - stockSubCategory = items.type
     * - trnDate          = stock_ledger.date
     * - trnRefNo         = stock_ledger.type
     */
    fun searchStockLedgerReport(
        stockCategory: String?,
        itemCode: String?,
        storeLocation: String?,
        reportPeriodStart: String?,
        reportPeriodEnd: String?,
    ): List<Map<String, Any>> {
    
        val args = mutableMapOf<String, Any>()
    
        // 1) 先決定 reportPeriodEnd：如果有填 end，就用使用者的；否則用今天
        val reportPeriodEnd = (reportPeriodEnd?.replace("/", "-")
            ?: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
    
        // 2) 如果有填 start，就用使用者的；否則從 DB 查最早一筆日期
        val reportPeriodStart = if (!reportPeriodStart.isNullOrBlank()) {
            reportPeriodStart.replace("/", "-")
        } else {
            // 用簡單 SQL 查全表最早一筆日期（可加上類似 stockCategory/itemCode 過濾）
            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)
                ?: reportPeriodEnd   // 如果表是空的，就退回用今天
        }
    
        // 3) 把 from/to 塞到 args，供後面 SQL 使用
        args["reportPeriodStart"] = reportPeriodStart
        args["reportPeriodEnd"] = reportPeriodEnd
    
        // 4) 之後再用你原來的 stockCategorySql / itemCodeSql / storeLocationSql
        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 (wh_in.code LIKE :storeLocation OR wh_out.code LIKE :storeLocation)"
        } else {
            ""
        }
    
        val reportPeriodEndSql = "AND DATE(sl.date) <= :reportPeriodEnd"
    

        val sql = """
WITH base AS (
    SELECT
        sl.id                                                AS slId,
        DATE(sl.date)                                        AS trnDateRaw,
        DATE_FORMAT(sl.date, '%Y-%m-%d')                      AS trnDate,
        CASE
            WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'TKE' THEN 'TKE'
            WHEN UPPER(TRIM(COALESCE(sl.type, ''))) = 'ADJ'
                 AND (so.stockTakeId IS NOT NULL OR so.type = 'stockTake' OR si.stockTakeId IS NOT NULL) THEN 'TKE'
            ELSE COALESCE(sl.type, '')
        END                                                  AS trnRefNoRaw,                                               

        sl.itemId                                            AS itemId,
        sl.itemCode                                          AS itemCode,

        COALESCE(sl.inQty, 0)                                AS inQty,
        COALESCE(sl.outQty, 0)                               AS outQty,
        (COALESCE(sl.inQty, 0) - COALESCE(sl.outQty, 0))      AS delta,

        it.type                                              AS stockSubCategory,
        it.code                                              AS itemNo,
        it.name                                              AS itemName,
        uc.udfudesc                                          AS unitOfMeasure,

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

                COALESCE(TRIM(do.code), TRIM(jo_po.code), TRIM(jo.code), TRIM(po.code), '') AS orderRefNo,
        COALESCE(TRIM(jo.code), '')                          AS jobOrderNo,

        ''                                                   AS openingBalance,
        ''                                                   AS cumStockIn,
        ''                                                   AS cumStockOut,
        ''                                                   AS currentBalance,
        ''                                                   AS lastInDate,
        ''                                                   AS lastOutDate,
        ''                                                   AS reOrderLevel,
        ''                                                   AS reOrderQty
    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,
                il.lotNo                        AS lotNo,
                il.expiryDate                   AS expiryDate,
                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, il.lotNo, il.expiryDate
        ) lot
            ON lot.lotId = COALESCE(il_in.id, il_out.id)

        LEFT JOIN items it
            ON sl.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

        -- 這兩個 alias 是為了配合你上面 storeLocationSql 的 wh_in / wh_out
        LEFT JOIN inventory_lot_line ill_in
            ON il_in.id = ill_in.inventoryLotId
           AND ill_in.deleted = 0
        LEFT JOIN warehouse wh_in
            ON ill_in.warehouseId = wh_in.id
           AND wh_in.deleted = 0
        LEFT JOIN warehouse wh_out
            ON ill_out.warehouseId = wh_out.id
           AND wh_out.deleted = 0
        LEFT JOIN stock_out so
            ON sol.stockOutId = so.id
           AND so.deleted = 0
        LEFT JOIN pick_order_line pol
            ON sol.pickOrderLineId = pol.id
           AND pol.deleted = 0
        LEFT JOIN pick_order po_out
            ON pol.poId = po_out.id
           AND po_out.deleted = 0
          LEFT JOIN job_order jo_po
            ON po_out.joId = jo_po.id
           AND jo_po.deleted = 0
        LEFT JOIN delivery_order do
            ON po_out.doId = do.id
           AND do.deleted = 0
        LEFT JOIN stock_in si
            ON sil.stockInId = si.id
           AND si.deleted = 0
        LEFT JOIN job_order jo
            ON sil.jobOrderId = jo.id
           AND jo.deleted = 0
        LEFT JOIN purchase_order po
            ON sil.purchaseOrderId = po.id
           AND po.deleted = 0
    WHERE
        sl.deleted = 0
        AND sl.itemCode IS NOT NULL
        AND sl.itemCode <> ''
        AND DATE(sl.date) <= :reportPeriodEnd
        $stockCategorySql
        $itemCodeSql
        $storeLocationSql
        AND lot.lotId IS NOT NULL
),
opening AS (
    SELECT
        itemCode,
        COALESCE(SUM(delta), 0) AS openingBeforeStart
    FROM base
    WHERE trnDateRaw < :reportPeriodStart
    GROUP BY itemCode
),
period AS (
    SELECT
        b.*,
        COALESCE(o.openingBeforeStart, 0) AS openingBeforeStart
    FROM base b
    LEFT JOIN opening o
        ON o.itemCode = b.itemCode
    WHERE b.trnDateRaw BETWEEN :reportPeriodStart AND :reportPeriodEnd
)
SELECT
    stockSubCategory,
    itemNo,
    itemName,
    unitOfMeasure,
    lotNo,
    expiryDate,
     trnDate,
    CASE trnRefNoRaw
        WHEN 'OPEN' THEN '開倉'
        WHEN 'NOR' THEN '出入倉'
        WHEN 'TRF' THEN '轉倉'
        WHEN 'ADJ' THEN '調整'
        WHEN 'MISS' THEN '遺失'
        WHEN 'BAD' THEN '不良品'
        WHEN 'TKE' THEN '盤點'
        ELSE trnRefNoRaw
    END                                                      AS trnRefNo,
    storeLocation,
    orderRefNo,
    jobOrderNo,

    openingBalance,
    cumStockIn,
    cumStockOut,
    currentBalance,
    lastInDate,
    lastOutDate,
    reOrderLevel,
    reOrderQty,

    -- jrxml 需要 String；負數括號顯示，無小數
    CASE WHEN COALESCE(inQty, 0) < 0 THEN CONCAT('(', FORMAT(-inQty, 0), ')') ELSE FORMAT(COALESCE(inQty, 0), 0) END AS stockIn,
    CASE WHEN COALESCE(outQty, 0) < 0 THEN CONCAT('(', FORMAT(-outQty, 0), ')') ELSE FORMAT(COALESCE(outQty, 0), 0) END AS stockOut,

    -- 累計存量（跨 lot：只用 itemCode 分區）
    CASE WHEN (openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId)) < 0
        THEN CONCAT('(', FORMAT(-(openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId)), 0), ')')
        ELSE FORMAT(openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId), 0) END AS cumBalance,

    -- 累計期初存量 = 本行累計 - 本行異動
    CASE WHEN (openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId) - delta) < 0
        THEN CONCAT('(', FORMAT(-(openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId) - delta), 0), ')')
        ELSE FORMAT(openingBeforeStart + SUM(delta) OVER (PARTITION BY itemCode ORDER BY trnDateRaw, slId) - delta, 0) END AS cumOpeningBal,

    -- footer totals（同樣輸出 String）
    CASE WHEN COALESCE(openingBeforeStart, 0) < 0 THEN CONCAT('(', FORMAT(-openingBeforeStart, 0), ')') ELSE FORMAT(COALESCE(openingBeforeStart, 0), 0) END AS totalCumOpeningBal,
    CASE WHEN SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemCode) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemCode), 0), ')') ELSE FORMAT(SUM(COALESCE(inQty, 0)) OVER (PARTITION BY itemCode), 0) END AS totalStockIn,
    CASE WHEN SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemCode) < 0 THEN CONCAT('(', FORMAT(-SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemCode), 0), ')') ELSE FORMAT(SUM(COALESCE(outQty, 0)) OVER (PARTITION BY itemCode), 0) END AS totalStockOut,
    CASE WHEN (openingBeforeStart + SUM(inQty) OVER (PARTITION BY itemCode) - SUM(outQty) OVER (PARTITION BY itemCode)) < 0
        THEN CONCAT('(', FORMAT(-(openingBeforeStart + SUM(inQty) OVER (PARTITION BY itemCode) - SUM(outQty) OVER (PARTITION BY itemCode)), 0), ')')
        ELSE FORMAT(openingBeforeStart + SUM(inQty) OVER (PARTITION BY itemCode) - SUM(outQty) OVER (PARTITION BY itemCode), 0) END AS totalCumBalance

FROM period
ORDER BY
    itemNo,
    trnDateRaw,
    slId,
    lotNo
""".trimIndent()
    
        val result = jdbcDao.queryForList(sql, args)

        return result
    }

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