package com.ffii.fpsms.modules.master.service
import com.ffii.core.exception.NotFoundException
import com.ffii.fpsms.modules.master.entity.*
import com.ffii.fpsms.modules.master.web.models.*
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 org.springframework.core.io.ClassPathResource
import org.springframework.core.io.Resource
import org.springframework.core.io.support.PathMatchingResourcePatternResolver
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import java.io.ByteArrayOutputStream
import java.time.LocalDateTime
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.io.FileInputStream
import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository
import com.ffii.fpsms.modules.settings.entity.BomWeightingScoreRepository
import java.math.BigDecimal
import java.math.RoundingMode
import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest
import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemResponse
import com.ffii.fpsms.modules.master.web.models.ItemType
import org.springframework.beans.factory.annotation.Value
import org.springframework.web.multipart.MultipartFile
import jakarta.servlet.http.Part
import java.util.UUID
import java.util.Comparator
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository
import org.springframework.transaction.annotation.Transactional
import com.fasterxml.jackson.databind.ObjectMapper
import com.ffii.fpsms.m18.service.M18BomForShopService
import com.ffii.fpsms.m18.model.M18BomShopSyncTriggerResult
import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse
import com.ffii.fpsms.m18.entity.M18BomShopSyncLog
import com.ffii.fpsms.m18.entity.M18BomShopSyncLogRepository
import com.ffii.fpsms.modules.common.SettingNames
import com.ffii.fpsms.modules.settings.entity.Settings
import com.ffii.fpsms.modules.settings.service.SettingsService

@Service
open class BomService(
    private val bomRepository: BomRepository,
    private val bomMaterialRepository: BomMaterialRepository,
    private val bomProcessRepository: BomProcessRepository,
    private val bomProcessMaterialRepository: BomProcessMaterialRepository,
    private val itemsService: ItemsService,
    private val itemsRepository: ItemsRepository,
    private val uomConversionService: UomConversionService,
    private val uomConversionRepository: UomConversionRepository,
    private val equipmentRepository: EquipmentRepository,
    private val processRepository: ProcessRepository,
    private val equipmentDetailRepository: EquipmentDetailRepository,
    private val bomWeightingScoreRepository: BomWeightingScoreRepository,
    private val itemUomService: ItemUomService,
    private val jobOrderRepository: JobOrderRepository,
    private val productProcessRepository: ProductProcessRepository,
    private val m18BomForShopService: M18BomForShopService,
    private val m18BomShopSyncLogRepository: M18BomShopSyncLogRepository,
    private val objectMapper: ObjectMapper,
    private val settingsService: SettingsService,
    @Value("\${bom.import.temp-dir:\${java.io.tmpdir}/fpsms-bom-import}") private val bomImportTempDir: String,
) {
    open fun uploadBomFiles(files: List<MultipartFile>): BomUploadResponse {
        val batchId = UUID.randomUUID().toString()
        val batchDir = Paths.get(bomImportTempDir, batchId).toAbsolutePath()
        Files.createDirectories(batchDir)
        val fileNames = mutableListOf<String>()
        val usedNames = mutableSetOf<String>()
        for (file in files) {
            if (file.isEmpty) continue
            val originalFilename = file.originalFilename ?: "unknown_${fileNames.size}.xlsx"
            var safeName = originalFilename.takeIf { it.endsWith(".xlsx", ignoreCase = true) } ?: "$originalFilename.xlsx"
            while (usedNames.contains(safeName)) {
                val base = safeName.dropLast(5) // ".xlsx"
                val n = fileNames.count { it.startsWith(base) && it.endsWith(".xlsx") } + 1
                safeName = "${base}_$n.xlsx"
            }
            usedNames.add(safeName)
            val dest = batchDir.resolve(safeName)
            file.transferTo(dest.toFile())
            fileNames.add(safeName)
        }
        return BomUploadResponse(batchId = batchId, fileNames = fileNames)
    }

    open fun uploadBomFilesFromParts(parts: List<Part>): BomUploadResponse {
        val batchId = UUID.randomUUID().toString()
        val batchDir = Paths.get(bomImportTempDir, batchId).toAbsolutePath()
        Files.createDirectories(batchDir)
        val fileNames = mutableListOf<String>()
        val usedNames = mutableSetOf<String>()
        for (part in parts) {
            val originalFilename = part.submittedFileName?.takeIf { it.isNotBlank() } ?: "unknown_${fileNames.size}.xlsx"
            var safeName = originalFilename.takeIf { it.endsWith(".xlsx", ignoreCase = true) } ?: "$originalFilename.xlsx"
            while (usedNames.contains(safeName)) {
                val base = safeName.dropLast(5)
                val n = fileNames.count { it.startsWith(base) && it.endsWith(".xlsx") } + 1
                safeName = "${base}_$n.xlsx"
            }
            usedNames.add(safeName)
            val dest = batchDir.resolve(safeName)
            part.inputStream.use { Files.copy(it, dest, StandardCopyOption.REPLACE_EXISTING) }
            fileNames.add(safeName)
        }
        return BomUploadResponse(batchId = batchId, fileNames = fileNames)
    }

    private fun getBatchDir(batchId: String): java.nio.file.Path =
        Paths.get(bomImportTempDir, batchId).toAbsolutePath()
    open fun findAll(): List<Bom> {
        return bomRepository.findAll()
    }

    open fun findById(id: Long): Bom? {
        return bomRepository.findByIdAndDeletedIsFalse(id)
    }

    open fun findByM18Id(m18Id: Long): Bom? {
        return bomRepository.findByM18IdAndDeletedIsFalse(m18Id)
    }

    open fun findByItemId(itemId: Long): Bom? {
        return bomRepository.findAllByItemIdAndDeletedIsFalse(itemId)
            .minByOrNull { if (it.description == "FG") 0 else 1 }
            ?: bomRepository.findAllByItemIdAndDeletedIsFalse(itemId).firstOrNull()
    }

    /** Resolve BOM header for a finished-good item code ([Items.code] on [Bom.item]). */
    open fun findBomSummaryByItemCode(itemCodeTrimmed: String): BomIdByItemCodeResponse {
        val code = itemCodeTrimmed.trim()
        val item = itemsRepository.findByCodeAndDeletedFalse(code)
            ?: return BomIdByItemCodeResponse(
                itemCode = code,
                message = "Item not found for code",
            )
        val bom = findByItemId(item.id!!)
            ?: return BomIdByItemCodeResponse(
                itemCode = code,
                itemId = item.id,
                message = "No BOM linked to this item",
            )
        return BomIdByItemCodeResponse(
            itemCode = code,
            itemId = item.id,
            bomId = bom.id,
            bomCode = bom.code,
            bomM18Id = bom.m18Id,
        )
    }

    open fun saveBom(request: SaveBomRequest): SaveBomResponse {

        val item = request.code.let { itemsService.findByM18BomCode(it) } ?: request.itemId?.let { itemsService.findById(it) }

        val uom = request.m18UomId?.let { uomConversionService.findByM18Id(it) }
            ?: request.uomId?.let { uomConversionService.findById(it) }

        val bom = request.m18Id?.let { findByM18Id(it) }
            ?: request.id?.let { findById(it) }
            ?: Bom()

        bom.apply {
            this.item = item
            code = request.code
            name = request.name
            description = request.description
            outputQty = request.outputQty
            outputQtyUom = request.outputQtyUom
            yield = request.yield
            this.uom = uom
            m18Id = request.m18Id ?: m18Id
            m18LastModifyDate = request.m18LastModifyDate ?: m18LastModifyDate
        }

        val response = bomRepository.saveAndFlush(bom).let {
            SaveBomResponse(
                id = it.id,
                code = it.code,
                name = it.name,
                description = it.description,
                outputQty = it.outputQty,
                outputQtyUom = it.outputQtyUom,
                yield = it.yield,
                uomName = uom?.udfudesc,
            )
        }

        return response
    }

    /**
     * Edit BOM (basic fields + BOM materials + BOM process lines).
     *
     * Key rules:
     * 1) baseScore is server-calculated and cannot be edited by client.
     * 2) Input existence will be validated (BOM/Item/Process/Equipment/UOM settings).
     * 3) When `materials` or `processes` is provided, we rebuild BomProcessMaterial mappings
     *    using current BOM materials & process lines (default: link every material to every process line).
     */
    @Transactional
    open fun editBom(id: Long, request: EditBomRequest): BomDetailResponse {
        val bom = bomRepository.findByIdAndDeletedIsFalse(id)
            ?: throw NotFoundException()

        // ---- update basic/score fields (only when provided) ----
        request.description?.let { bom.description = it }
        request.outputQty?.let { bom.outputQty = it }
        request.outputQtyUom?.let { bom.outputQtyUom = it }
        request.yield?.let { bom.yield = it }

        request.isDark?.let { bom.isDark = it }
        request.isFloat?.let { bom.isFloat = it }
        request.isDense?.let { bom.isDense = it }
        request.scrapRate?.let { bom.scrapRate = it }
        request.allergicSubstances?.let { bom.allergicSubstances = it }
        request.timeSequence?.let { bom.timeSequence = it }
        request.complexity?.let { bom.complexity = it }
        request.isDrink?.let { bom.isDrink = it }

        val replaceMaterials = request.materials != null
        val replaceProcesses = request.processes != null

        if (!replaceMaterials && !replaceProcesses) {
            // Only basic/score fields changed: just recalc.
            bom.baseScore = calculateBaseScore(bom)
            bomRepository.saveAndFlush(bom)
            return getBomDetail(bom.id!!)
        }

        // ---- pre-clean BomProcessMaterial to avoid FK issues when clearing/recreating children ----
        fun deleteAllBomProcessMaterialsForCurrentProcesses() {
            val processIds = bom.bomProcesses.mapNotNull { it.id }
            if (processIds.isNotEmpty()) {
                val existing = bomProcessMaterialRepository.findByBomProcess_IdIn(processIds)
                if (existing.isNotEmpty()) {
                    bomProcessMaterialRepository.deleteAll(existing)
                }
            }
        }

        if (replaceMaterials) {
            deleteAllBomProcessMaterialsForCurrentProcesses()
            bom.bomMaterials.clear()
            request.materials?.forEach { mReq ->
                val item = when {
                    mReq.itemId != null -> itemsRepository.findByIdAndDeletedFalse(mReq.itemId) ?: throw IllegalArgumentException("Item not found: id=${mReq.itemId}")
                    !mReq.itemCode.isNullOrBlank() -> itemsRepository.findByCodeAndDeletedFalse(mReq.itemCode.trim()) ?: throw IllegalArgumentException("Item not found: code=${mReq.itemCode}")
                    else -> throw IllegalArgumentException("Material requires itemId or itemCode")
                }

                val baseItemUom = itemUomService.findBaseUnitByItemId(item.id!!)
                    ?: throw IllegalArgumentException("Item base unit not configured: itemId=${item.id}")
                val stockItemUom = itemUomService.findStockUnitByItemId(item.id!!)
                    ?: throw IllegalArgumentException("Item stock unit not configured: itemId=${item.id}")
                val salesItemUom = itemUomService.findSalesUnitByItemId(item.id!!)
                    ?: throw IllegalArgumentException("Item sales unit not configured: itemId=${item.id}")

                val baseUom = baseItemUom.uom ?: throw IllegalArgumentException("Base UOM conversion not configured: itemId=${item.id}")
                val stockUom = stockItemUom.uom ?: throw IllegalArgumentException("Stock UOM conversion not configured: itemId=${item.id}")
                val salesUom = salesItemUom.uom ?: throw IllegalArgumentException("Sales UOM conversion not configured: itemId=${item.id}")

                val baseQty = mReq.qty

                val stockQty = itemUomService.convertUomByItem(
                    ConvertUomByItemRequest(
                        itemId = item.id!!,
                        qty = baseQty,
                        uomId = baseUom.id,
                        targetUnit = "stockUnit"
                    )
                ).newQty
                
                val salesConverted = itemUomService.convertUomByItem(
                    ConvertUomByItemRequest(
                        itemId = item.id!!,
                        qty = baseQty,
                        uomId = baseUom.id,
                        targetUnit = "salesUnit"
                    )
                )
                val salesQty = salesConverted.newQty
                val stockUnitName = stockItemUom.uom?.udfudesc
                val baseUnitName = baseItemUom.uom?.udfudesc

                val salesUnitCode = salesConverted.udfudesc

                val bomMaterial = BomMaterial().apply {
                    this.item = item
                    this.itemName = item.name
                    this.isConsumable = mReq.isConsumable ?: false

                    // BOM unit fields: derive from item's base unit.
                    this.qty = baseQty
                    this.uom = baseUom
                    this.uomName = baseUnitName

                    // Derived UOMs for display/logic.
                    this.saleQty = salesQty
                    this.salesUnit = salesUom
                    this.salesUnitCode = salesUnitCode

                    this.baseQty = baseQty
                    this.baseUnit = baseUom.id!!.toInt()
                        .let { Integer.valueOf(it) } as? Integer
                    this.baseUnitName = baseUnitName

                    this.stockQty = stockQty
                    this.stockUnit = stockUom.id!!.toInt()
                        .let { Integer.valueOf(it) } as? Integer
                    this.stockUnitName = stockUnitName

                    this.bom = bom
                }
                bom.bomMaterials.add(bomMaterial)
            }
        }

        if (replaceProcesses) {
            deleteAllBomProcessMaterialsForCurrentProcesses()
            bom.bomProcesses.clear()

            val maxSeqNoFromProvided =
                request.processes?.mapNotNull { it.seqNo }?.maxOrNull() ?: 0L
            var nextSeqNo = maxSeqNoFromProvided + 1

            // ---- seqNo uniqueness validation (when provided) ----
            val seqCounts = request.processes
                ?.mapNotNull { it.seqNo }
                ?.groupingBy { it }
                ?.eachCount()
                ?: emptyMap()
            val duplicates = seqCounts.filter { it.value > 1 }.keys
            if (duplicates.isNotEmpty()) {
                throw IllegalArgumentException("Duplicate process seqNo: ${duplicates.joinToString(",")}")
            }

            request.processes?.forEach { pReq ->
                val process = when {
                    pReq.processId != null -> processRepository.findById(pReq.processId).orElse(null)
                    !pReq.processCode.isNullOrBlank() -> processRepository.findByCodeAndDeletedIsFalse(pReq.processCode.trim())
                    else -> null
                } ?: throw IllegalArgumentException("Process not found: processId=${pReq.processId}, processCode=${pReq.processCode}")

                if (process.deleted == true) {
                    throw IllegalArgumentException("Process is deleted: id=${process.id}")
                }

                val equipment = resolveEquipmentForBomProcess(pReq)

                val seqNo = pReq.seqNo ?: nextSeqNo++
                val description = pReq.description ?: ""

                val bomProcess = BomProcess().apply {
                    this.process = process
                    this.equipment = equipment
                    this.description = description
                    this.seqNo = seqNo
                    this.durationInMinute = pReq.durationInMinute ?: 0
                    this.prepTimeInMinute = pReq.prepTimeInMinute ?: 0
                    this.postProdTimeInMinute = pReq.postProdTimeInMinute ?: 0
                    this.bom = bom
                }
                bom.bomProcesses.add(bomProcess)
            }
        }

        // ---- persist children first (so ids exist) ----
        bomRepository.saveAndFlush(bom)

        // ---- rebuild BomProcessMaterial mappings when children changed ----
        if (replaceMaterials || replaceProcesses) {
            val processIds = bom.bomProcesses.mapNotNull { it.id }
            if (processIds.isNotEmpty()) {
                val existing = bomProcessMaterialRepository.findByBomProcess_IdIn(processIds)
                if (existing.isNotEmpty()) {
                    bomProcessMaterialRepository.deleteAll(existing)
                }
            }

            val mappings = mutableListOf<BomProcessMaterial>()
            bom.bomProcesses.forEach { proc ->
                bom.bomMaterials.forEach { mat ->
                    mappings.add(
                        BomProcessMaterial().apply {
                            this.bomProcess = proc
                            this.bomMaterial = mat
                        }
                    )
                }
            }
            if (mappings.isNotEmpty()) {
                bomProcessMaterialRepository.saveAll(mappings)
            }
        }

        // ---- baseScore recalculation (server controlled) ----
        bom.baseScore = calculateBaseScore(bom)
        bomRepository.saveAndFlush(bom)

        return getBomDetail(bom.id!!)
    }

    /**
     * When {@link SettingNames#M18_BOM_SHOP_SYNC_ENABLED} is true, push BOM to M18 udfBomForShop.
     * Use {@code POST /m18/test/bom-shop-sync/{bomId}} (or future UI) to trigger explicitly.
     * Optional [m18HeaderId]: M18 udfBomForShop **header** record id (e.g. [BomIdByItemCodeResponse.bomM18Id])
     * to force **update** when [Bom.m18Id] is missing or stale. When null, uses [Bom.m18Id] if set.
     */
    open fun pushBomToM18ShopIfAllowed(bomId: Long, m18HeaderId: Long? = null): M18BomShopSyncTriggerResult {
        if (!isM18BomShopSyncEnabled()) {
            return M18BomShopSyncTriggerResult(
                bomId = bomId,
                synced = false,
                skippedReason = "M18 BOM shop sync disabled (${SettingNames.M18_BOM_SHOP_SYNC_ENABLED} is not true)",
            )
        }
        val bom = bomRepository.findByIdAndDeletedIsFalse(bomId)
            ?: return M18BomShopSyncTriggerResult(bomId, false, skippedReason = "BOM not found")
        val req = m18BomForShopService.buildSaveRequest(bom, m18HeaderId)
            ?: return M18BomShopSyncTriggerResult(bomId, false, skippedReason = "Cannot build M18 payload (missing m18 UOM/item ids or no materials)")

        val requestJsonPayload = m18BomForShopService.toJson(req)
        var resp: GoodsReceiptNoteResponse? = null
        var callError: Throwable? = null
        try {
            resp = m18BomForShopService.saveBomForShop(req)
        } catch (e: Exception) {
            callError = e
        }

        val responseJsonPayload = when {
            resp != null -> m18BomForShopService.toJson(resp)
            callError != null ->
                runCatching {
                    objectMapper.writeValueAsString(
                        mapOf(
                            "exceptionType" to callError.javaClass.name,
                            "message" to (callError.message ?: ""),
                        ),
                    )
                }.getOrElse { """{"error":"failed to serialize exception"}""" }
            else -> """{"error":"M18 API returned null"}"""
        }

        val msgSummary = resp?.messages?.joinToString("; ") { it.msgDetail ?: it.msgCode ?: "" }.orEmpty()
        val apiStatus = resp?.status == true
        val recordId = resp?.recordId ?: 0L

        val result = when {
            callError != null ->
                M18BomShopSyncTriggerResult(
                    bomId = bomId,
                    synced = false,
                    skippedReason = callError.message ?: "M18 API call failed",
                    status = false,
                    messageSummary = callError.message,
                )
            resp == null ->
                M18BomShopSyncTriggerResult(bomId, false, skippedReason = "M18 API returned null")
            resp.status == true && resp.recordId > 0L -> {
                bom.m18Id = resp.recordId
                bomRepository.saveAndFlush(bom)
                M18BomShopSyncTriggerResult(
                    bomId = bomId,
                    synced = true,
                    recordId = resp.recordId,
                    status = true,
                    messageSummary = msgSummary.ifBlank { null },
                )
            }
            else ->
                M18BomShopSyncTriggerResult(
                    bomId = bomId,
                    synced = false,
                    skippedReason = "M18 save failed or status=false",
                    recordId = resp.recordId.takeIf { it > 0 },
                    status = resp.status,
                    messageSummary = msgSummary.ifBlank { null },
                )
        }

        val logMessage = listOfNotNull(
            msgSummary.ifBlank { null },
            callError?.message,
            result.skippedReason?.takeIf { !result.synced },
        ).joinToString("; ").take(4000)

        m18BomShopSyncLogRepository.save(
            M18BomShopSyncLog().apply {
                this.bomId = bomId
                finishedItemCode = req.udfbomforshop.values.firstOrNull()?.udfBomCode
                m18HeaderCode = req.udfbomforshop.values.firstOrNull()?.code
                requestFingerprint = m18BomForShopService.contentFingerprint(req)
                m18RecordId = recordId.takeIf { it > 0 }
                m18ApiStatus = apiStatus
                synced = result.synced
                message = logMessage.ifBlank { null }
                requestJson = requestJsonPayload
                responseJson = responseJsonPayload
            },
        )

        return result
    }

    private fun isM18BomShopSyncEnabled(): Boolean =
        settingsService.findByName(SettingNames.M18_BOM_SHOP_SYNC_ENABLED)
            .map { Settings.VALUE_BOOLEAN_TRUE == it.value }
            .orElse(false)

    private fun resolveEquipmentForBomProcess(pReq: EditBomProcessRequest): Equipment {
        val equipmentId = pReq.equipmentId
        val equipmentCode = pReq.equipmentCode?.trim().orEmpty()

        val found = when {
            equipmentId != null -> equipmentRepository.findByIdAndDeletedFalse(equipmentId)
            equipmentCode.isNotBlank() -> equipmentRepository.findByCodeAndDeletedFalse(equipmentCode)
                ?: equipmentRepository.findByNameAndDeletedIsFalse(equipmentCode)
            else -> null
        }

        if (found != null) return found

        // Create new equipment on-demand (optional)
        if (pReq.newEquipment != null) {
            val req = pReq.newEquipment
            val duplicated = equipmentRepository.findByCodeAndDeletedIsFalse(req.code.trim())
            if (duplicated != null) {
                throw IllegalArgumentException("Equipment code already exists: code=${req.code}")
            }

            val equipmentDetail = req.equipmentTypeId?.let { equipmentDetailRepository.findByIdAndDeletedFalse(it) }

            val entity = Equipment().apply {
                code = req.code.trim()
                name = req.name.trim()
                description = req.description?.trim().orEmpty()
                this.EquipmentDetail = equipmentDetail
                deleted = false
            }
            return equipmentRepository.saveAndFlush(entity)
        }

        // Fallback: create a default placeholder equipment to satisfy non-null FK.
        val placeholderCode = if (equipmentCode.isNotBlank()) equipmentCode else "不適用"
        return equipmentRepository.findByCodeAndDeletedIsFalse(placeholderCode)
            ?: equipmentRepository.findByNameAndDeletedIsFalse(placeholderCode)
            ?: equipmentRepository.saveAndFlush(
                Equipment().apply {
                    code = placeholderCode
                    name = placeholderCode
                    description = placeholderCode
                    deleted = false
                }
            )
    }

    //////// -------------------------------- for excel import ------------------------------- /////////
    private fun saveBomEntity(req: ImportBomRequest): Bom {
        val item =  itemsRepository.findByCodeAndDeletedFalse(req.code) ?: itemsRepository.findByNameAndDeletedFalse(req.name)
        val uom = if (req.uomId != null) uomConversionRepository.findById(req.uomId!!).orElseThrow() else null
        val bom = bomRepository.findByCodeAndDeletedIsFalse(req.code) ?: Bom()
        bom.apply {
            this.isDark = req.isDark
            this.isFloat = req.isFloat
            this.isDense = req.isDense
            this.code = req.code
            this.scrapRate = req.scrapRate
            this.timeSequence = req.timeSequence
            this.complexity = req.complexity
            this.allergicSubstances = req.allergicSubstances
            this.name = req.name
            this.description = req.description
            this.item = item
            this.outputQty = req.outputQty
            this.outputQtyUom = uom?.udfudesc
            this.yield = req.yield
            this.uom = uom
//            this.excelUom = req.excelUom
        }
        bom.baseScore = calculateBaseScore(bom)
        val savedBom = bomRepository.saveAndFlush(bom)
//        println("saved: ${savedBom.id}")
        return savedBom
    }

    private fun saveBomMaterial(req: ImportBomMatRequest): BomMaterial {
        val bomMaterial = req.bom?.id?.let { bId -> 
            req.item?.id?.let { iId -> 
                bomMaterialRepository.findByBomIdAndItemId(bId, iId) 
            } 
        } ?: BomMaterial()

        val item = req.item
        val bomId = req.bom?.id
        val bomCode = req.bom?.code

        // ===== 新逻辑：使用 Excel column 6 的 saleQty 和 column 7 的 sales unit =====
        val excelSaleQty = req.saleQty  // Excel 第6栏的 saleQty
        val excelSalesUnitCode = req.salesUnitCode  // Excel 第7栏的 sales unit code（字符串）
        val excelSalesUnit = req.salesUnit  // Excel 第7栏查找的 uom_conversion（可能为 null）

        var saleQty: BigDecimal? = excelSaleQty
        var saleUnitId: Long? = null
        var saleUnitCode: String? = null

        var baseQty: BigDecimal? = null
        var baseUnit: Integer? = null
        var baseUnitName: String? = null

        var stockQty: BigDecimal? = null
        var stockUnit: Integer? = null
        var stockUnitName: String? = null

        if (item?.id != null) {
            val itemId = item.id!!
            try {
                // ---- 1) 获取 item 的真实 sale unit ----
                val saleItemUom = itemUomService.findSalesUnitByItemId(itemId)
                val itemSaleUnit = saleItemUom?.uom
                
                // ---- 2) 确定 sale unit（来自 Excel column 7）----
                if (excelSalesUnit != null) {
                    // Excel 找到了 uom_conversion
                    saleUnitId = excelSalesUnit.id
                    saleUnitCode = excelSalesUnit.udfudesc
                    
                    // 检查 Excel sales unit 与 item sale unit 是否匹配
                    if (itemSaleUnit != null && excelSalesUnit.id != itemSaleUnit.id) {
                        bomMaterialImportIssues.add(
                            BomMaterialImportIssue(
                                bomId = bomId,
                                bomCode = bomCode,
                                itemId = item.id,
                                itemCode = item.code,
                                itemName = item.name,
                                reason = "Excel sales unit (${excelSalesUnit.code}/${excelSalesUnit.udfudesc}) does not match item sale unit (${itemSaleUnit.code}/${itemSaleUnit.udfudesc})",
                                srcQty = excelSaleQty,
                                srcUomCode = excelSalesUnit.code
                            )
                        )
                    }
                } else {
                    // Excel 没有找到 uom_conversion，使用 Excel 数据本身
                    saleUnitCode = excelSalesUnitCode
                    
                    if (excelSalesUnitCode != null) {
                        bomMaterialImportIssues.add(
                            BomMaterialImportIssue(
                                bomId = bomId,
                                bomCode = bomCode,
                                itemId = item.id,
                                itemCode = item.code,
                                itemName = item.name,
                                reason = "Excel sales unit code '$excelSalesUnitCode' not found in uom_conversion, using Excel data as-is",
                                srcQty = excelSaleQty,
                                srcUomCode = excelSalesUnitCode
                            )
                        )
                    }
                }

                // ---- 3) 从 saleQty（sale unit）转换为 baseQty（base unit）----
                if (excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) {
                    try {
                        // 先检查 item 是否有 base unit
                        val baseItemUom = itemUomService.findBaseUnitByItemId(itemId)
                        if (baseItemUom == null) {
                            bomMaterialImportIssues.add(
                                BomMaterialImportIssue(
                                    bomId = bomId,
                                    bomCode = bomCode,
                                    itemId = item.id,
                                    itemCode = item.code,
                                    itemName = item.name,
                                    reason = "Cannot convert saleQty to baseQty: item base unit not found",
                                    srcQty = excelSaleQty,
                                    srcUomCode = excelSalesUnit.code
                                )
                            )
                        } else {
                            val baseResult = itemUomService.convertUomByItem(
                                ConvertUomByItemRequest(
                                    itemId = itemId,
                                    qty = excelSaleQty,
                                    uomId = excelSalesUnit.id!!,
                                    targetUnit = "baseUnit"
                                )
                            )
                            baseQty = baseResult.newQty
                            
                            // 获取 base unit 信息
                            baseUnit = baseItemUom.uom?.id?.toInt()?.let { Integer.valueOf(it) } as? Integer
                            baseUnitName = baseItemUom.uom?.udfudesc
                            
                            // 验证转换结果
                            if (baseQty == null) {
                                bomMaterialImportIssues.add(
                                    BomMaterialImportIssue(
                                        bomId = bomId,
                                        bomCode = bomCode,
                                        itemId = item.id,
                                        itemCode = item.code,
                                        itemName = item.name,
                                        reason = "Cannot convert saleQty to baseQty: conversion returned null",
                                        srcQty = excelSaleQty,
                                        srcUomCode = excelSalesUnit.code
                                    )
                                )
                            }
                        }
                    } catch (e: IllegalArgumentException) {
                        bomMaterialImportIssues.add(
                            BomMaterialImportIssue(
                                bomId = bomId,
                                bomCode = bomCode,
                                itemId = item.id,
                                itemCode = item.code,
                                itemName = item.name,
                                reason = "Cannot convert saleQty to baseQty: ${e.message ?: "IllegalArgumentException"}",
                                srcQty = excelSaleQty,
                                srcUomCode = excelSalesUnit.code
                            )
                        )
                        println("【BOM Import Warning】bomCode=$bomCode, item=${item.code} 转 baseQty 失败: ${e.message}")
                    } catch (e: Exception) {
                        bomMaterialImportIssues.add(
                            BomMaterialImportIssue(
                                bomId = bomId,
                                bomCode = bomCode,
                                itemId = item.id,
                                itemCode = item.code,
                                itemName = item.name,
                                reason = "Cannot convert saleQty to baseQty: ${e.javaClass.simpleName} - ${e.message ?: "Unknown error"}",
                                srcQty = excelSaleQty,
                                srcUomCode = excelSalesUnit.code
                            )
                        )
                        println("【BOM Import Error】bomCode=$bomCode, item=${item.code} 转 baseQty 失败: ${e.message}")
                    }
                } else if (excelSaleQty != null && excelSalesUnit == null) {
                    bomMaterialImportIssues.add(
                        BomMaterialImportIssue(
                            bomId = bomId,
                            bomCode = bomCode,
                            itemId = item.id,
                            itemCode = item.code,
                            itemName = item.name,
                            reason = "Cannot convert saleQty to baseQty: Excel sales unit not found",
                            srcQty = excelSaleQty,
                            srcUomCode = excelSalesUnitCode
                        )
                    )
                }

                // ---- 4) 从 saleQty（sale unit）转换为 stockQty（stock unit）----
                if (excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) {
                    try {
                        // 先检查 item 是否有 stock unit
                        val stockItemUom = itemUomService.findStockUnitByItemId(itemId)
                        if (stockItemUom == null) {
                            bomMaterialImportIssues.add(
                                BomMaterialImportIssue(
                                    bomId = bomId,
                                    bomCode = bomCode,
                                    itemId = item.id,
                                    itemCode = item.code,
                                    itemName = item.name,
                                    reason = "Cannot convert saleQty to stockQty: item stock unit not found",
                                    srcQty = excelSaleQty,
                                    srcUomCode = excelSalesUnit.code
                                )
                            )
                        } else {
                            val stockResult = itemUomService.convertUomByItem(
                                ConvertUomByItemRequest(
                                    itemId = itemId,
                                    qty = excelSaleQty,
                                    uomId = excelSalesUnit.id!!,
                                    targetUnit = "stockUnit"
                                )
                            )
                            stockQty = stockResult.newQty
                            
                            // 获取 stock unit 信息
                            stockUnit = stockItemUom.uom?.id?.toInt()?.let { Integer.valueOf(it) } as? Integer
                            stockUnitName = stockItemUom.uom?.udfudesc
                            
                            // 验证转换结果
                            if (stockQty == null) {
                                bomMaterialImportIssues.add(
                                    BomMaterialImportIssue(
                                        bomId = bomId,
                                        bomCode = bomCode,
                                        itemId = item.id,
                                        itemCode = item.code,
                                        itemName = item.name,
                                        reason = "Cannot convert saleQty to stockQty: conversion returned null",
                                        srcQty = excelSaleQty,
                                        srcUomCode = excelSalesUnit.code
                                    )
                                )
                            }
                        }
                    } catch (e: IllegalArgumentException) {
                        bomMaterialImportIssues.add(
                            BomMaterialImportIssue(
                                bomId = bomId,
                                bomCode = bomCode,
                                itemId = item.id,
                                itemCode = item.code,
                                itemName = item.name,
                                reason = "Cannot convert saleQty to stockQty: ${e.message ?: "IllegalArgumentException"}",
                                srcQty = excelSaleQty,
                                srcUomCode = excelSalesUnit.code
                            )
                        )
                        println("【BOM Import Warning】bomCode=$bomCode, item=${item.code} 转 stockQty 失败: ${e.message}")
                    } catch (e: Exception) {
                        bomMaterialImportIssues.add(
                            BomMaterialImportIssue(
                                bomId = bomId,
                                bomCode = bomCode,
                                itemId = item.id,
                                itemCode = item.code,
                                itemName = item.name,
                                reason = "Cannot convert saleQty to stockQty: ${e.javaClass.simpleName} - ${e.message ?: "Unknown error"}",
                                srcQty = excelSaleQty,
                                srcUomCode = excelSalesUnit.code
                            )
                        )
                        println("【BOM Import Error】bomCode=$bomCode, item=${item.code} 转 stockQty 失败: ${e.message}")
                    }
                } else if (excelSaleQty != null && excelSalesUnit == null) {
                    bomMaterialImportIssues.add(
                        BomMaterialImportIssue(
                            bomId = bomId,
                            bomCode = bomCode,
                            itemId = item.id,
                            itemCode = item.code,
                            itemName = item.name,
                            reason = "Cannot convert saleQty to stockQty: Excel sales unit not found",
                            srcQty = excelSaleQty,
                            srcUomCode = excelSalesUnitCode
                        )
                    )
                }

                // 最终检查：如果 stockQty 仍然为 null，记录问题
                if (stockQty == null && excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) {
                    // 检查是否已经记录过这个问题（避免重复）
                    val alreadyReported = bomMaterialImportIssues.any { issue ->
                        issue.itemId == item.id && 
                        issue.reason.contains("stockQty", ignoreCase = true) &&
                        issue.srcQty == excelSaleQty
                    }
                    if (!alreadyReported) {
                        bomMaterialImportIssues.add(
                            BomMaterialImportIssue(
                                bomId = bomId,
                                bomCode = bomCode,
                                itemId = item.id,
                                itemCode = item.code,
                                itemName = item.name,
                                reason = "Cannot convert saleQty to stockQty: conversion failed (final check)",
                                srcQty = excelSaleQty,
                                srcUomCode = excelSalesUnit.code
                            )
                        )
                    }
                }

            } catch (e: IllegalArgumentException) {
                bomMaterialImportIssues.add(
                    BomMaterialImportIssue(
                        bomId = bomId,
                        bomCode = bomCode,
                        itemId = item.id,
                        itemCode = item.code,
                        itemName = item.name,
                        reason = e.message ?: "UOM conversion error",
                        srcQty = excelSaleQty,
                        srcUomCode = excelSalesUnitCode
                    )
                )
                println("【BOM Import Warning】bomCode=$bomCode, item=${item.code} 转 UOM 失败: ${e.message}")
            } catch (e: Exception) {
                bomMaterialImportIssues.add(
                    BomMaterialImportIssue(
                        bomId = bomId,
                        bomCode = bomCode,
                        itemId = item.id,
                        itemCode = item.code,
                        itemName = item.name,
                        reason = "Unknown error: ${e.message}",
                        srcQty = excelSaleQty,
                        srcUomCode = excelSalesUnitCode
                    )
                )
                println("【BOM Import Error】bomCode=$bomCode, item=${item.code}: ${e.message}")
            }
        } else {
            // item 缺失
            bomMaterialImportIssues.add(
                BomMaterialImportIssue(
                    bomId = bomId,
                    bomCode = bomCode,
                    itemId = item?.id,
                    itemCode = item?.code,
                    itemName = item?.name,
                    reason = "Missing item for conversion",
                    srcQty = excelSaleQty,
                    srcUomCode = excelSalesUnitCode
                )
            )
        }

        // ===== 写回 BomMaterial =====
        bomMaterial.apply {
            this.item = req.item
            this.itemName = req.item?.name
            this.isConsumable = req.isConsumable
            this.qty = req.qty                 // BOM 原始用量（column 2，保留）
            this.uom = req.uom                 // BOM 原始 UOM（column 3，保留）
            this.uomName = req.uomName

            // 新逻辑：使用 Excel column 6 的 saleQty 和 column 7 的 sales unit
            this.saleQty = saleQty
            this.salesUnit = excelSalesUnit  // 使用 Excel 的 sales unit，不是 item 的 stock unit
            this.salesUnitCode = saleUnitCode

            // 从 sale unit 转换为 base unit
            this.baseQty = baseQty
            this.baseUnit = baseUnit
            this.baseUnitName = baseUnitName

            // 从 sale unit 转换为 stock unit（新增）
            this.stockQty = stockQty
            this.stockUnit = stockUnit
            this.stockUnitName = stockUnitName

            this.bom = req.bom
        }
        return bomMaterialRepository.saveAndFlush(bomMaterial)
    }
    private fun saveBomProcess(req: ImportBomProcessRequest): BomProcess {
        val bomProcess = req.bom!!.id?.let { id -> req.seqNo?.let { seqNo -> bomProcessRepository.findBySeqNoAndBomIdAndDeletedIsFalse(seqNo.toInt(), id) } } ?: BomProcess()
        bomProcess.apply {
            this.process = req.process
            this.equipment = req.equipment
            this.description = req.description
            this.seqNo = req.seqNo
            this.durationInMinute = req.durationInMinute
            this.prepTimeInMinute = req.prepTimeInMinute
            this.postProdTimeInMinute = req.postProdTimeInMinute
            this.bom = req.bom
        }
        return bomProcessRepository.saveAndFlush(bomProcess)
    }
    fun saveBomProcessMaterial(req: ImportBomProcessMaterialRequest): BomProcessMaterial {
        val bomProcessMaterial = req.bomProcess?.id?.let { pid -> req.bomMaterial?.id?.let { mid -> bomProcessMaterialRepository.findByBomProcessIdAndBomMaterialId(pid, mid)} } ?: BomProcessMaterial()
        bomProcessMaterial.apply {
            this.bomProcess = req.bomProcess
            this.bomMaterial = req.bomMaterial
        }
        return bomProcessMaterialRepository.saveAndFlush(bomProcessMaterial)
    }
    private fun importExcelBomMaterial(bom: Bom, sheet: Sheet) {
        val startCol = 0
        val endCol = 10
        val maxRowIndex = 200
    
        // 1) 找到「材料編號」表头
        var headerRowIndex = -1
        var searchRowIndex = 10
        while (searchRowIndex < maxRowIndex) {
            val row = sheet.getRow(searchRowIndex)
            val cell = row?.getCell(startCol)
            if (cell != null &&
                cell.cellType == CellType.STRING &&
                cell.stringCellValue.trim() == "材料編號"
            ) {
                headerRowIndex = searchRowIndex
                break
            }
            searchRowIndex++
        }
    
        if (headerRowIndex == -1) {
            println("importExcelBomMaterial: 找不到『材料編號』表頭，略過材料匯入")
            return
        }
    
        // 2) 从表头下一行开始读，直到 col0 空白
        var rowIdx = headerRowIndex + 1
        while (rowIdx < maxRowIndex) {
            val row = sheet.getRow(rowIdx) ?: break
            val firstCell = row.getCell(0)
            if (firstCell == null || firstCell.cellType == CellType.BLANK) {
                break
            }
    
            val bomMatRequest = ImportBomMatRequest(bom = bom)
            val bomProcessMatRequest = ImportBomProcessMaterialRequest()
    
            try {
                for (colIdx in startCol..endCol) {
                    val cell = row.getCell(colIdx) ?: continue
    
                    when (colIdx) {
                        0 -> {
                            val nameCell = row.getCell(1)
                            val itemCode = cell.stringCellValue.trim()
                            val itemName = nameCell?.stringCellValue?.trim()
    
                            val item = itemsRepository.findByCodeAndDeletedFalse(itemCode)
                                ?: itemName?.let { itemsRepository.findByNameAndDeletedFalse(it) }
    
                            bomMatRequest.item = item
                        }
                        2 -> {
                            bomMatRequest.qty = cell.numericCellValue.toBigDecimal()
                        }
                        3 -> {
                            val uomCode = cell.stringCellValue.trim()
                            val uom = uomConversionRepository.findByCodeAndDeletedFalse(uomCode)
                            bomMatRequest.uom = uom
                            bomMatRequest.uomName = uom?.udfudesc
                        }
                        6 -> {
                            bomMatRequest.saleQty = cell.numericCellValue.toBigDecimal()
                        }
                        7 -> {
                            val salesUnitCodeStr = cell.stringCellValue.trim()
                            val salesUnit = uomConversionRepository.findByCodeAndDeletedFalse(salesUnitCodeStr)
    
                            bomMatRequest.salesUnit = salesUnit
                            bomMatRequest.salesUnitCode = salesUnitCodeStr
                        }
                        10 -> {
                            val seqNo = cell.numericCellValue.toInt()
                            val bomProcess = bomProcessRepository.findBySeqNoAndBomIdAndDeletedIsFalse(
                                seqNo = seqNo,
                                bomId = bom.id!!
                            ) // if null = bugged
                            bomProcessMatRequest.bomProcess = bomProcess
                        }
                    }
                }
    
                val bomMaterial = saveBomMaterial(bomMatRequest)
                bomProcessMatRequest.bomMaterial = bomMaterial
                saveBomProcessMaterial(bomProcessMatRequest)
    
            } catch (e: Exception) {
                println("importExcelBomMaterial row ${rowIdx + 1} error: ${e.message}")
            }
    
            rowIdx++
        }
    }

    private fun importExcelBomBasicInfo(sheet: Sheet): Bom {
        var request = ImportBomRequest(
            code = "",
            name = "",
            description = "",
        )
        var startRowIndex = 0
        val endRowIndex = 10
        var startColumnIndex = 0
        val endColumnIndex = 9
        while (startRowIndex != endRowIndex || startColumnIndex != endColumnIndex) {
            val tempRow = sheet.getRow(startRowIndex)
            val tempCell = tempRow.getCell(startColumnIndex)
            if (tempCell != null && tempCell.cellType == CellType.STRING) {
                val tempCellVal = tempCell.stringCellValue.trim()
                // --------------------- assigning value that is under a header to request ------------------------ //
                val topTargetValueRow = sheet.getRow(startRowIndex + 1)
                val topTargetValueCell = topTargetValueRow.getCell(startColumnIndex)
                when (tempCellVal) {
                   "編號" -> {
                       request.code = topTargetValueCell.stringCellValue.trim()
                   }
                   "產品名稱" -> {
                       val r5Cell = sheet.getRow(4)?.getCell(17)
                       request.name = when {
                           r5Cell == null || r5Cell.cellType == CellType.BLANK -> ""
                           r5Cell.cellType == CellType.STRING -> r5Cell.stringCellValue.trim()
                           r5Cell.cellType == CellType.FORMULA && r5Cell.cachedFormulaResultType == CellType.STRING -> r5Cell.stringCellValue.trim()
                           else -> ""
                       }
                   }
                   "種類" ->  request.description = topTargetValueCell.stringCellValue.trim()
                   "份量 (Qty)" ->  request.outputQty = topTargetValueCell.numericCellValue.toBigDecimal()
                   "單位" -> {
                        val rawUomCode = topTargetValueCell.stringCellValue.trim()

                        // 先把 Excel 字原封不動存起來
                        request.outputQtyUom = rawUomCode

                        // 用它當作 UOM code 去 uom_conversion 找
                        val uom = uomConversionRepository.findByCodeAndDeletedFalse(rawUomCode)

                        if (uom != null) {
                            // 之後 saveBomEntity 會用這個 id 拿到 entity
                            request.uomId = uom.id
                            // 如果你希望 outputQtyUom 存「標準 code」而不是 Excel 原字，可以改成：
                            // request.outputQtyUom = uom.code
                        } else {
                            // 找不到就維持 Excel 原字，或在 issue log 裡紀錄問題（看你要不要）
                        }
                    }
                }
                // ----------------------------------------------------------------------------------------------- //
                // --------------------- assigning value that is next to a header to request ------------------------ //
                fun calculateColourScore(value: String): Int {
                    var score = -1
                    when (value) {
                        "淺" -> score = 0
                        "深" -> score = 1
                    }
                    return score
                }
                fun calculateFloatScore(value: String): Int {
                    var score = -1
                    when (value) {
                        "沉" -> score = 0
                        "浮" -> score = 1
                    }
                    return score
                }

                fun calculateAllergicSubstancesScore(value: String): Int {
                    var score = -1
                    when (value) {
                        "N/A" -> score = 0
                        "0" -> score = 0
                        "Yes" -> score = 5
                        "1" -> score = 5
                        "1.0" -> score = 5
                        "0.0" -> score = 0
                        " " -> score = 0
                        "" -> score = 0
                    }
                    return score
                }
                val leftTargetValueRow = sheet.getRow(startRowIndex)
                val leftTargetValueCell = leftTargetValueRow.getCell(startColumnIndex + 1)

                fun getCellValueAsString(cell: org.apache.poi.ss.usermodel.Cell?): String {
                    return when {
                        cell == null -> ""
                        cell.cellType == CellType.STRING -> cell.stringCellValue.trim()
                        cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toString().trim()
                        cell.cellType == CellType.BLANK -> ""
                        else -> ""
                    }
                }

                when {
                        //use fix row column by index not by search value contain later
                    tempCellVal.contains("深淺") ->  request.isDark = if (leftTargetValueCell?.cellType == CellType.NUMERIC)
                        leftTargetValueCell.numericCellValue.toInt()
                    else 0
                    tempCellVal.contains("浮沉") ->  request.isFloat = if (leftTargetValueCell?.cellType == CellType.NUMERIC)
                        leftTargetValueCell.numericCellValue.toInt()
                    else 0
                    tempCellVal.contains("過敏原 （如有）") ->  request.allergicSubstances = calculateAllergicSubstancesScore(getCellValueAsString(leftTargetValueCell))
                    tempCellVal.contains("濃淡") ->  request.isDense = if (leftTargetValueCell?.cellType == CellType.NUMERIC)
                        leftTargetValueCell.numericCellValue.toInt()
                    else 0
                    tempCellVal.contains("損耗率") ->  request.scrapRate = if (leftTargetValueCell?.cellType == CellType.NUMERIC)
                        leftTargetValueCell.numericCellValue.toInt()
                    else 0
                    tempCellVal.contains("生產時段先後數值") -> {
                        val timeSeq = if (leftTargetValueCell?.cellType == CellType.NUMERIC) {
                            leftTargetValueCell.numericCellValue.toInt()
                        } else {
                            0
                        }
                    
                        println("timeSequence cellType=${leftTargetValueCell?.cellType}, raw=${leftTargetValueCell?.toString()}, value=$timeSeq")
                    
                        request.timeSequence = timeSeq
                    }
                    
                    tempCellVal.contains("複雜度") ->  request.complexity = if (leftTargetValueCell?.cellType == CellType.NUMERIC)
                        leftTargetValueCell.numericCellValue.toInt()
                    else 0
                }
            }
        
            if (startRowIndex < endRowIndex) {
                startRowIndex++
            } else if (startColumnIndex < endColumnIndex) {
                startColumnIndex++
                startRowIndex = 0
            }
        }
        return saveBomEntity(request)
    }

    private fun generateNextTMPEquipmentCode(): String {
        // 讀出所有未刪除的 EquipmentDetail
        val allEquipmentDetails = equipmentDetailRepository.findAllByDeletedFalse()
    
        // 過濾出符合新格式的 code：3 個大寫字母 + 2 位數字，例如 AAA01
        val indices = allEquipmentDetails
            .mapNotNull { it.equipmentCode }
            .map { it.trim() }
            .filter { Regex("^[A-Z]{3}\\d{2}$").matches(it) }
            .mapNotNull { code ->
                // 只拿前三個字母轉成序號
                val letters = code.substring(0, 3)
                lettersToIndex(letters)
            }
    
        // 找到目前最大的 index，沒有就從 -1 開始，下一個就是 0 -> "AAA"
        val maxIndex = indices.maxOrNull() ?: -1
        val nextIndex = maxIndex + 1
    
        // 把 index 轉回三個大寫字母（0 -> AAA, 1 -> AAB, ...）
        val nextLetters = indexToLetters(nextIndex)
    
        // 尾巴兩位數固定 "01"
        return nextLetters + "01"
    }
    
    // 把 3 個大寫字母轉成序號（AAA=0, AAB=1, ..., ZZZ=17575）
    private fun lettersToIndex(letters: String): Int? {
        if (letters.length != 3) return null
        val chars = letters.toCharArray()
        if (chars.any { it !in 'A'..'Z' }) return null
    
        return ((chars[0] - 'A') * 26 * 26) +
               ((chars[1] - 'A') * 26) +
               (chars[2] - 'A')
    }
    
    // 把序號轉回 3 個大寫字母（0 -> AAA, 1 -> AAB, ...）
    private fun indexToLetters(index: Int): String {
        val i = if (index < 0) 0 else index
    
        val c3 = ('A'.code + (i % 26)).toChar()
        val c2 = ('A'.code + ((i / 26) % 26)).toChar()
        val c1 = ('A'.code + ((i / (26 * 26)) % 26)).toChar()
    
        return String(charArrayOf(c1, c2, c3))
    }
    private fun bomGetOrCreateEquipment(equipmentName: String): Equipment {
        var equipment = equipmentRepository.findByNameAndDeletedIsFalse(equipmentName)
            ?: equipmentRepository.findByCodeAndDeletedIsFalse(equipmentName)
    
        var equipment_detail = equipmentDetailRepository.findByNameAndDeletedIsFalse(equipmentName)
        if (equipment == null) {
            // 解析 XXX-YYY 格式
            val parts = equipmentName.split("-")
            val firstPart = if (parts.size > 0) parts[0] else equipmentName  // XXX
            val secondPart = if (parts.size > 1) parts[1] else ""   // YYY
            
            // 创建 equipment
            equipment = Equipment().apply {
                this.code = equipmentName  // 完整值 XXX-YYY
                this.name = secondPart
                this.description = firstPart  // XXX 写入 description
            }
            equipment = equipmentRepository.saveAndFlush(equipment)
            
            // 创建 equipment_detail
            if (equipment_detail == null) {
                val nextEquipmentCode = generateNextTMPEquipmentCode()
                equipment_detail = EquipmentDetail().apply {
                    this.name = "1號"  // 默认值
                    this.description = equipmentName  // equipmentName (完整值 XXX-YYY)
                    this.code = "${this.description}-$name" 
                    this.equipmentCode=nextEquipmentCode
                }
                equipment_detail = equipmentDetailRepository.saveAndFlush(equipment_detail)
            }
        }
        return equipment!!
    }

    private fun bomGetOrCreateProcess(name: String): Process {
        var process = processRepository.findByNameAndDeletedIsFalse(name)
            ?: processRepository.findByCodeAndDeletedIsFalse(name)
        if (process == null) {
            val truncatedName = name.take(30)
            process = Process().apply {
                this.name = truncatedName
                this.code = truncatedName
                this.description = name
            }
            process = processRepository.saveAndFlush(process)
        }
        return process!!
    }

    private fun extractDurationStringToMinutes(str: String): Int {
        val regex = """(\d+)(\D+)""".toRegex()
        val matchResult = regex.find(str)
//        fun returnMultiplier(unit: String): {
//
//        }
        val (number, unit) = matchResult?.let {
            val num1 = it.groupValues[1].toInt()
            val unit1 = it.groupValues[2]
//            val num2 = it.groupValues[3].toInt()
//            val unit2 = it.groupValues[4]
            Pair(num1, unit1)
        }!!
        var multiplier = 1
        when {
            unit.contains("min") -> {
                multiplier = 1
            }
            unit.contains("hr") -> {
                multiplier = 60
            }
        }
        return number * multiplier
    }
    private fun importExcelBomProcess(bom: Bom, sheet: Sheet) {
        var startRowIndex = 30
        var endRowIndex = 70
        var startColumnIndex = 0
        val endColumnIndex = 11
        while (startRowIndex < endRowIndex) {
            val tempRow = sheet.getRow(startRowIndex) ?: throw Exception()
            val tempCell = tempRow.getCell(startColumnIndex)
            if (tempCell != null && tempCell.cellType == CellType.STRING && tempCell.stringCellValue.trim() == "工序") {
                startRowIndex += 2 // skip column header
//                println("last: $startRowIndex")
                break
            }
            startRowIndex++
        }
        var searchRowIndex = startRowIndex
        val maxSearchRow = 50 
        while (searchRowIndex < maxSearchRow) {
            val tempRow = sheet.getRow(searchRowIndex)
            val tempCell = tempRow?.getCell(0)
            if (tempCell == null || tempCell.cellType == CellType.BLANK) {
                endRowIndex = searchRowIndex
                break
            }
            searchRowIndex++
        }
        var bomProcessRequest = ImportBomProcessRequest(
            bom = bom
        )
        while (startRowIndex != endRowIndex || startColumnIndex != endColumnIndex) {
            val tempRow = sheet.getRow(startRowIndex)
            val tempCell = tempRow.getCell(startColumnIndex)
//            val checkCell = tempRow.getCell(0)
            if (startColumnIndex == 0 && (tempCell == null || tempCell.cellType == CellType.BLANK)) {
//                println("hi")
                break
            } else {
//                println(startRowIndex)
//                println(startColumnIndex)
//                println(tempCell.cellType)
//                println(tempCell.toString())
                try {
                    when (startColumnIndex) {
                        0 -> {
//                            println("startRowIndex: $startRowIndex")
                            bomProcessRequest.seqNo = tempCell.numericCellValue.toLong()
//                            println("seqNo: ${tempCell.numericCellValue.toLong()}")
//                            println("bomProcessRequest: $bomProcessRequest")
                        }

                        1 -> {
                            val processName = tempCell.stringCellValue.trim()
                            val process = bomGetOrCreateProcess(processName)
                            bomProcessRequest.process = process
//                            println("process created")
                        }
                        2 -> {
                            val description = tempCell.stringCellValue.trim()
                            bomProcessRequest.description = description
                        }
                        3 -> {
                            val equipmentName = tempCell.stringCellValue.trim()
                            if (equipmentName != "不適用") {
                                val equipment = bomGetOrCreateEquipment(equipmentName)
//                                println("equipment created")
                                bomProcessRequest.equipment = equipment
                            }
                        }
                        5 -> { //duration
                            when (tempCell.cellType) {
                                CellType.NUMERIC -> {
                                    bomProcessRequest.durationInMinute = tempCell.numericCellValue.toInt()
                                }
                                CellType.STRING -> {
                                    val durationString = tempCell.stringCellValue.trim()
                                    bomProcessRequest.durationInMinute = extractDurationStringToMinutes(durationString)
                                }

                                else -> {}
                            }

                        }
                        /*
                        5 -> { //processOutputQty
                            val processOutputQty = tempCell.stringCellValue.trim()
                            bomProcessRequest.processOutputQty = processOutputQty

                        }
                        6 -> { //processOutputUnit
                            val processOutputUnit = tempCell.stringCellValue.trim()
                            bomProcessRequest.processOutputUnit = processOutputUnit

                        }
                        7 -> { //byProductName
                            val byProductQty = tempCell.stringCellValue.trim()
                            bomProcessRequest.byProductQty = byProductQty
                        }
                        8 -> { //byProductQty
                            val byProductUnit = tempCell.stringCellValue.trim()
                            bomProcessRequest.byProductUnit = byProductUnit
                        }

                        9 -> { //byProductUnit
                            val byProductUnit = tempCell.stringCellValue.trim()
                            bomProcessRequest.byProductUnit = byProductUnit
                        }
                    */
                        9 -> { //scrapRate
                           // val scrapRate = tempCell.stringCellValue.trim()
                           // bomProcessRequest.scrapRate = scrapRate
                        }
                        10 -> { //prepTimeInMinute
                            when (tempCell.cellType) {
                                CellType.NUMERIC -> {
                                    bomProcessRequest.prepTimeInMinute = tempCell.numericCellValue.toInt()
                                }
                                CellType.STRING -> {
                                    val prepTimeInMinuteString = tempCell.stringCellValue.trim()
                                    bomProcessRequest.prepTimeInMinute = extractDurationStringToMinutes(prepTimeInMinuteString)
                                }

                                else -> {}
                            }
                        }
                        11 -> { //postProdTimeInMinute
                            when (tempCell.cellType) {
                                CellType.NUMERIC -> {
                                    bomProcessRequest.postProdTimeInMinute = tempCell.numericCellValue.toInt()
                                }
                                CellType.STRING -> {
                                    val postProdTimeInMinuteString = tempCell.stringCellValue.trim()
                                    bomProcessRequest.postProdTimeInMinute = extractDurationStringToMinutes(postProdTimeInMinuteString)
                                }
                                else -> {}
                            }
                        }
                    }
//                    println("req:")
//                    println(bomProcessRequest)
                    // moving the loop
                    if (startColumnIndex < endColumnIndex) {
//                        println("case1")
                        startColumnIndex++
//                        println("next col startColumnIndex: $startColumnIndex")
                    } else if (startRowIndex < endRowIndex) {
//                        println("case2")
                        // when doesn't meet first condition, falls to this and go new row
                        startRowIndex++
//                        println("next row startRowIndex: $startRowIndex")
                        // do save
                        saveBomProcess(bomProcessRequest)
                        // clean up
                        startColumnIndex = 0
                        bomProcessRequest = ImportBomProcessRequest(
                            bom = bom
                        )
                    }
                } catch (e: Error) {
//                    throw e
                }
            }
        }
    }

    /**
     * Calculates baseScore from BOM basic info using bom_weighting_score (code = BOM column name).
     * For isDark, isFloat, isDense, allergicSubstances, timeSequence, complexity: (extractedScore / range) * weighting * 100.
     * For equipmentConflict: weighting * 100 only.
     */
    private fun calculateBaseScore(bom: Bom): BigDecimal {
        val scale = 2
        val roundingMode = RoundingMode.HALF_UP
        var sum = BigDecimal.ZERO.setScale(scale, roundingMode)

        // Score columns: contribution = (extractedScore / range) * weighting * 100
        val scoreColumns = listOf(
            "isDark" to (bom.isDark?.toBigDecimal() ?: BigDecimal.ZERO),
            "isFloat" to (bom.isFloat?.toBigDecimal() ?: BigDecimal.ZERO),
            "isDense" to (bom.isDense?.toBigDecimal() ?: BigDecimal.ZERO),
            "allergicSubstances" to (bom.allergicSubstances?.toBigDecimal() ?: BigDecimal.ZERO),
            "timeSequence" to (bom.timeSequence?.toBigDecimal() ?: BigDecimal.ZERO),
            "complexity" to (bom.complexity?.toBigDecimal() ?: BigDecimal.ZERO),
        )
        for ((code, extractedScore) in scoreColumns) {
            val row = bomWeightingScoreRepository.findByCodeAndDeletedFalse(code) ?: continue
            val range = (row.range ?: 1).toBigDecimal()
            val weighting = row.weighting ?: continue
            if (range.compareTo(BigDecimal.ZERO) == 0) continue
            val contribution = extractedScore
                .divide(range, scale, roundingMode)
                .multiply(weighting)
                .multiply(BigDecimal(100))
                .setScale(scale, roundingMode)
            sum = sum.add(contribution)
        }

        // equipmentConflict: contribution = weighting * 100 only
        val equipmentConflictRow = bomWeightingScoreRepository.findByCodeAndDeletedFalse("equipmentConflict")
        if (equipmentConflictRow?.weighting != null) {
            val contribution = equipmentConflictRow.weighting!!
                .multiply(BigDecimal(100))
                .setScale(scale, roundingMode)
            sum = sum.add(contribution)
        }

        return sum.setScale(scale, roundingMode)
    }
    open fun exportProblematicBOM() {
        val resolver = PathMatchingResourcePatternResolver()
        val successExcel = resolver.getResources("file:C:/Users/2Fi/Desktop/fail/*.xlsx")
        val failedExcels = resolver.getResources("file:C:/Users/2Fi/Desktop/fail/*.xlsx")



    }
    /** Reads BOM code (編號) from sheet without saving. */
    private fun readBomCodeFromSheet(sheet: Sheet): String? {
        for (r in 0..9) {
            for (c in 0..9) {
                val cell = sheet.getRow(r)?.getCell(c) ?: continue
                if (cell.cellType != CellType.STRING) continue
                if (cell.stringCellValue.trim() != "編號") continue
                val valueRow = sheet.getRow(r + 1) ?: return null
                val valueCell = valueRow.getCell(c) ?: return null
                return when {
                    valueCell.cellType == CellType.STRING -> valueCell.stringCellValue.trim().takeIf { it.isNotEmpty() }
                    valueCell.cellType == CellType.FORMULA && valueCell.cachedFormulaResultType == CellType.STRING -> valueCell.stringCellValue.trim().takeIf { it.isNotEmpty() }
                    else -> null
                }
            }
        }
        return null
    }

    private fun softDeleteBomAndRelated(bomId: Long) {
        val bom = bomRepository.findById(bomId).orElse(null) ?: return
        bom.deleted = true
        bomRepository.saveAndFlush(bom)
        bomMaterialRepository.findAllByBomIdAndDeletedIsFalse(bomId).forEach { m ->
            m.deleted = true
            bomMaterialRepository.saveAndFlush(m)
        }
        val processes = bomProcessRepository.findByBomId(bomId)
        val processIds = processes.mapNotNull { it.id }
        if (processIds.isNotEmpty()) {
            bomProcessMaterialRepository.findByBomProcess_IdIn(processIds).forEach { pm ->
                pm.deleted = true
                bomProcessMaterialRepository.saveAndFlush(pm)
            }
        }
        processes.forEach { p ->
            p.deleted = true
            bomProcessRepository.saveAndFlush(p)
        }
    }
    private fun updateJobOrderAndProductProcessBomReference(oldBomId: Long, newBomId: Long) {
        val newBom = bomRepository.findById(newBomId).orElse(null) ?: return
        jobOrderRepository.findByBom_Id(oldBomId).forEach { jo ->
            jo.bom = newBom
            jobOrderRepository.saveAndFlush(jo)
        }
        productProcessRepository.findByBom_Id(oldBomId).forEach { pp ->
            pp.bom = newBom
            productProcessRepository.saveAndFlush(pp)
        }
    }
    open fun importBOM(batchId: String, items: List<ImportBomItemRequest>): ByteArray {
        bomMaterialImportIssues.clear()
        val batchDir = getBatchDir(batchId)
        if (!Files.isDirectory(batchDir)) return ByteArray(0)
        //val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx")
        //val templateInputStream = logExcel.inputStream
        //val workbook: Workbook = XSSFWorkbook(templateInputStream)
        //val logSheet: Sheet = workbook.getSheetAt(0)
        val itemFileNames = items.map { it.fileName }.toSet()
        Files.list(batchDir).use { stream ->
            stream.filter { p -> p.toString().lowercase().endsWith(".xlsx") }
                .filter { itemFileNames.contains(it.fileName.toString()) }
                .forEach { path ->
                    val filename = path.fileName.toString()
                        val isAlsoWip = items.find { it.fileName == filename }?.isAlsoWip == true
                    val isDrink= items.find { it.fileName == filename }?.isDrink == true
                    println("importBOM: filename='$filename', isAlsoWip=$isAlsoWip")
                    try {
                        FileInputStream(path.toFile()).use { input ->
                            val workbook2: Workbook = XSSFWorkbook(input)
                            val sheet = workbook2.getSheet("食物成品 ")
                            ?: workbook2.getSheet("食物成品")
                            ?: workbook2.getSheetAt(0)
                            val code = readBomCodeFromSheet(sheet)
                            var oldBomId: Long? = null
                            code?.let { c ->
                                bomRepository.findByCodeAndDeletedIsFalse(c)?.id?.let { existingId ->
                                    softDeleteBomAndRelated(existingId)
                                    oldBomId = existingId
                                }
                            }
                            val bom = importExcelBomBasicInfo(sheet)
                            bom.isDrink = isDrink
                            bomRepository.saveAndFlush(bom)
                            importExcelBomProcess(bom, sheet)
                            importExcelBomMaterial(bom, sheet)
                            if (isAlsoWip) {
                                createWipCopyFromFgBom(bom)
                            }
                            oldBomId?.let { updateJobOrderAndProductProcessBomReference(it, bom.id!!) }
                            workbook2.close()
                        }
                    } catch (e: Exception) {
                        println("错误：处理文件 '$filename' 时发生异常: ${e.message}")
                    }
                }
        }
        printBomMaterialImportIssues()
        val outputStream = ByteArrayOutputStream()
        //workbook.write(outputStream)
        //workbook.close()
        try {
            Files.walk(batchDir).use { stream -> stream.sorted(Comparator.reverseOrder()).forEach { Files.deleteIfExists(it) } }
        } catch (_: Exception) { }
        return outputStream.toByteArray()
    }

    private fun createWipBomFromFg(fgBom: Bom) {
        val wipCode = "${fgBom.code}-WIP"
        val wipItem = itemsRepository.findByCodeAndDeletedFalse(wipCode)
        ?: itemsRepository.saveAndFlush(Items().apply {
            code = wipCode
            name = fgBom.name
            description = fgBom.description
            type = ItemType.SFG.type
            m18LastModifyDate = LocalDateTime.now()
        })
        val wipBom = Bom().apply {
            code = wipCode
            name = fgBom.name
            description = fgBom.description
            item = wipItem
            outputQty = fgBom.outputQty
            outputQtyUom = fgBom.outputQtyUom
            yield = fgBom.yield
            isDark = fgBom.isDark
            isFloat = fgBom.isFloat
            isDense = fgBom.isDense
            scrapRate = fgBom.scrapRate
            timeSequence = fgBom.timeSequence
            complexity = fgBom.complexity
            allergicSubstances = fgBom.allergicSubstances
            uom = fgBom.uom
            isDrink = fgBom.isDrink
        }
        wipBom.baseScore = calculateBaseScore(wipBom)
        bomRepository.saveAndFlush(wipBom)
    }
        /** 方案 A：複製 FG BOM 為一筆相同 code、相同 item、description=WIP 的 BOM，並複製 materials 與 processes。 */
    private fun createWipCopyFromFgBom(fgBom: Bom) {
        val wipBom = Bom().apply {
            code = fgBom.code
            name = fgBom.name
            description = "WIP"
            item = fgBom.item
            outputQty = fgBom.outputQty
            outputQtyUom = fgBom.outputQtyUom
            yield = fgBom.yield
            isDark = fgBom.isDark
            isFloat = fgBom.isFloat
            isDense = fgBom.isDense
            scrapRate = fgBom.scrapRate
            timeSequence = fgBom.timeSequence
            complexity = fgBom.complexity
            allergicSubstances = fgBom.allergicSubstances
            uom = fgBom.uom
            isDrink = fgBom.isDrink
        }
        wipBom.baseScore = calculateBaseScore(wipBom)
        bomRepository.saveAndFlush(wipBom)

        val fgBomId = fgBom.id ?: return
        val oldMaterials = bomMaterialRepository.findAllByBomIdAndDeletedIsFalse(fgBomId)
        val oldToNewMaterial = mutableMapOf<Long, BomMaterial>()
        for (m in oldMaterials) {
            val newM = BomMaterial().apply {
                item = m.item
                itemName = m.itemName
                isConsumable = m.isConsumable
                qty = m.qty
                uom = m.uom
                uomName = m.uomName
                saleQty = m.saleQty
                salesUnit = m.salesUnit
                salesUnitCode = m.salesUnitCode
                baseQty = m.baseQty
                baseUnit = m.baseUnit
                baseUnitName = m.baseUnitName
                stockQty = m.stockQty
                stockUnit = m.stockUnit
                stockUnitName = m.stockUnitName
                bom = wipBom
                m18Id = m.m18Id
                m18LastModifyDate = m.m18LastModifyDate ?: LocalDateTime.now()
                remarks = m.remarks
            }
            bomMaterialRepository.saveAndFlush(newM)
            m.id?.let { oldToNewMaterial[it] = newM }
        }

        val oldProcesses = bomProcessRepository.findAllByBomIdAndDeletedFalse(fgBomId)
        val oldToNewProcess = mutableMapOf<Long, BomProcess>()
        for (p in oldProcesses) {
            val newP = BomProcess().apply {
                process = p.process
                equipment = p.equipment
                description = p.description
                seqNo = p.seqNo
                durationInMinute = p.durationInMinute
                prepTimeInMinute = p.prepTimeInMinute
                postProdTimeInMinute = p.postProdTimeInMinute
                bom = wipBom
            }
            bomProcessRepository.saveAndFlush(newP)
            p.id?.let { oldToNewProcess[it] = newP }
        }

        val oldProcessIds = oldProcesses.mapNotNull { it.id }
        if (oldProcessIds.isNotEmpty()) {
            val processMaterials = bomProcessMaterialRepository.findByBomProcess_IdIn(oldProcessIds)
            for (pm in processMaterials) {
                val newProcess = pm.bomProcess?.id?.let { oldToNewProcess[it] } ?: continue
                val oldMatId = pm.bomMaterial?.id ?: continue
                val newMaterial = oldToNewMaterial[oldMatId] ?: continue
                val newPm = BomProcessMaterial().apply {
                    bomProcess = newProcess
                    bomMaterial = newMaterial
                }
                bomProcessMaterialRepository.saveAndFlush(newPm)
            }
        }
    }
    private fun printBomMaterialImportIssues() {
        println("\n========== BOM Material UOM Conversion Issues ==========")
        println("Total issues: ${bomMaterialImportIssues.size}")
        if (bomMaterialImportIssues.isEmpty()) {
            println("No issues.")
            println("========================================================\n")
            return
        }

        // 按 BOM 分组统计
        val issuesByBom = bomMaterialImportIssues.groupBy { 
            it.bomCode?.takeIf { it.isNotEmpty() } ?: "BOM-id-${it.bomId}" 
        }
        
        println("\n按 BOM 分组统计:")
        issuesByBom.forEach { (bomKey, issues) ->
            println("  $bomKey: ${issues.size} 个问题")
        }
        
        println("\n详细问题列表:")
        bomMaterialImportIssues
            .sortedWith(compareBy({ it.bomCode ?: "BOM-id-${it.bomId}" }, { it.itemCode }))
            .forEach { issue ->
                val bomIdentifier = issue.bomCode?.takeIf { it.isNotEmpty() } 
                    ?: "BOM-id-${issue.bomId}"
                println(
                    "BOM=$bomIdentifier, " +
                            "Item=${issue.itemCode ?: "null"} (${issue.itemName ?: "null"}), " +
                            "src=${issue.srcQty} ${issue.srcUomCode} -> reason=${issue.reason}"
                )
            }

        println("========================================================\n")
    }
    fun loadBomFormatIssueLog(batchId: String, issueLogFileId: String): Path? {
        val dir = getBatchDir(batchId)
        val path = dir.resolve("bom_format_issue_$issueLogFileId.xlsx")
        return if (Files.exists(path)) path else null
    }
    open fun checkBomExcelFormat(batchId: String): BomFormatCheckResponse {
        val batchDir = getBatchDir(batchId)
        if (!Files.isDirectory(batchDir)) return BomFormatCheckResponse(correctFileNames = emptyList(), failList = emptyList())
        val issues = mutableListOf<BomFormatIssue>()
        Files.list(batchDir).use { stream ->
            stream.filter { p -> p.toString().lowercase().endsWith(".xlsx") }.forEach { path ->
                val fileName = path.fileName.toString()
                try {
                    FileInputStream(path.toFile()).use { input ->
                        val workbook: Workbook = XSSFWorkbook(input)
                        val sheet = workbook.getSheet("食物成品 ")
                            ?: workbook.getSheet("食物成品")
                            ?: workbook.getSheetAt(0)   // 這裡不需要再接 elvis，因為不會是 null
                        validateBasicInfoSection(fileName, sheet, issues)
                        validateProcessLikeImport(sheet, fileName, issues)
                        validateMaterialLikeImport(sheet, fileName, issues)
                        workbook.close()
                    }
                } catch (e: org.apache.poi.openxml4j.exceptions.NotOfficeXmlFileException) {
                    issues += BomFormatIssue(fileName, "檔案不是有效的 Excel 檔")
                } catch (e: java.util.zip.ZipException) {
                    issues += BomFormatIssue(fileName, "檔案格式錯誤或損毀")
                } catch (e: Exception) {
                    issues += BomFormatIssue(
                        fileName,
                        "檢查時發生例外：${e.javaClass.simpleName} - ${e.message ?: "Unknown error"}"
                    )
                }
            }
        }
    
        val allFileNames = Files.list(batchDir).use { stream ->
            stream
                .filter { p -> p.toString().lowercase().endsWith(".xlsx") }
                .map { p -> p.fileName.toString() }
                .sorted()
                .toList()
        }
    
        val failList = issues
            .groupBy { it.fileName }
            .map { (fileName, list) ->
                BomFormatFileGroup(
                    fileName = fileName,
                    problems = list.map { it.problem }.distinct().sorted()
                )
            }
            .sortedBy { it.fileName }
    
            val logWb: Workbook = XSSFWorkbook()
            val logSheet: Sheet = logWb.createSheet("Issues")
            
            // 建一行表頭
            val header = logSheet.createRow(0)
            header.createCell(0).setCellValue("File Name")
            header.createCell(1).setCellValue("Problem")
            
            // 從第 2 列開始寫問題
            var rowIdx = 1
            issues.sortedWith(compareBy({ it.fileName }, { it.problem })).forEach { issue ->
                val row = logSheet.createRow(rowIdx)
                row.createCell(0).setCellValue(issue.fileName)
                row.createCell(1).setCellValue(issue.problem)
                rowIdx++
            }
    
        val issueLogId = UUID.randomUUID().toString()
        println("issueLogId: $issueLogId")
        val outPath = batchDir.resolve("bom_format_issue_$issueLogId.xlsx")
        Files.newOutputStream(outPath).use { os -> logWb.write(os) }
        logWb.close()
    
        val filesWithIssues = issues.asSequence().map { it.fileName }.toSet()
        val correctFileNames = allFileNames.filter { it !in filesWithIssues }
    
        return BomFormatCheckResponse(
            correctFileNames = correctFileNames,
            failList = failList,
            issueLogFileId = issueLogId
        )
    }
    private fun isValidEquipmentType(value: String): Boolean {
        val trimmed = value.trim()
        if (trimmed.isEmpty()) return false
        if (trimmed == "不合用" || trimmed == "不適用") return true
        if (trimmed.contains(",")) return false  // 新增：不允許逗號
        val regex = Regex("^[^-/]+-[^-/]+$")  // 例：工具類-切絲機
        return regex.matches(trimmed)
    }

    /** Cell 是否為非空字串（含 STRING / FORMULA 結果為字串） */
    private fun isNonEmptyStringCell(cell: org.apache.poi.ss.usermodel.Cell?): Boolean {
        if (cell == null || cell.cellType == CellType.BLANK) return false
        return when (cell.cellType) {
            CellType.STRING -> cell.stringCellValue.trim().isNotEmpty()
            CellType.FORMULA -> when (cell.cachedFormulaResultType) {
                CellType.STRING -> cell.stringCellValue.trim().isNotEmpty()
                else -> false
            }
            else -> false
        }
    }
    private fun validateProcessLikeImport(
        sheet: Sheet,
        fileName: String,
        issues: MutableList<BomFormatIssue>
    ) {
        val byProductItemCache = mutableMapOf<String, Boolean>()
        val uomCache = mutableMapOf<String, Boolean>()
        val itemStockUomCache = mutableMapOf<Long, Long?>()
        var startRowIndex = 30
        var endRowIndex = 70
        var startColumnIndex = 0
        val endColumnIndex = 11
    
        // 1) 找到 "工序" header
        var headerFound = false
        while (startRowIndex < endRowIndex) {
            val tempRow = sheet.getRow(startRowIndex)
            val tempCell = tempRow?.getCell(startColumnIndex)
            if (tempCell != null &&
                tempCell.cellType == CellType.STRING &&
                tempCell.stringCellValue.trim() == "工序"
            ) {
                headerFound = true
                startRowIndex += 2
                break
            }
            startRowIndex++
        }
    
        if (!headerFound) {
            issues += BomFormatIssue(fileName, "工序區：找不到『工序』表頭")
            return
        }
    
        var searchRowIndex = startRowIndex
        val maxSearchRow = 50
        while (searchRowIndex < maxSearchRow) {
            val tempRow = sheet.getRow(searchRowIndex)
            val tempCell = tempRow?.getCell(0)
            if (tempCell == null || tempCell.cellType == CellType.BLANK) {
                endRowIndex = searchRowIndex
                break
            }
            searchRowIndex++
        }
    
        while (startRowIndex != endRowIndex || startColumnIndex != endColumnIndex) {
            val tempRow = sheet.getRow(startRowIndex)
            val tempCell = tempRow?.getCell(startColumnIndex)
    
            if (startColumnIndex == 0 &&
                (tempCell == null || tempCell.cellType == CellType.BLANK)
            ) {
                break
            } else if (tempCell != null) {
                val rowNum = startRowIndex + 1
                when (startColumnIndex) {
                    0 -> {
                        // Process Sequence — 必填，數值
                        if (!isNumericLike(tempCell)) {
                            issues += BomFormatIssue(fileName, "工序區：第${rowNum}行『編號(次序)』不可為空且須為數值")
                        }
                    }
                    1 -> {
                        // Process Name — 必填，非空字串
                        if (!isNonEmptyStringCell(tempCell)) {
                            issues += BomFormatIssue(fileName, "工序區：第${rowNum}行『名稱』不可為空")
                        }
                    }
                    2 -> {
                        // Process Brief Description — 必填，非空字串
                        if (!isNonEmptyStringCell(tempCell)) {
                            issues += BomFormatIssue(fileName, "工序區：第${rowNum}行『描述』不可為空")
                        }
                    }
                    3 -> {
                        // Equipment Type — 必填，格式 不合用/不適用/XXXXX-YYYY
                        val str = when (tempCell.cellType) {
                            CellType.STRING -> tempCell.stringCellValue.trim()
                            CellType.FORMULA -> if (tempCell.cachedFormulaResultType == CellType.STRING) tempCell.stringCellValue.trim() else ""
                            else -> ""
                        }
                        if (str.isEmpty()) {
                            issues += BomFormatIssue(fileName, "工序區：第${rowNum}行『使用設備』不可為空")
                        } else if (!isValidEquipmentType(str)) {
                            issues += BomFormatIssue(
                                fileName,
                                "工序區：第${rowNum}行『使用設備』格式須為 不合用、不適用 或 XXX-YYY（只允許一個 '-'，不含 '/'）"
                            )
                        }
                    }
                    5 -> {
                        // 步驟時間 (mins) — 必填，數值
                        if (!isNumericLike(tempCell)) {
                            issues += BomFormatIssue(fileName, "工序區：第${rowNum}行『步驟時間(mins)』不可為空且須為數值")
                        }
                    }
                    // 4, 6~11 可 null，不檢查
                    8 -> {
                        val rowNum = startRowIndex + 1
                
                        // 1) 如果 cell 為 null 或 BLANK，直接當作空，完全不查 DB
                        if (tempCell == null || tempCell.cellType == CellType.BLANK) {
                            // 允許空，不新增 issue，不查 DB
                        } else if (tempCell.cellType == CellType.STRING) {
                            // 2) 只有 STRING 類型才處理，FORMULA 一律當作沒填，避免奇怪情況
                            val fullName = tempCell.stringCellValue.trim()
                
                            // 3) 去掉前後空白後，長度不足 6 直接略過，不查 DB
                            if (fullName.length >= 6) {
                                val byProductItemCode = fullName.take(6)
                
                                // 4) 先查快取，避免重複 hit DB
                                val exists = byProductItemCache.getOrPut(byProductItemCode) {
                                    val item = itemsRepository.findByCodeAndDeletedFalse(byProductItemCode)
                                    item != null
                                }
                
                                if (!exists) {
                                    issues += BomFormatIssue(
                                        fileName,
                                        "工序區：第${rowNum}行『副產品名稱』($fullName) 對應品項($byProductItemCode) 在 Items 資料表找不到"
                                    )
                                }
                            }
                            // fullName.length < 6 就當作「沒填有效副產品」，不檢查、不報錯
                        } else {
                            // 其他型別 (NUMERIC / FORMULA 等) 先全部當作「沒填」，不檢查
                        }
                    }
                    10 -> {
                        val rowNum = startRowIndex + 1
                    
                        // 1) 副產品單位這格本身為 null / BLANK：當作沒填，直接跳過
                        if (tempCell == null || tempCell.cellType == CellType.BLANK) {
                            // 允許空，不查 DB
                        } else if (tempCell.cellType == CellType.STRING) {
                            val uomCode = tempCell.stringCellValue.trim()
                    
                            
                    
                            // 2) 從同一列第 8 欄拿副產品名稱，推 itemCode（前 6 碼）
                            val byProductNameCell = tempRow?.getCell(8)
                            val byProductName = if (
                                byProductNameCell != null &&
                                byProductNameCell.cellType == CellType.STRING
                            ) {
                                byProductNameCell.stringCellValue.trim()
                            } else {
                                ""
                            }
                            val byProductItemCode =
                                if (byProductName.length >= 6) byProductName.take(6) else ""
                    
                            // 如果連 itemCode 都湊不出來，就只檢查 UOM 是否存在，不做 stock unit 檢查
                            val itemExists: Boolean
                            val itemId: Long?
                    
                            if (byProductItemCode.isNotEmpty()) {
                                itemExists = byProductItemCache.getOrPut(byProductItemCode) {
                                    val item = itemsRepository.findByCodeAndDeletedFalse(byProductItemCode)
                                    if (item != null) {
                                        itemStockUomCache[item.id!!] = null   // 先佔一個 key，等會兒查 stockUom
                                        true
                                    } else {
                                        false
                                    }
                                }
                    
                                itemId = if (itemExists) {
                                    // 再查一次拿 itemId（也可以把整個 item 快取起來，看你現有結構）
                                    itemsRepository.findByCodeAndDeletedFalse(byProductItemCode)?.id
                                } else null
                    
                                if (!itemExists) {
                                    issues += BomFormatIssue(
                                        fileName,
                                        "工序區：第${rowNum}行 副產品品項($byProductItemCode) 在 Items 資料表找不到"
                                    )
                                }
                            } else {
                                itemExists = false
                                itemId = null
                            }
                    
                            // 3) 檢查 UOM 是否存在（用快取）
                            val uomExists = uomCache.getOrPut(uomCode) {
                                uomConversionRepository.findByCodeAndDeletedFalse(uomCode) != null
                            }
                            if (!uomExists) {
                                issues += BomFormatIssue(
                                    fileName,
                                    "工序區：第${rowNum}行『副產品單位』($uomCode) 在 UOM 資料表找不到"
                                )
                            }
                    
                            // 4) 如果 item 與 uom 都存在，再做「是不是 stock unit」檢查
                            if (itemId != null && uomExists) {
                                val stockUomId = itemStockUomCache.getOrPut(itemId) {
                                    val stockItemUom = itemUomService.findStockUnitByItemId(itemId)
                                    stockItemUom?.uom?.id   // 可能為 null
                                }
                    
                                if (stockUomId == null) {
                                    issues += BomFormatIssue(
                                        fileName,
                                        "工序區：第${rowNum}行 副產品品項($byProductItemCode) 未設定 Stock Unit"
                                    )
                                } else {
                                    // 再查一次 uom 拿 id 比對（也可以在上面快取 uomId）
                                    val uom = uomConversionRepository.findByCodeAndDeletedFalse(uomCode)
                                    if (uom == null || uom.id != stockUomId) {
                                        issues += BomFormatIssue(
                                            fileName,
                                            "工序區：第${rowNum}行 副產品單位($uomCode) 並非品項($byProductItemCode) 的 Stock Unit"
                                        )
                                    }
                                }
                            }
                        } else {
                            // 非 STRING（NUMERIC / FORMULA 等）暫時都當作沒填，避免怪型別拖慢
                        }
                    }
                    else -> { }
                }
            }
    
            if (startColumnIndex < endColumnIndex) {
                startColumnIndex++
            } else if (startRowIndex < endRowIndex) {
                startRowIndex++
                startColumnIndex = 0
            }
        }
    }

    private fun validateMaterialLikeImport(
        sheet: Sheet,
        fileName: String,
        issues: MutableList<BomFormatIssue>
    ) {
        val startCol = 0
        val endCol = 10
        val maxRowIndex = 200
    
        // 1) 找表头
        var headerRowIndex = -1
        var searchRowIndex = 10
        while (searchRowIndex < maxRowIndex) {
            val row = sheet.getRow(searchRowIndex)
            val cell = row?.getCell(startCol)
            if (cell != null &&
                cell.cellType == CellType.STRING &&
                cell.stringCellValue.trim() == "材料編號"
            ) {
                headerRowIndex = searchRowIndex
                break
            }
            searchRowIndex++
        }
    
        if (headerRowIndex == -1) {
            issues += BomFormatIssue(fileName, "材料區：找不到『材料編號』表頭")
            return
        }
    
        // 2) 从表头下一行开始，读到 col0 空白
        var bomMatRowIdx = headerRowIndex + 1
        while (bomMatRowIdx < maxRowIndex) {
            val row = sheet.getRow(bomMatRowIdx) ?: break
            val firstCell = row.getCell(0)
            if (firstCell == null || firstCell.cellType == CellType.BLANK) {
                break
            }
    
            for (startColumnIndex in 0..endCol) {
                val tempCell = row.getCell(startColumnIndex)
                val rowNum = bomMatRowIdx + 1
    
                when (startColumnIndex) {
                    0 -> {
                        if (tempCell == null || !isNonEmptyStringCell(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『材料編號』(欄1)不可為空")
                        }
                    }
                    1 -> {
                        if (tempCell == null || !isNonEmptyStringCell(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『材料』(欄2)不可為空")
                        }
                    }
                    2 -> {
                        if (tempCell == null || !isNumericLike(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『使用份量』(欄3)不可為空且須為數值")
                        }
                    }
                    3 -> {
                        if (tempCell == null || !isNonEmptyStringCell(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『使用單位』(欄4)不可為空")
                        }
                    }
                    4 -> {
                        if (tempCell == null || !isNumericLike(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『轉用單位份量』(欄5)不可為空且須為數值")
                        }
                    }
                    5 -> {
                        if (tempCell == null || !isNonEmptyStringCell(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『轉用單位』(欄6)不可為空")
                        }
                    }
                    6 -> {
                        if (tempCell == null || !isNumericLike(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『份量(銷售單位)』(欄7)不可為空且須為數值")
                        }
                    }
                    7 -> {
                        if (tempCell == null || !isNonEmptyStringCell(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『銷售單位』(欄8)不可為空")
                        }
                    }
                    8 -> {
                        if (tempCell == null || !isNumericLike(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『採購單價』(欄9)不可為空且須為數值")
                        }
                    }
                    9 -> {
                        if (tempCell == null || !isNonEmptyStringCell(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『採購單位』(欄10)不可為空")
                        }
                    }
                    10 -> {
                        if (tempCell == null || !isNumericLike(tempCell)) {
                            issues += BomFormatIssue(fileName, "材料區：第${rowNum}行『加入步驟』(欄11)不可為空且須為數值")
                        }
                    }
                }
            }
    
            // DB / 轉換檢查（保留你原本逻辑）
            val codeCell = row.getCell(0)
            val uomCell = row.getCell(3)
            val saleQtyCell = row.getCell(6)
            val salesUnitCell = row.getCell(7)
            val rowNum = bomMatRowIdx + 1
    
            val itemCode = codeCell?.stringCellValue?.trim().orEmpty()
            if (itemCode.isNotEmpty()) {
                val item = itemsRepository.findByCodeAndDeletedFalse(itemCode)
                if (item == null) {
                    issues += BomFormatIssue(
                        fileName,
                        "材料區：第${rowNum}行『材料編號』($itemCode) 在 Items 資料表找不到"
                    )
                }
            }
    
            val useUomCode = uomCell?.stringCellValue?.trim().orEmpty()
            if (useUomCode.isNotEmpty()) {
                val useUom = uomConversionRepository.findByCodeAndDeletedFalse(useUomCode)
                if (useUom == null) {
                    issues += BomFormatIssue(
                        fileName,
                        "材料區：第${rowNum}行『使用單位』($useUomCode) 在 UOM 資料表找不到"
                    )
                }
            }
    
            // 下面 sales unit / convert 檢查，直接沿用你原本 2136 行後的邏輯即可
            // （你可把原区块整段搬进来，row / rowNum 变量名一致即可）
    
            bomMatRowIdx++
        }
    }
    // ===================== 新增：Basic Info 區塊檢查 =====================
    
    /**
     * 取得 basic info 對應 value 的 cell：
     * 1. 先看「同一列右邊一格」
     * 2. 若沒有，再看「下一列同一欄」
     */
    private fun getBasicInfoValueCell(
        sheet: Sheet,
        rowIdx: Int,
        colIdx: Int
    ): org.apache.poi.ss.usermodel.Cell? {
        val row = sheet.getRow(rowIdx)
        val rightCell = row?.getCell(colIdx + 1)
        if (rightCell != null && rightCell.cellType != CellType.BLANK) {
            return rightCell
        }
    
        val belowRow = sheet.getRow(rowIdx + 1)
        val belowCell = belowRow?.getCell(colIdx)
        if (belowCell != null && belowCell.cellType != CellType.BLANK) {
            return belowCell
        }
    
        return null
    }
    
    /**
     * 檢查基本資料區（編號 / 產品名稱 / 份量 / 單位）
     * 版面支援：「標題在左、值在右」或「標題在上、值在下」。
     */
    
    private fun validateBasicInfoSection(
    fileName: String,
    sheet: Sheet,
    issues: MutableList<BomFormatIssue>
) {
    // 留一點 buffer，把 A11 甚至再下面幾行都掃進來
    val maxRow = 15
    val maxCol = 15

    var codeHeaderFound = false
    var nameHeaderFound = false
    var qtyHeaderFound = false
    var uomHeaderFound = false
    var timeSeqHeaderFound = false
    var complexityHeaderFound = false
    var AllergicSubstancesHeaderFound = false
    var ColorDepthHeaderFound = false
    var FloatingHeaderFound = false
    var ConcentrationHeaderFound = false

    var codeValueOk = false
    var nameValueOk = false
    var qtyValueOk = false
    var uomValueOk = false
    var timeSeqValueOk = false
    var complexityValueOk = false
    var AllergicSubstancesValueOk = false
    var ColorDepthValueOk = false
    var FloatingValueOk = false
    var ConcentrationValueOk = false
    println("=== Debug sheet content for $fileName ===")
for (r in 0..20) {
    val row = sheet.getRow(r) ?: continue
    for (c in 0..20) {
        val cell = row.getCell(c) ?: continue
        val value = when {
            cell.cellType == CellType.STRING -> "'${cell.stringCellValue}'"
            cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toString()
            cell.cellType == CellType.FORMULA -> {
                val cached = when (cell.cachedFormulaResultType) {
                    CellType.STRING -> "'${cell.stringCellValue}'"
                    CellType.NUMERIC -> cell.numericCellValue.toString()
                    else -> "?"
                }
                "FORMULA -> $cached"
            }
            else -> cell.cellType.toString()
        }
        if (value.isNotBlank() && value != "BLANK") {
            println("($r, $c) = $value")
        }
    }
}
println("=====================================")
    for (rowIdx in 0 until maxRow) {
        val row = sheet.getRow(rowIdx) ?: continue
        for (colIdx in 0 until maxCol) {
            val cell = row.getCell(colIdx) ?: continue
            if (cell.cellType != CellType.STRING) continue

            val text = cell.stringCellValue.trim()

            when {
                text == "編號" -> {
                    codeHeaderFound = true
                    val codeCell = sheet.getRow(1)?.getCell(0)
                    if (codeCell == null || codeCell.cellType == CellType.BLANK) {
                        issues += BomFormatIssue(fileName, "基本資料：『編號』欄位缺值或型別錯誤")
                    } else {
                        val v = when (codeCell.cellType) {
                            CellType.STRING -> codeCell.stringCellValue.trim()
                            CellType.FORMULA ->
                                if (codeCell.cachedFormulaResultType == CellType.STRING)
                                    codeCell.stringCellValue.trim()
                                else ""

                            else -> ""
                        }
                        if (v.isEmpty()) {
                            issues += BomFormatIssue(fileName, "基本資料：『編號』欄位缺值或型別錯誤")
                        } else {
                            codeValueOk = true
                        }
                    }
                }

                text == "產品名稱" -> {
                    nameHeaderFound = true
                    val r5Cell = sheet.getRow(4)?.getCell(17)
                    if (r5Cell == null || r5Cell.cellType == CellType.BLANK) {
                        issues += BomFormatIssue(fileName, "基本資料：『產品名稱』欄位缺值或型別錯誤")
                    } else {
                        val v = when (r5Cell.cellType) {
                            CellType.STRING -> r5Cell.stringCellValue.trim()
                            CellType.FORMULA ->
                                if (r5Cell.cachedFormulaResultType == CellType.STRING)
                                    r5Cell.stringCellValue.trim()
                                else ""

                            else -> ""
                        }
                        if (v.isEmpty()) {
                            issues += BomFormatIssue(fileName, "基本資料：『產品名稱』欄位缺值或型別錯誤")
                        } else {
                            nameValueOk = true
                        }
                    }
                }

                text == "份量 (Qty)" -> {
                    qtyHeaderFound = true
                    val valueCell = getBasicInfoValueCell(sheet, rowIdx, colIdx)
                    if (valueCell == null || valueCell.cellType != CellType.NUMERIC) {
                        issues += BomFormatIssue(fileName, "基本資料：『份量 (Qty)』應為數值")
                    } else {
                        qtyValueOk = true
                    }
                }

                text == "單位" -> {
                    uomHeaderFound = true
                    val uomCell = sheet.getRow(3)?.getCell(6)
                    if (uomCell == null || uomCell.cellType == CellType.BLANK) {
                        issues += BomFormatIssue(fileName, "基本資料：『單位』欄位缺值或型別錯誤")
                    } else {
                        val uomCode = when (uomCell.cellType) {
                            CellType.STRING -> uomCell.stringCellValue.trim()
                            CellType.FORMULA ->
                                if (uomCell.cachedFormulaResultType == CellType.STRING)
                                    uomCell.stringCellValue.trim()
                                else ""

                            else -> ""
                        }
                        if (uomCode.isEmpty()) {
                            issues += BomFormatIssue(fileName, "基本資料：『單位』欄位缺值或型別錯誤")
                        } else {
                            val uom = uomConversionRepository.findByCodeAndDeletedFalse(uomCode)
                            if (uom == null) {
                                issues += BomFormatIssue(fileName, "基本資料：『單位』($uomCode) 在 UOM 資料表中找不到")
                            } else {
                                uomValueOk = true
                            }
                        }
                    }
                }
                text == "種類" -> {
                    val cell = sheet.getRow(1)?.getCell(9)   // J2: row index=1, col index=9
                    val value = when {
                        cell == null || cell.cellType == CellType.BLANK -> ""
                        cell.cellType == CellType.STRING -> cell.stringCellValue.trim()
                        cell.cellType == CellType.FORMULA &&
                                cell.cachedFormulaResultType == CellType.STRING ->
                            cell.stringCellValue.trim()
                        else -> ""
                    }
                    if (value.isEmpty() || (value != "FG" && value != "WIP")) {
                        issues += BomFormatIssue(fileName, "基本資料：『種類』必須為 FG 或 WIP")
                    }
                }
                text.contains("過敏原") -> {
                    AllergicSubstancesHeaderFound = true
                    val valueCell = getBasicInfoValueCell(sheet, rowIdx, colIdx)
                    if (valueCell == null || valueCell.cellType == CellType.BLANK) {
                        // 允許空值，當作 0 交給 import 時處理，不報錯
                    } else if (!isNumericLike(valueCell)) {
                        issues += BomFormatIssue(fileName, "基本資料：『過敏原』應為數值 0 或 5")
                    } else {
                        val v = when (valueCell.cellType) {
                            CellType.NUMERIC -> valueCell.numericCellValue.toInt()
                            CellType.FORMULA -> valueCell.numericCellValue.toInt()
                            CellType.STRING -> valueCell.stringCellValue.trim().toIntOrNull() ?: Int.MIN_VALUE
                            else -> Int.MIN_VALUE
                        }
                        if (v !in listOf(0, 5)) {
                            issues += BomFormatIssue(fileName, "基本資料：『過敏原』應為數值 0 或 5")
                        } else {
                            AllergicSubstancesValueOk = true
                        }
                    }
                }

                text.contains("顔色深淺度") -> {
                    ColorDepthHeaderFound = true
                    val valueCell = getBasicInfoValueCell(sheet, rowIdx, colIdx)
                    if (valueCell == null || valueCell.cellType == CellType.BLANK) {
                        // 允許空
                    } else if (!isNumericLike(valueCell)) {
                        issues += BomFormatIssue(fileName, "基本資料：『顔色深淺度』必須為 1~5 的數值")
                    } else {
                        val v = when (valueCell.cellType) {
                            CellType.NUMERIC -> valueCell.numericCellValue.toInt()
                            CellType.FORMULA -> valueCell.numericCellValue.toInt()
                            CellType.STRING -> valueCell.stringCellValue.trim().toIntOrNull() ?: Int.MIN_VALUE
                            else -> Int.MIN_VALUE
                        }
                        if (v !in 1..5) {
                            issues += BomFormatIssue(fileName, "基本資料：『顔色深淺度』必須為 1~5 的數值")
                        } else {
                            ColorDepthValueOk = true
                        }
                    }
                }
            

                text.contains("浮沉") -> {
                    FloatingHeaderFound = true
                    val valueCell = getBasicInfoValueCell(sheet, rowIdx, colIdx)
                    if (valueCell == null || valueCell.cellType == CellType.BLANK) {
                        // 允許空
                    } else if (!isNumericLike(valueCell)) {
                        issues += BomFormatIssue(fileName, "基本資料：『浮沉』必須為 0、3 或 5")
                    } else {
                        val v = when (valueCell.cellType) {
                            CellType.NUMERIC -> valueCell.numericCellValue.toInt()
                            CellType.FORMULA -> valueCell.numericCellValue.toInt()
                            CellType.STRING -> valueCell.stringCellValue.trim().toIntOrNull() ?: Int.MIN_VALUE
                            else -> Int.MIN_VALUE
                        }
                        if (v !in listOf(0, 3, 5)) {
                            issues += BomFormatIssue(fileName, "基本資料：『浮沉』必須為 0、3 或 5")
                        } else {
                            FloatingValueOk = true
                        }
                    }
                }

                text.contains("濃淡程度") -> {
                    ConcentrationHeaderFound = true
                    val valueCell = getBasicInfoValueCell(sheet, rowIdx, colIdx)
                    if (valueCell == null || valueCell.cellType == CellType.BLANK) {
                        // 允許空
                    } else if (!isNumericLike(valueCell)) {
                        issues += BomFormatIssue(fileName, "基本資料：『濃淡程度』必須為 0、3 或 5")
                    } else {
                        val v = when (valueCell.cellType) {
                            CellType.NUMERIC -> valueCell.numericCellValue.toInt()
                            CellType.FORMULA -> valueCell.numericCellValue.toInt()
                            CellType.STRING -> valueCell.stringCellValue.trim().toIntOrNull() ?: Int.MIN_VALUE
                            else -> Int.MIN_VALUE
                        }
                        if (v !in listOf(0, 3, 5)) {
                            issues += BomFormatIssue(fileName, "基本資料：『濃淡程度』必須為 0、3 或 5")
                        } else {
                            ConcentrationValueOk = true
                        }
                    }
                }
                // 生產時段先後數值：文字可能有多一點說明，用 contains 比較保險
                text.contains("先後") -> {
                    timeSeqHeaderFound = true
                    val valueCell = getBasicInfoValueCell(sheet, rowIdx, colIdx)
                    if (valueCell == null || valueCell.cellType == CellType.BLANK) {
                        // 允許空
                    } else if (!isNumericLike(valueCell)) {
                        issues += BomFormatIssue(fileName, "基本資料：『生產時段先後數值』必須為 0、1 或 5")
                    } else {
                        val v = when (valueCell.cellType) {
                            CellType.NUMERIC -> valueCell.numericCellValue.toInt()
                            CellType.FORMULA -> valueCell.numericCellValue.toInt()
                            CellType.STRING -> valueCell.stringCellValue.trim().toIntOrNull() ?: Int.MIN_VALUE
                            else -> Int.MIN_VALUE
                        }
                        if (v !in listOf(0, 1, 5)) {
                            issues += BomFormatIssue(fileName, "基本資料：『生產時段先後數值』必須為 0、1 或 5")
                        } else {
                            timeSeqValueOk = true
                        }
                    }
                }

                // 生產複雜度：同樣用 contains，位置可能在 A11
                text.contains("生產複雜度") -> {
                    complexityHeaderFound = true
                    val valueCell = getBasicInfoValueCell(sheet, rowIdx, colIdx)
                    if (valueCell == null || valueCell.cellType == CellType.BLANK) {
                        // 允許空
                    } else if (!isNumericLike(valueCell)) {
                        issues += BomFormatIssue(fileName, "基本資料：『生產複雜度』必須為 0、3、5 或 10")
                    } else {
                        val v = when (valueCell.cellType) {
                            CellType.NUMERIC -> valueCell.numericCellValue.toInt()
                            CellType.FORMULA -> valueCell.numericCellValue.toInt()
                            CellType.STRING -> valueCell.stringCellValue.trim().toIntOrNull() ?: Int.MIN_VALUE
                            else -> Int.MIN_VALUE
                        }
                        if (v !in listOf(0, 3, 5, 10)) {
                            issues += BomFormatIssue(fileName, "基本資料：『生產複雜度』必須為 0、3、5 或 10")
                        } else {
                            complexityValueOk = true
                        }
                    }
                }
            }
        }
    }

    if (!codeHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『編號』欄位")
    }
    if (!nameHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『產品名稱』欄位")
    }
    if (!qtyHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『份量 (Qty)』欄位")
    }
    if (!uomHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『單位』欄位")
    }
    if (!AllergicSubstancesHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『過敏原』欄位")
    }
    if (!ColorDepthHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『顔色深淺度』欄位")
    }
    if (!FloatingHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『浮沉』欄位")
    }
    if (!ConcentrationHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『濃淡程度』欄位")
    }
    if (!timeSeqHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『生產時段先後數值』欄位")
    }
    if (!complexityHeaderFound) {
        issues += BomFormatIssue(fileName, "基本資料：找不到『生產複雜度』欄位")
    }
}
    
    
    // ===================== 新增：判斷「看起來像數字」的 helper =====================
    
    private fun isNumericLike(cell: org.apache.poi.ss.usermodel.Cell): Boolean {
        return when (cell.cellType) {
            CellType.NUMERIC -> true
            CellType.FORMULA ->
                cell.cachedFormulaResultType == CellType.NUMERIC
            CellType.STRING ->
                cell.stringCellValue.trim().toDoubleOrNull() != null
            else -> false
        }
    }
    
    // ===================== 新增：材料區檢查 =====================
    
    private fun validateMaterialSection(
        fileName: String,
        sheet: Sheet,
        issues: MutableList<BomFormatIssue>
    ) {
        // 找「材料編號」所在列（你模板左下那一段）
        var headerRowIdx = -1
        val startCol = 0
        val maxRowSearch = 40
    
        for (rowIdx in 0 until maxRowSearch) {
            val row = sheet.getRow(rowIdx) ?: continue
            val cell = row.getCell(startCol)
            if (cell != null &&
                cell.cellType == CellType.STRING &&
                cell.stringCellValue.trim().startsWith("材料編號")
            ) {
                headerRowIdx = rowIdx
                break
            }
        }
    
        if (headerRowIdx == -1) {
            issues += BomFormatIssue(fileName, "材料區：找不到『材料編號』表頭")
            return
        }
    
        var rowIdx = headerRowIdx + 1
        val endRowIdx = 200
    
        while (rowIdx < endRowIdx) {
            val row = sheet.getRow(rowIdx) ?: break
            val firstCell = row.getCell(startCol)
    
            // 第一欄空白視為材料列表結束
            if (firstCell == null || firstCell.cellType == CellType.BLANK) {
                break
            }
    
            // col0: 材料編號（字串、必填）
            val codeCell = row.getCell(0)
            if (codeCell == null ||
                codeCell.cellType != CellType.STRING ||
                codeCell.stringCellValue.trim().isEmpty()
            ) {
                issues += BomFormatIssue(
                    fileName,
                    "材料區：第${rowIdx + 1}行『材料編號』(欄 1) 應為非空字串"
                )
            }
    
            // col2: 使用份量（數值、必填）——依照你現有 import 的欄位位置
            val qtyCell = row.getCell(2)
            if (qtyCell == null || !isNumericLike(qtyCell)) {
                issues += BomFormatIssue(
                    fileName,
                    "材料區：第${rowIdx + 1}行『使用份量』(欄 3) 應為數值"
                )
            }
    
            // col3: 使用單位（字串、必填）
            val uomCell = row.getCell(3)
            if (uomCell == null ||
                uomCell.cellType != CellType.STRING ||
                uomCell.stringCellValue.trim().isEmpty()
            ) {
                issues += BomFormatIssue(
                    fileName,
                    "材料區：第${rowIdx + 1}行『使用單位』(欄 4) 應為非空字串"
                )
            }
    
            // col6: 銷售數量（若有值必須是數值）
            val saleQtyCell = row.getCell(6)
            if (saleQtyCell != null && saleQtyCell.cellType != CellType.BLANK) {
                if (!isNumericLike(saleQtyCell)) {
                    issues += BomFormatIssue(
                        fileName,
                        "材料區：第${rowIdx + 1}行『銷售數量』(欄 7) 若有值必須是數值"
                    )
                }
            }
    
            // col7: 銷售單位（若有值必須是字串）
            val salesUnitCell = row.getCell(7)
            if (salesUnitCell != null && salesUnitCell.cellType != CellType.BLANK) {
                if (salesUnitCell.cellType != CellType.STRING ||
                    salesUnitCell.stringCellValue.trim().isEmpty()
                ) {
                    issues += BomFormatIssue(
                        fileName,
                        "材料區：第${rowIdx + 1}行『銷售單位』(欄 8) 若有值必須是非空字串"
                    )
                }
            }
    
            rowIdx++
        }
    }
    
    // ===================== 新增：工序區檢查 =====================
    
    private fun validateProcessSection(
        fileName: String,
        sheet: Sheet,
        issues: MutableList<BomFormatIssue>
    ) {
        var rowIdx = 30
        val maxRowSearch = 100
        var headerRowIdx = -1
    
        // 找到「工序」表頭所在列（和你現有 importProcess 的邏輯類似）
        while (rowIdx < maxRowSearch) {
            val row = sheet.getRow(rowIdx) ?: break
            val cell = row.getCell(0)
            if (cell != null &&
                cell.cellType == CellType.STRING &&
                cell.stringCellValue.trim() == "工序"
            ) {
                headerRowIdx = rowIdx
                break
            }
            rowIdx++
        }
    
        if (headerRowIdx == -1) {
            issues += BomFormatIssue(fileName, "工序區：找不到『工序』表頭")
            return
        }
    
        // 跳過 header + 欄名一行（依你原來 import 是 +2）
        rowIdx = headerRowIdx + 2
        val endRowIdx = headerRowIdx + 70
    
        while (rowIdx < endRowIdx) {
            val row = sheet.getRow(rowIdx) ?: break
            val firstCell = row.getCell(0)
    
            if (firstCell == null || firstCell.cellType == CellType.BLANK) {
                break
            }
    
            // col0: 工序次序（數值）
            if (!isNumericLike(firstCell)) {
                issues += BomFormatIssue(
                    fileName,
                    "工序區：第${rowIdx + 1}行『工序次序』(欄 1) 應為數值"
                )
            }
    
            // col1: 工序名稱（字串）
            val processNameCell = row.getCell(1)
            if (processNameCell == null ||
                processNameCell.cellType != CellType.STRING ||
                processNameCell.stringCellValue.trim().isEmpty()
            ) {
                issues += BomFormatIssue(
                    fileName,
                    "工序區：第${rowIdx + 1}行『工序名稱』(欄 2) 應為非空字串"
                )
            }
    
            // col5: 時間（允許 numeric 或 formula 或 string; 這裡只做型別大致檢查）
            val durationCell = row.getCell(5)
            if (durationCell != null && durationCell.cellType != CellType.BLANK) {
                val ok = durationCell.cellType == CellType.NUMERIC ||
                        (durationCell.cellType == CellType.FORMULA &&
                                durationCell.cachedFormulaResultType == CellType.NUMERIC) ||
                        durationCell.cellType == CellType.STRING
                if (!ok) {
                    issues += BomFormatIssue(
                        fileName,
                        "工序區：第${rowIdx + 1}行『時間』(欄 6) 只允許數值 / 公式 / 文字格式"
                    )
                }
            }
    
            rowIdx++
        }
    }
    open fun getBomDetail(id: Long): BomDetailResponse {
        // 1) 正確處理找不到 BOM 的情況（用 NotFoundException 或你專案慣例）
        val bom = bomRepository.findByIdAndDeletedIsFalse(id)
            ?: throw NotFoundException()
    
        // 2) 不要用不存在的 deleted 欄位過濾，直接 map 即可
        val materials = bom.bomMaterials
            .map { m ->
                BomMaterialDto(
                    itemCode = m.item?.code,
                    itemName = m.item?.name ?: m.itemName,
                    isConsumable = m.isConsumable,
                    baseQty = m.baseQty,
                    baseUom = m.baseUnitName,
                    stockQty = m.stockQty,
                    stockUom = m.stockUnitName,
                    salesQty = m.saleQty,
                    salesUom = m.salesUnitCode
                )
            }
    
        val processes = bom.bomProcesses
            .sortedBy { it.seqNo }
            .map { p ->
                BomProcessDto(
                    seqNo = p.seqNo,
                    processCode = p.process?.code,
                    processName = p.process?.name,
                    processDescription = p.description,
                    equipmentCode = p.equipment?.code,
                    equipmentName = p.equipment?.name,
                    durationInMinute = p.durationInMinute,
                    prepTimeInMinute = p.prepTimeInMinute,
                    postProdTimeInMinute = p.postProdTimeInMinute
                )
            }
    
        // 3) 其餘維持不變即可
        return BomDetailResponse(
            id = bom.id!!,
            itemCode = bom.item?.code,
            itemName = bom.item?.name,
            isDark = (bom.isDark ?: 0) != 0,
            isFloat = bom.isFloat,
            isDense = bom.isDense,
            isDrink = bom.isDrink,
            scrapRate = bom.scrapRate,
            allergicSubstances = bom.allergicSubstances,
            timeSequence = bom.timeSequence,
            complexity = bom.complexity,
            baseScore = bom.baseScore?.toInt(),
            description = bom.description,
            outputQty = bom.outputQty,
            outputQtyUom = bom.outputQtyUom,
            materials = materials,
            processes = processes
        )
    }
}