package com.ffii.fpsms.modules.master.service

import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.common.CodeGenerator
import com.ffii.fpsms.modules.master.entity.*
import com.ffii.fpsms.modules.master.web.models.ItemQc
import com.ffii.fpsms.modules.master.web.models.ItemWithQcResponse
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.master.web.models.NewItemRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.io.IOException
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalAdjusters
import kotlin.jvm.optionals.getOrNull
import com.ffii.core.utils.ExcelUtils
import org.apache.poi.ss.usermodel.Cell
import org.apache.poi.ss.usermodel.CellType
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.math.BigDecimal
import com.ffii.fpsms.modules.stock.entity.Inventory
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.entity.StockIn
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.entity.StockInRepository
import com.ffii.fpsms.modules.stock.enums.StockTakeLineStatus
import java.io.FileWriter
import java.io.PrintWriter
import com.ffii.fpsms.modules.stock.service.StockTakeService
import com.ffii.fpsms.modules.stock.web.model.SaveStockTakeRequest
import com.ffii.fpsms.modules.stock.enums.StockTakeStatus
import com.ffii.fpsms.modules.stock.service.StockInService
import com.ffii.fpsms.modules.stock.service.StockTakeLineService
import com.ffii.fpsms.modules.stock.web.model.SaveStockInRequest
import com.ffii.fpsms.modules.stock.web.model.SaveStockTakeLineRequest
import org.springframework.context.annotation.Lazy
import com.ffii.fpsms.modules.stock.service.StockInLineService
import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest
import com.ffii.fpsms.modules.stock.web.model.StockInLineStatus
import com.ffii.fpsms.modules.stock.web.model.SaveInventoryLotLineForSil
import com.ffii.fpsms.modules.bag.entity.Bag
import com.ffii.fpsms.modules.bag.entity.BagRepository

@Service
open class ItemsService(
    private val jdbcDao: JdbcDao,
    private val itemsRepository: ItemsRepository,
    private val qcCheckRepository: QcCheckRepository,
    private val qcItemsRepository: QcItemRepository, private val qcCategoryRepository: QcCategoryRepository,
    private val inventoryRepository: InventoryRepository,
    private val warehouseService: WarehouseService,
    private val stockTakeService: StockTakeService,
    @Lazy private val stockInService: StockInService,
    private val stockInRepository: StockInRepository,
    private val stockTakeLineService: StockTakeLineService,
    @Lazy private val itemUomService: ItemUomService,
    private val stockInLineService: StockInLineService,
    private val stockInLineRepository: StockInLineRepository,
    private val inventoryLotLineRepository: InventoryLotLineRepository,
    private val inventoryLotRepository: InventoryLotRepository,
    private val bagRepository: BagRepository,
    private val itemsQcCategoryMappingRepository: ItemsQcCategoryMappingRepository,
): AbstractBaseEntityService<Items, Long, ItemsRepository>(jdbcDao, itemsRepository) {

    private val excelImportPath: String = System.getProperty("user.home") + "/Downloads/StockTakeImport/"

    private fun assignStockInNo(): String {
        val prefix = "SI"
        val midfix = CodeGenerator.DEFAULT_MIDFIX
        val latestCode = stockInRepository.findLatestCodeByPrefix("${prefix}-${midfix}")
        return CodeGenerator.generateNo(prefix = prefix, latestCode = latestCode)
    }

    inner class PatchLogWriter(private val logFilePath: String) {
        private val logFile = File(logFilePath)
        private val logWriter = PrintWriter(FileWriter(logFile, true))
        private val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))

        init {
            writeHeader()
        }

        fun writeHeader() {
            logWriter.println("=".repeat(80))
            logWriter.println("PATCH ITEMS FROM EXCEL - LOG FILE")
            logWriter.println("Start Time: $timestamp")
            logWriter.println("=".repeat(80))
            logWriter.println()
        }

        fun writeFileInfo(lastRow: Int, startRow: Int) {
            logWriter.println("Excel File Information:")
            logWriter.println("  Last Row: $lastRow")
            logWriter.println("  Starting Row: ${startRow + 1} (0-indexed: $startRow)")
            logWriter.println()
            logWriter.println("-".repeat(80))
            logWriter.println()
        }

        fun writeRowError(rowNum: Int, message: String) {
            logWriter.println("Row ${rowNum + 1}: ERROR - $message")
        }

        fun writeRowSkipped(rowNum: Int, reason: String) {
            logWriter.println("Row ${rowNum + 1}: SKIPPED - $reason")
        }

        fun writeItemPatched(
            rowNum: Int,
            itemCode: String,
            itemId: Long?,
            itemName: String?,
            inventorySheet: String?,
            type: String?,
            storeId: String?,
            storeLocation: String?,
            warehouse: String?,
            area: String?,
            slot: String?,
            locationCode: String?,
            mtmsPickRoutingId: Int?
        ) {
            logWriter.println("Row ${rowNum + 1}: SUCCESS - Patched item '$itemCode' (ID: $itemId, Name: $itemName)")
            logWriter.println("  - inventorySheet: '$inventorySheet'")
            logWriter.println("  - type: '$type'")
            logWriter.println("  - store_id: '$storeId'")
            logWriter.println("  - storeLocation: '$storeLocation'")
            logWriter.println("  - warehouse: '$warehouse'")
            logWriter.println("  - area: '$area'")
            logWriter.println("  - slot: '$slot'")
            logWriter.println("  - LocationCode: '$locationCode'")
            logWriter.println("  - MTMSPickRoutingID: '$mtmsPickRoutingId'")
        }

        fun writeInventoryDeleted(rowNum: Int, inventoryId: Long?) {
            logWriter.println("  INVENTORY: Deleted existing inventory (ID: $inventoryId)")
        }

        fun writeInventoryDeleteError(rowNum: Int, message: String) {
            logWriter.println("  INVENTORY: WARNING - Failed to delete existing inventory - $message")
        }

        fun writeInventoryCreated(rowNum: Int, stockTakeCount: BigDecimal) {
            logWriter.println("  INVENTORY: CREATED - onHandQty: $stockTakeCount")
        }

        fun writeInventoryError(rowNum: Int, message: String) {
            logWriter.println("  INVENTORY: ERROR - $message")
        }

        fun writeInventoryIdNull(rowNum: Int) {
            logWriter.println("  INVENTORY: ERROR - Item ID is null after save")
        }

        fun writeEmptyLine() {
            logWriter.println()
        }

        fun writeSummary(
            successCount: Int,
            errorCount: Int,
            duplicateSkippedCount: Int,
            inventoryCreatedCount: Int,
            inventoryFailedCount: Int,
            missingInventoryItems: List<String>,
            errors: List<String>
        ) {
            logWriter.println()
            logWriter.println("=".repeat(80))
            logWriter.println("SUMMARY")
            logWriter.println("=".repeat(80))
            logWriter.println(
                "End Time: ${
                    LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
                }"
            )
            logWriter.println()
            logWriter.println("Items Patched: $successCount")
            logWriter.println("Items with Errors: $errorCount")
            logWriter.println("Duplicate Items Skipped: $duplicateSkippedCount")
            logWriter.println()
            logWriter.println("Inventory Records Created: $inventoryCreatedCount")
            logWriter.println("Inventory Records Failed: $inventoryFailedCount")
            logWriter.println()

            if (missingInventoryItems.isNotEmpty()) {
                logWriter.println("Items Missing Inventory Records (${missingInventoryItems.size}):")
                missingInventoryItems.forEach { logWriter.println("  - $it") }
                logWriter.println()
            }

            if (errors.isNotEmpty()) {
                logWriter.println("Errors Encountered (${errors.size}):")
                errors.forEach { logWriter.println("  - $it") }
            }

            logWriter.println()
            logWriter.println("=".repeat(80))
            logWriter.println("END OF LOG")
            logWriter.println("=".repeat(80))
        }

        fun close() {
            logWriter.close()
        }

        fun getLogFilePath(): String {
            return logFilePath
        }
    }

    private fun createPatchLogWriter(): PatchLogWriter {
        val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
        val logFileName = "${excelImportPath}patch_log_$timestamp.txt"
        return PatchLogWriter(logFileName)
    }


    // do mapping with projection
    open fun allItems(): List<Items> {
        // TODO: Replace by actual logic
        val items = itemsRepository.findAll()
        return items
    }

    open fun allConsumables(): List<Map<String, Any>> {
        val args = mapOf(
            "type" to "mat"
        )
        val sql = StringBuilder(
            "select"
                    + " i.id, "
                    + " concat(i.code , ' - ' , i.name) as label, "
                    + " uc.id as uomId, "
                    + " uc.code as uom, "
                    + " uc.udfudesc as uomDesc, "
                    + " COALESCE(inv.availableQty, 0) as currentStockBalance "
                    + " from items i "
                    + " left join item_uom iu on iu.itemId = i.id and iu.deleted = false and iu.salesUnit = true "
                    + " left join uom_conversion uc on uc.id = iu.uomId "
                    + " left join ("
                    + "   SELECT "
                    + "     il.itemId, "
                    + "     SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty "
                    + "   FROM inventory_lot_line ill "
                    + "   JOIN inventory_lot il ON il.id = ill.inventoryLotId "
                    + "   LEFT JOIN item_uom iu_ratio ON iu_ratio.itemId = il.itemId AND iu_ratio.deleted = false AND iu_ratio.salesUnit = true "
                    + "   WHERE ill.status = 'available' "
                    + "   AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 "
                    + "   AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE()) "
                    + "   GROUP BY il.itemId "
                    + " ) inv ON inv.itemId = i.id "
                    + " where i.deleted = false "
                    + " and i.type = :type "
        )

        return jdbcDao.queryForList(sql.toString(), args);
    }

    /** FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22 */
    open fun getPickOrderItemsByPage(args: Map<String, Any>): List<Map<String, Any>> {
        try {
            println("=== Debug: getPickOrderItemsByPage in ItemsService ===")
            //println("Args: $args")

            val sql = StringBuilder(
                """
                SELECT 
                    CONCAT(po.id, '-', pol.id) as id,
                    po.id as pickOrderId,
                    po.code as pickOrderCode,
                    pol.id as pickOrderLineId,
                    pol.itemId,
                    pol.uomId,
                    i.code as itemCode,
                    i.name as itemName,
                    pol.qty as requiredQty,
                    COALESCE(inv.availableQty, 0) as currentStock,
                    COALESCE(uc.udfudesc, 'N/A') as unit,
                    DATE_FORMAT(po.targetDate, '%Y-%m-%d') as targetDate,
                    po.status,
                    po.consoCode,
                    po.assignTo,
                    COALESCE(pog.name, 'No Group') as groupName
                FROM pick_order po
                JOIN pick_order_line pol ON pol.poId = po.id
                JOIN items i ON i.id = pol.itemId
                LEFT JOIN uom_conversion uc ON uc.id = pol.uomId
                LEFT JOIN pick_order_group pog ON pog.pick_order_id = po.id AND pog.deleted = false
                LEFT JOIN (
                    SELECT 
                        il.itemId,
                        iu.uomId as uomId,
                        SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty
                    FROM inventory_lot_line ill
                    JOIN inventory_lot il ON il.id = ill.inventoryLotId
                    JOIN item_uom iu ON iu.id = ill.stockItemUomId AND iu.deleted = false
                    WHERE ill.status = 'available'
                    AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0
                    AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE())
                    GROUP BY il.itemId, iu.uomId
                ) inv ON inv.itemId = pol.itemId AND inv.uomId = pol.uomId
                WHERE po.deleted = false
                AND po.consoCode IS NULL
                """
            )

            if (args.containsKey("itemCode")) {
                sql.append(" AND i.code LIKE :itemCode ")
            }

            if (args.containsKey("itemName")) {
                sql.append(" AND i.name LIKE :itemName ")
            }

            if (args.containsKey("status")) {
                sql.append(" AND po.status = :status ")
            }

            if (args.containsKey("targetDateFrom")) {
                sql.append(" AND po.targetDate >= :targetDateFrom ")
            }

            if (args.containsKey("targetDateTo")) {
                sql.append(" AND po.targetDate <= :targetDateTo ")
            }

            sql.append(" ORDER BY po.targetDate DESC, i.name ASC ")

            val finalSql = sql.toString()
           // println("Final SQL: $finalSql")
            //println("SQL args: $args")

            val result = jdbcDao.queryForList(finalSql, args)
            //println("Query result size: ${result.size}")
           // result.forEach { row -> println("Result row: $row") }
            return result
            
        } catch (e: Exception) {
            println("Error in getPickOrderItemsByPage: ${e.message}")
            e.printStackTrace()
            throw e
        }
    }

    open fun getRoughScheduleList(): List<Map<String, Any>> {
        val now = LocalDateTime.now()
        val lastMonthStart =
            now.minusMonths(1).withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0) // Start of last month
        val lastMonthEnd =
            now.minusDays(now.dayOfMonth.toLong()).withHour(23).withMinute(59).withSecond(59) // End of last month

        val curMonthStart = now.withDayOfMonth(1) // Start of last month
        val curMonthEnd =
            now.with(TemporalAdjusters.lastDayOfMonth()).withHour(23).withMinute(59).withSecond(59) // End of last month

        // Format dates if needed
        val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

        val args = mapOf(
            "lastMonthStart" to lastMonthStart.format(formatter),
            "lastMonthEnd" to lastMonthEnd.format(formatter)
        )

        println("lastMonthStart:" + args.get("lastMonthStart"))
        println("lastMonthEnd:" + args.get("lastMonthEnd"))

        val sql = StringBuilder(
            " SELECT "
                    + " dol.itemId, "
                    + " i.name AS itemName, "
                    + " dol.itemNo, "
                    + " 0 AS openBalance, "
                    + " COUNT(DISTINCT dol.deliveryOrderId) AS orderCount, "
                    + " SUM(IFNULL(dol.qty, 0)) AS totalQty, "
                    + " SUM(IFNULL(dol.qty, 0)) / COUNT(DISTINCT dol.deliveryOrderId) AS lastMonthAvgSalesCount, "
                    + " 22000 - IFNULL(i.maxQty , 0) AS fgProductionLimit "
                    + " FROM delivery_order do "
                    + " LEFT JOIN delivery_order_line dol ON dol.deliveryOrderId = do.id "
                    + " LEFT JOIN items i ON i.id = dol.itemId "
                    + " WHERE do.deleted = false "
                    + " AND do.estimatedArrivalDate >= :lastMonthStart "
                    + " AND do.estimatedArrivalDate <= :lastMonthEnd "
                    + " AND dol.itemId is not null "
                    + " GROUP BY dol.itemId, dol.itemNo, i.name "
        );
        return jdbcDao.queryForList(sql.toString(), args);
    }

    open fun getItemsByPage(args: Map<String, Any>): List<Map<String, Any>> {
        // Extract parameters from the DTO
        val sql = StringBuilder(
            "SELECT " +
                    "i.id, " +
                    "i.code, " +
                    "i.name, " +
                    "i.description, " +
                    "i.type, " +
                    "i.`LocationCode` as LocationCode, " +
                    "i.`qcCategoryId` as qcCategoryId, " +
                    "i.AverageUnitPrice as averageUnitPrice, " +
                    "i.LatestMarketUnitPrice as latestMarketUnitPrice, " +
                    "DATE_FORMAT(i.latestMupUpdatedDate, '%Y-%m-%d %H:%i') as latestMupUpdatedDate, " +
                    "uc_p.udfudesc as purchaseUnit " +
                    "FROM items i " +
                    "LEFT JOIN item_uom iu_p ON iu_p.itemId = i.id AND iu_p.deleted = false AND iu_p.purchaseUnit = true " +
                    "LEFT JOIN uom_conversion uc_p ON uc_p.id = iu_p.uomId " +
                    "WHERE i.deleted = false"
        );

        if (args.containsKey("name")) {
            sql.append(" AND i.name like :name ");
        }

        if (args.containsKey("code")) {
            sql.append(" AND i.code like :code ");
        }

        return jdbcDao.queryForList(sql.toString(), args);
    }

    /**
     * Incremental update of AverageUnitPrice for the given items only.
     * Recomputes weighted average (SUM(up*qty)/SUM(qty)) from purchase_order_line
     * where po.orderDate >= '2026-01-01', then updates items.AverageUnitPrice.
     * Items with no qualifying PO lines get AverageUnitPrice = null.
     */
    @Transactional
    open fun updateAverageUnitPriceForItems(itemIds: Set<Long>) {
        if (itemIds.isEmpty()) return
        val idList = itemIds.toList()
        val args = mapOf("itemIds" to idList)
        val avgSql = """
            SELECT pol.itemId AS itemId,
                ROUND(CAST(SUM(pol.up * pol.qty) / NULLIF(SUM(pol.qty), 0) AS DECIMAL(14,2)), 2) AS avgUp
            FROM purchase_order_line pol
            JOIN purchase_order po ON po.id = pol.purchaseOrderId AND po.deleted = false
            WHERE pol.itemId IN (:itemIds) AND pol.deleted = false
                AND po.orderDate >= '2026-01-01'
                AND pol.up IS NOT NULL AND pol.qty > 0
            GROUP BY pol.itemId
        """.trimIndent()
        val rows = jdbcDao.queryForList(avgSql, args)
        val avgByItem = rows.mapNotNull { row ->
            val id = (row["itemId"] as? Number)?.toLong() ?: return@mapNotNull null
            val avg = row["avgUp"] as? BigDecimal ?: return@mapNotNull null
            id to avg.toPlainString()
        }.toMap()
        // Use JDBC UPDATE instead of load+save: avoids ObjectOptimisticLockingFailureException when the same
        // Items row was already updated earlier in this transaction (e.g. M18 PO sync) or concurrently.
        idList.forEach { itemId ->
            val avg = avgByItem[itemId]
            jdbcDao.executeUpdate(
                """
                UPDATE items
                SET AverageUnitPrice = :avg,
                    modified = NOW(),
                    version = version + 1
                WHERE id = :id AND deleted = 0
                """.trimIndent(),
                mapOf("avg" to avg, "id" to itemId),
            )
        }
    }

    /** Column headers in Chinese for market unit price template/import */
    private val MARKET_UNIT_PRICE_HEADERS_ZH = listOf("項目編號", "項目名稱", "採購單位", "最新市場價格")

    /** Accepted column 3 header (price column); 最新市場單位價格 allowed for backward compatibility. */
    private val ACCEPTED_COL3_ALIASES = setOf("最新市場價格", "最新市場單位價格", "Latest Market Unit Price", "Latest Market Price", "Market Unit Price")

    /**
     * Generates Excel template for latest market unit price upload.
     * Columns: 項目編號, 項目名稱, 採購單位, 最新市場價格 (empty).
     */
    open fun generateMarketUnitPriceTemplate(): ByteArray {
        val data = getItemsByPage(emptyMap())
        val workbook = XSSFWorkbook()
        val sheet = workbook.createSheet("Market Unit Price")
        val headerRow = sheet.createRow(0)
        MARKET_UNIT_PRICE_HEADERS_ZH.forEachIndexed { index, header ->
            headerRow.createCell(index).setCellValue(header)
        }
        data.forEachIndexed { index, row ->
            val r = sheet.createRow(index + 1)
            r.createCell(0).setCellValue((row["code"]?.toString() ?: "").trim())
            r.createCell(1).setCellValue((row["name"]?.toString() ?: "").trim())
            r.createCell(2).setCellValue((row["purchaseUnit"]?.toString() ?: "").trim())
            r.createCell(3).setCellValue("") // empty for user to fill
        }
        val baos = ByteArrayOutputStream()
        workbook.use { it.write(baos) }
        return baos.toByteArray()
    }

    /**
     * Imports latest market unit price from Excel. Validates header row matches expected format.
     * @return map with "success", "updated" count, "errors" list
     */
    @Transactional
    open fun importMarketUnitPriceExcel(workbook: Workbook?): Map<String, Any> {
        val errors = mutableListOf<String>()
        if (workbook == null) {
            return mapOf("success" to false, "updated" to 0, "errors" to listOf("No file or invalid Excel file."))
        }
        val sheet = workbook.getSheetAt(0) ?: return mapOf("success" to false, "updated" to 0, "errors" to listOf("Sheet not found."))
        val headerRow = sheet.getRow(0) ?: return mapOf("success" to false, "updated" to 0, "errors" to listOf("Header row not found."))
        // Validate header format: exact 4 columns 項目編號, 項目名稱, 採購單位, 最新市場價格
        val actualHeaders = (0..3).map { ExcelUtils.getStringValue(headerRow.getCell(it)).trim() }
        val expectedCol0 = listOf("項目編號", "Item Code")
        val expectedCol1 = listOf("項目名稱", "Item Name")
        val expectedCol2 = listOf("採購單位", "Purchase Unit")
        val validFormat = (actualHeaders[0] in expectedCol0)
                && (actualHeaders[1] in expectedCol1)
                && (actualHeaders[2] in expectedCol2)
                && (actualHeaders[3] in ACCEPTED_COL3_ALIASES)
        if (!validFormat) {
            errors.add("Invalid format: first row must be exactly 項目編號, 項目名稱, 採購單位, 最新市場價格 (or English: Item Code, Item Name, Purchase Unit, Latest Market Price). Got: ${actualHeaders.joinToString(", ")}")
            return mapOf("success" to false, "updated" to 0, "errors" to errors)
        }
        if (sheet.lastRowNum < 1) {
            errors.add("No data rows: file must contain at least one data row after the header.")
            return mapOf("success" to false, "updated" to 0, "errors" to errors)
        }
        var updated = 0
        val now = LocalDateTime.now()
        for (i in 1..sheet.lastRowNum) {
            val row = sheet.getRow(i) ?: continue
            val codeCell = row.getCell(0)
            if (codeCell == null) {
                errors.add("Row ${i + 1}: missing 項目編號 (column A).")
                continue
            }
            val code = ExcelUtils.getStringValue(codeCell).trim()
            val priceCell = row.getCell(3)
            val priceStr = if (priceCell != null) ExcelUtils.getStringValue(priceCell).trim() else ""
            if (code.isEmpty() && priceStr.isEmpty()) continue
            if (code.isEmpty()) {
                errors.add("Row ${i + 1}: 項目編號 (column A) is required when 最新市場價格 is filled.")
                continue
            }
            // Empty 最新市場價格 is okay: skip row and do not update
            if (priceStr.isEmpty()) continue
            val price = try {
                if (priceCell != null && priceCell.cellType == CellType.NUMERIC) ExcelUtils.getDoubleValue(priceCell) else priceStr.toDouble()
            } catch (_: NumberFormatException) {
                errors.add("Row ${i + 1}: invalid price '$priceStr' for item $code")
                continue
            }
            if (price < 0) {
                errors.add("Row ${i + 1}: price must be non-negative for item $code")
                continue
            }
            val item = findByCode(code)
            if (item == null) {
                errors.add("Row ${i + 1}: item not found for code '$code'")
                continue
            }
            item.latestMarketUnitPrice = price
            item.latestMupUpdatedDate = now
            itemsRepository.saveAndFlush(item)
            updated++
        }
        return mapOf("success" to true, "updated" to updated, "errors" to errors)
    }

    open fun findById(id: Long): Items? {
        return itemsRepository.findByIdAndDeletedFalse(id);
    }

    open fun findByCode(code: String): Items? {
        return itemsRepository.findByCodeAndDeletedFalse(code);
    }

    open fun findByM18Id(m18Id: Long): Items? {
        return itemsRepository.findByM18IdAndDeletedIsFalse(m18Id)
    }

    open fun findByNameLikeAndM18UomId(name: String, m18UomId: Long): Items? {
        return itemsRepository.findByNameLikeAndUomM18Id(name, m18UomId);
    }

    open fun findByNameExactlyAndM18UomId(name: String, m18UomId: Long): Items? {
        return itemsRepository.findByNameExactlyAndUomM18Id(name, m18UomId);
    }

    open fun findByNameAndM18UomId(name: String, m18UomId: Long): Items? {
        return findByNameExactlyAndM18UomId(name, m18UomId) ?: findByNameLikeAndM18UomId(name, m18UomId);
    }

    open fun findByM18BomCode(m18BomCode: String): Items? {
        return itemsRepository.findByM18BomCode(m18BomCode);
    }

    open fun markDeleted(id: Long): Items {
        val item = itemsRepository.findById(id).orElseThrow().apply {
            deleted = true
        }
        return itemsRepository.save(item)
    }

    // QcCheck included item
    open fun getItem(id: Long): ItemWithQcResponse {
        val list = listOf(1, 2)
        val item = itemsRepository.findByIdAndDeletedFalse(id) ?: Items()
        val qcItems = qcItemsRepository.findAllByDeletedIsFalse()
        val qcCheckMap = qcCheckRepository.findAllByItemIdAndDeletedFalse(id).associateBy { it.id }
        val qc = qcItems.map {
            val check = qcCheckMap[it.id] // Find the corresponding qcCheck
            println(check?.description)
            println(check?.lowerLimit)
            ItemQc(
                id = it.id!!,
                name = it.name!!,
                code = it.code!!,
                description = it.description!!,
                instruction = check?.description,
                lowerLimit = check?.lowerLimit,
                upperLimit = check?.upperLimit,
                isActive = check != null
            ) // Create combined item
        }
        // Fetch qcType from items_qc_category_mapping table
        val qcCategoryMapping = itemsQcCategoryMappingRepository.findFirstByItemId(id)
        val qcType = qcCategoryMapping?.type
        
        val response = ItemWithQcResponse(
            item = item,
            qcChecks = qc,
            qcType = qcType
        )
        // TODO: Return with QC items
        return response
    }

    //    open fun getItem(id: Long): Items? {
//        // TODO: Return with QC items
//        return itemsRepository.findById(id).get()
//    }
    @Throws(IOException::class)
    @Transactional
    open fun saveItem(request: NewItemRequest): MessageResponse {
        val duplicatedItem = itemsRepository.findByCodeAndTypeAndDeletedFalse(request.code, request.type)
        if (duplicatedItem != null && duplicatedItem.id != request.id) {
            if (request.m18Id != null && request.id == null && duplicatedItem.m18Id == null) {
                duplicatedItem.m18Id = request.m18Id
                duplicatedItem.m18LastModifyDate = request.m18LastModifyDate
                val linked = itemsRepository.saveAndFlush(duplicatedItem)
                return MessageResponse(
                    id = linked.id,
                    code = linked.code,
                    name = linked.name,
                    type = linked.type.toString(),
                    message = "Linked m18Id to existing item with same code",
                    errorPosition = null,
                )
            }
            return MessageResponse(
                id = request.id ?: duplicatedItem.id,
                code = request.code,
                name = request.name,
                type = request.type.toString(),
                message = "The item code has already existed",
                errorPosition = "code"
            )
        }
        val item = if (request.m18Id != null) findByM18Id(request.m18Id) ?: Items()
        else if (request.id != null && request.id > 0) itemsRepository.findByIdAndDeletedFalse(request.id) ?: Items()
        else Items()
        logger.info("item: $item")
        if (item.m18LastModifyDate == request.m18LastModifyDate) {
            return MessageResponse(
                id = item.id,
                code = item.code,
                name = item.name,
                type = item.type.toString(),
                message = "M18 Item does not have any updates",
                errorPosition = "m18LastModifyDate"
            )
        }

        val qcCategory = request.qcCategoryId?.let { qcCategoryRepository.findById(it).getOrNull() }

        item.apply {
            code = request.code
            name = request.name
            description = request.description
            remarks = request.remarks
            shelfLife = request.shelfLife
            countryOfOrigin = request.countryOfOrigin
            maxQty = request.maxQty
            this.type = request.type
            this.qcCategory = qcCategory
            m18Id = request.m18Id ?: this.m18Id
            m18LastModifyDate = request.m18LastModifyDate ?: this.m18LastModifyDate
            request.isEgg?.let        { isEgg = it }
            request.isFee?.let        { isFee = it }
            request.isBag?.let        { isBag = it }
            request.store_id?.let     { store_id = it }
            request.warehouse?.let    { warehouse = it }
            request.area?.let         { area = it }
            request.slot?.let         { slot = it }
            request.LocationCode?.let { LocationCode = it }
        }
        logger.info("saving item: $item")
        val savedItem = itemsRepository.saveAndFlush(item)
        logger.info("save success")
        
        // Create Bag record if isBag is true
        if (savedItem.isBag == true && savedItem.id != null) {
            val existingBag = bagRepository.findByItemIdAndDeletedIsFalse(savedItem.id!!)
            if (existingBag == null) {
                val bag = Bag().apply {
                    itemId = savedItem.id
                    itemCode = savedItem.code
                    itemName = savedItem.name
                    takenBagBalance = 0
                }
                bagRepository.save(bag)
                logger.info("Created bag record for item: ${savedItem.code} (ID: ${savedItem.id})")
            } else {
                logger.info("Bag record already exists for item: ${savedItem.code} (ID: ${savedItem.id})")
            }
        }
        
        // Save QC category mapping if qcType and qcCategoryId are provided
        if (request.qcType != null && request.qcCategoryId != null && savedItem.id != null) {
            // Find existing mapping by itemId only (update existing record for this item)
            val existingMapping = itemsQcCategoryMappingRepository.findFirstByItemId(savedItem.id!!)
            val mapping = existingMapping ?: ItemsQcCategoryMapping()
            mapping.apply {
                itemId = savedItem.id
                qcCategoryId = request.qcCategoryId
                type = request.qcType
            }
            itemsQcCategoryMappingRepository.save(mapping)
            logger.info("Saved QC category mapping for item: ${savedItem.code} (ID: ${savedItem.id}), type: ${request.qcType}")
        }
        
        return MessageResponse(
            id = savedItem.id,
            name = savedItem.name,
            code = savedItem.code,
            type = savedItem.type.toString(),
            message = "Item Save Success",
            errorPosition = null,
        )
    }

    /**
     * FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22
     * Pick-order create search: one row per item + available lot UOM
     * (available qty summed per UOM). Items with no available stock are omitted.
     */
    open fun getItemsWithDetailsByPage(args: Map<String, Any>): List<Map<String, Any>> {
        val sql = StringBuilder(
            """
            SELECT
              i.id,
              i.code,
              i.name,
              i.type,
              i.description,
              uc.id AS uomId,
              uc.code AS uom,
              uc.udfudesc AS uomDesc,
              SUM(
                COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)
              ) AS currentStockBalance
            FROM items i
            JOIN inventory_lot il
              ON il.itemId = i.id
             AND il.deleted = false
            JOIN inventory_lot_line ill
              ON ill.inventoryLotId = il.id
             AND ill.deleted = false
            JOIN item_uom iu
              ON iu.id = ill.stockItemUomId
             AND iu.deleted = false
            JOIN uom_conversion uc
              ON uc.id = iu.uomId
            WHERE i.deleted = false
              AND ill.status = 'available'
              AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0
              AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE())
            """.trimIndent()
        )

        if (args.containsKey("name")) {
            sql.append(" AND i.name LIKE :name ")
        }
        if (args.containsKey("code")) {
            sql.append(" AND i.code LIKE :code ")
        }
        if (args.containsKey("description")) {
            sql.append(" AND i.description LIKE :description ")
        }
        if (args.containsKey("type")) {
            sql.append(" AND i.type = :type ")
        }

        sql.append(
            """
             GROUP BY i.id, i.code, i.name, i.type, i.description, uc.id, uc.code, uc.udfudesc
             ORDER BY i.name ASC, uc.udfudesc ASC
            """.trimIndent()
        )

        return jdbcDao.queryForList(sql.toString(), args)
    }

    open fun getItemOrderOrderedItems(search: String?, type: String? = null): List<Map<String, Any>> {
        val sql = StringBuilder(
            """
            SELECT
                i.id,
                i.code,
                i.name,
                i.item_Order as itemOrder,
                i.store_id as storeId,
                i.warehouse,
                i.area,
                i.slot,
                i.LocationCode as locationCode
            FROM items i
            WHERE i.deleted = false
              AND i.item_Order IS NOT NULL
            """.trimIndent()
        )
        val args = mutableMapOf<String, Any>()
        val typeTrim = type?.trim()
        if (!typeTrim.isNullOrEmpty()) {
            sql.append(" AND i.type = :type ")
            args["type"] = typeTrim
        }
        val s = search?.trim()
        if (!s.isNullOrEmpty()) {
            sql.append(" AND (i.code LIKE :search OR i.name LIKE :search) ")
            args["search"] = "%$s%"
        }
        sql.append(" ORDER BY i.item_Order ASC, i.code ASC ")
        return jdbcDao.queryForList(sql.toString(), args)
    }

    open fun getItemOrderUnorderedItems(search: String?, type: String? = null): List<Map<String, Any>> {
        // To keep UI responsive, do not allow fetching the whole "unordered items" list without search.
        // Use the dedicated search API with LIMIT instead.
        val sql = StringBuilder(
            """
            SELECT
                i.id,
                i.code,
                i.name,
                i.item_Order as itemOrder,
                i.store_id as storeId,
                i.warehouse,
                i.area,
                i.slot,
                i.LocationCode as locationCode
            FROM items i
            WHERE i.deleted = false
              AND i.item_Order IS NULL
            """.trimIndent()
        )
        val args = mutableMapOf<String, Any>()
        val typeTrim = type?.trim()
        if (!typeTrim.isNullOrEmpty()) {
            sql.append(" AND i.type = :type ")
            args["type"] = typeTrim
        }
        val s = search?.trim()
        if (s.isNullOrEmpty()) {
            return emptyList()
        }
        sql.append(" AND (i.code LIKE :search OR i.name LIKE :search) ")
        args["search"] = "%$s%"
        sql.append(" ORDER BY i.code ASC ")
        return jdbcDao.queryForList(sql.toString(), args)
    }

    open fun searchItemOrderUnorderedItems(search: String, type: String? = null, limit: Int = 200): List<Map<String, Any>> {
        val s = search.trim()
        if (s.isEmpty()) return emptyList()
        val safeLimit = limit.coerceIn(1, 500)
        val sql = StringBuilder(
            """
            SELECT
                i.id,
                i.code,
                i.name,
                i.item_Order as itemOrder,
                i.store_id as storeId,
                i.warehouse,
                i.area,
                i.slot,
                i.LocationCode as locationCode
            FROM items i
            WHERE i.deleted = false
              AND i.item_Order IS NULL
              AND (i.code LIKE :search OR i.name LIKE :search)
            """.trimIndent()
        )
        val args = mutableMapOf<String, Any>(
            "search" to "%$s%",
            "limit" to safeLimit,
        )
        val typeTrim = type?.trim()
        if (!typeTrim.isNullOrEmpty()) {
            sql.append(" AND i.type = :type ")
            args["type"] = typeTrim
        }
        sql.append(" ORDER BY i.code ASC LIMIT :limit ")
        return jdbcDao.queryForList(sql.toString(), args)
    }

    @Transactional
    open fun saveItemOrder(orderedItemIds: List<Long>): MessageResponse {
        if (orderedItemIds.isEmpty()) {
            return MessageResponse(
                id = null,
                name = "Items",
                code = "items",
                type = "itemOrder",
                message = "No items provided",
                errorPosition = "orderedItemIds",
            )
        }

        val distinctIds = orderedItemIds.distinct()
        distinctIds.forEachIndexed { idx, itemId ->
            jdbcDao.executeUpdate(
                """
                UPDATE items
                SET item_Order = :itemOrder,
                    modified = NOW(),
                    version = version + 1
                WHERE id = :id AND deleted = false
                """.trimIndent(),
                mapOf("itemOrder" to (idx + 1), "id" to itemId),
            )
        }

        return MessageResponse(
            id = null,
            name = "Items",
            code = "items",
            type = "itemOrder",
            message = "Item order saved",
            errorPosition = null,
        )
    }

    open fun getItemsIdWithSameCategoryForQc(args: Map<String, Any>): List<Int> {
        val sql = StringBuilder(
            "SELECT"
                    + " i.id "
                    + " FROM items i "
                    + " INNER JOIN items_qc_category_mapping iqcm ON iqcm.itemId = i.id AND iqcm.type = :qcType "
                    + " LEFT JOIN qc_category qcc ON qcc.id = iqcm.qcCategoryId "
                    + " WHERE i.deleted = false AND qcc.deleted = false"
                    + " AND LEFT(i.code, 2) = (SELECT LEFT(code, 2) FROM items WHERE id = :itemId)"
                    + " AND i.id != :itemId "
        )
        return jdbcDao.queryForInts(sql.toString(), args);
    }

    // Get String Value
    private fun getCellStringValue(cell: Cell?): String? {
        return cell?.let {
            when (it.cellType) {
                CellType.STRING -> it.stringCellValue.trim()
                CellType.NUMERIC -> it.numericCellValue.toString().trim()
                CellType.BLANK -> null
                else -> null
            }
        }?.takeIf { it.isNotBlank() }
    }

    /**
     * FP-MTMS Version Checklist | Functions Ref. No. 35 | v1.0.1 | 2026-07-22
     * Pick-order create: stock UOM (default) + distinct UOMs from currently available lots.
     * Lot UOM = inventory_lot_line.stockItemUom → item_uom.uom (uom_conversion id).
     */
    open fun getAvailablePickUoms(itemId: Long): Map<String, Any?> {
        val args = mapOf("itemId" to itemId)

        val stockRows = jdbcDao.queryForList(
            """
            SELECT
              uc.id AS uomId,
              uc.code AS uomCode,
              uc.udfudesc AS uomDesc
            FROM item_uom iu
            JOIN uom_conversion uc ON uc.id = iu.uomId
            WHERE iu.itemId = :itemId
              AND iu.deleted = false
              AND iu.stockUnit = true
            LIMIT 1
            """.trimIndent(),
            args,
        )
        val stockUom = stockRows.firstOrNull()?.let { row ->
            mapOf(
                "uomId" to row["uomId"],
                "uomCode" to row["uomCode"],
                "uomDesc" to row["uomDesc"],
            )
        }

        val lotRows = jdbcDao.queryForList(
            """
            SELECT DISTINCT
              uc.id AS uomId,
              uc.code AS uomCode,
              uc.udfudesc AS uomDesc
            FROM inventory_lot_line ill
            JOIN inventory_lot il ON il.id = ill.inventoryLotId
            JOIN item_uom iu ON iu.id = ill.stockItemUomId AND iu.deleted = false
            JOIN uom_conversion uc ON uc.id = iu.uomId
            WHERE il.itemId = :itemId
              AND ill.deleted = false
              AND ill.status = 'available'
              AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) > 0
              AND (il.expiryDate IS NULL OR il.expiryDate >= CURDATE())
            ORDER BY uc.udfudesc ASC
            """.trimIndent(),
            args,
        )

        val byId = linkedMapOf<Long, Map<String, Any?>>()
        fun putUom(row: Map<String, Any?>?) {
            if (row == null) return
            val id = (row["uomId"] as? Number)?.toLong() ?: return
            if (id <= 0L) return
            if (!byId.containsKey(id)) {
                byId[id] = mapOf(
                    "uomId" to id,
                    "uomCode" to row["uomCode"],
                    "uomDesc" to row["uomDesc"],
                )
            }
        }
        putUom(stockUom)
        lotRows.forEach { putUom(it) }

        return mapOf(
            "itemId" to itemId,
            "stockUom" to stockUom,
            "options" to byId.values.toList(),
        )
    }

}