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

import com.ffii.core.response.RecordsRes
import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.core.utils.GsonUtils
import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.jobOrder.service.JobOrderBomMaterialService
import com.ffii.fpsms.modules.jobOrder.service.JobOrderProcessService
import com.ffii.fpsms.modules.jobOrder.service.JobOrderService
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderBomMaterialRequest
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderProcessRequest
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderRequest
import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderDetail
import com.ffii.fpsms.modules.master.entity.*
import com.ffii.fpsms.modules.master.entity.projections.*
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleLineRequest
import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleRequest
import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest
import com.ffii.fpsms.modules.settings.service.SettingsService
import com.ffii.fpsms.modules.stock.entity.Inventory
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.service.InventoryService
import com.ffii.fpsms.modules.productProcess.service.ProductProcessService
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.reflect.TypeToken
import org.springframework.data.domain.PageRequest
import org.springframework.scheduling.support.CronTrigger
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.jdbc.core.BeanPropertyRowMapper
import java.lang.reflect.Type
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.ScheduledFuture
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.jvm.optionals.getOrNull
import kotlin.math.ceil
import kotlin.comparisons.maxOf
import java.util.Locale

// === POI IMPORTS FOR EXCEL EXPORT WITH PRINT SETUP ===
import org.apache.poi.ss.usermodel.*
import org.apache.poi.ss.usermodel.IndexedColors
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.VerticalAlignment
import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.apache.poi.xssf.usermodel.XSSFPrintSetup
import java.io.ByteArrayOutputStream
import java.sql.Timestamp
import com.ffii.fpsms.modules.jobOrder.service.JobOrderCreationService

@Service
open class ProductionScheduleService(
    private val jdbcDao: JdbcDao,
    private val itemService: ItemsService,
    private val productionScheduleRepository: ProductionScheduleRepository,
    private val productionScheduleLineRepository: ProductionScheduleLineRepository,
    private val bomMaterialService: BomMaterialService,
    private val bomMaterialRepository: BomMaterialRepository,
    private val jobOrderService: JobOrderService,
    private val jobOrderBomMaterialService: JobOrderBomMaterialService,
    private val bomService: BomService,
    private val jobOrderProcessService: JobOrderProcessService,
    private val inventoryService: InventoryService,
    private val inventoryRepository: InventoryRepository,
    private val itemUomService: ItemUomService,
    private val productProcessService: ProductProcessService,
    private val jobOrderCreationService: JobOrderCreationService,
    private val settingsService: SettingsService,
) : AbstractBaseEntityService<ProductionSchedule, Long, ProductionScheduleRepository>(
    jdbcDao,
    productionScheduleRepository
) {
    // do mapping with projection
    open val formatter: DateTimeFormatter? = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
    val defaultCronExpression = "0 0 2 * * *";

    open fun getLatestScheduleAt(type: String?): LocalDateTime {
        return productionScheduleRepository.getLatestRoughScheduleAt(type)
    }

    open fun getScheduledAtByDate(inputDate: LocalDate): LocalDate {
        val daysToSubtract = when (inputDate.dayOfWeek) {
            DayOfWeek.WEDNESDAY -> 7
            else -> inputDate.dayOfWeek.value + 7 - DayOfWeek.WEDNESDAY.value
        }

        val lastWednesday = inputDate.minusDays(daysToSubtract.toLong())

        return lastWednesday;
    }

    open fun allProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> {
        val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10);

        val response = productionScheduleRepository.findProdScheduleInfoByPage(
            scheduleAt = request.scheduleAt,
            schedulePeriod = request.schedulePeriod,
            schedulePeriodTo = request.schedulePeriodTo,
            totalEstProdCount = request.totalEstProdCount,
            types = request.types,
            pageable = pageable
        )

        val records = response.content
        val total = response.totalElements
        return RecordsRes<ProdScheduleInfo>(records, total.toInt());
    }

    open fun allRoughProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> {
//        request.apply {
//            types = listOf("rough")
//        }
        return allProdSchedulesByPage(request);
    }

    open fun allDetailedProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> {
        val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10);

        val produceAtDate: LocalDate = request.produceAt?.takeIf { it.isNotBlank() }
        ?.let {
           LocalDate.parse(it.trim())
        }
        ?: LocalDate.now()

        val response = productionScheduleRepository.findProdScheduleInfoByProduceAtByPage(
            produceAt = produceAtDate!!,
            totalEstProdCount = request.totalEstProdCount,
            types = request.types,
            pageable = pageable
        )

        val records = response.content
        val total = response.totalElements
        return RecordsRes<ProdScheduleInfo>(records, total.toInt());
    }

    open fun roughProdScheduleDetail(id: Long): RoughProdScheduleWithLine {
        val zero = BigDecimal.ZERO
        val prodSchedule = productionScheduleRepository.findById(id).getOrNull() ?: throw NoSuchElementException()
        val dayOfWeekValue =
            if (prodSchedule.scheduleAt.dayOfWeek.value >= 7) prodSchedule.scheduleAt.dayOfWeek.value - 7 else prodSchedule.scheduleAt.dayOfWeek.value
        val schedulePeriod = prodSchedule.scheduleAt.plusDays((7 + 4 - dayOfWeekValue).toLong())
        val schedulePeriodTo = prodSchedule.scheduleAt.plusDays((7 + 4 + 6 - dayOfWeekValue).toLong())
        val prodScheduleLines = prodSchedule.productionScheduleLines.filter { !it.deleted }

        // ---------------------------------- By FG ----------------------------------//
        // Production Schedule By Date
        val prodScheduleLineInfosByFg = prodScheduleLines.map { line ->
            val bomMaterials = line.item.id?.let { bomMaterialRepository.findAllByBomItemIdAndDeletedIsFalse(it) }
                ?.map { bm ->
//                    val proportion =
//                        if (line.prodQty > 0 && bm.bom?.outputQty != null && (bm.bom?.outputQty ?: zero) > zero) {
//                            BigDecimal(line.prodQty).divide(bm.bom!!.outputQty, 2, RoundingMode.HALF_UP)
//                        } else {
//                            BigDecimal.ONE
//                        }

                    val proportion = BigDecimal.ONE // BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP)
                    val demandQty = bm.qty?.times(proportion) ?: zero
                    val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }

                    RoughProdScheduleLineBomMaterialInfoByFg(
                        id = bm.id,
                        code = bm.item?.code,
                        name = bm.item?.name,
                        type = bm.item?.type,
                        availableQty = bm.item?.inventories?.sumOf {
                            (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero)
                        },
                        demandQty = demandQty,
                        uomName = saleUnit?.uom?.udfudesc
                    )
                }

            val saleUnit = line.item.id?.let { itemUomService.findSalesUnitByItemId(it) }
            RoughProdScheduleLineInfoByFg(
                id = line.id,
                code = line.item.code,
                name = line.item.name,
                type = line.item.type,
                availableQty = line.item.inventories.sumOf {
                    (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero)
                },
                prodQty = BigDecimal(line.prodQty),
                lastMonthAvgSales = BigDecimal(line.lastMonthAvgSales),
                estCloseBal = BigDecimal(line.estCloseBal),
                priority = line.itemPriority,
                bomMaterials = bomMaterials,
                assignDate = line.assignDate,
                uomName = saleUnit?.uom?.udfudesc,
            )
        }.filter { !it.bomMaterials.isNullOrEmpty() }

        // Sum of the Production Schedule
        val sumProdScheduleLineInfosByFg = prodScheduleLineInfosByFg
            .groupBy { it.code }
            .map { (code, infos) ->
                val sumBomMaterials = infos
                    .flatMap { it.bomMaterials ?: mutableListOf() }
                    .groupBy { it.code }
                    .map { (code, bm) ->
                        RoughProdScheduleLineBomMaterialInfoByFg(
                            id = if (bm.isNotEmpty()) bm[0].id else null,
                            code = if (bm.isNotEmpty()) bm[0].code else null,
                            name = if (bm.isNotEmpty()) bm[0].name else null,
                            type = if (bm.isNotEmpty()) bm[0].type else null,
                            availableQty = bm.sumOf { it.availableQty ?: zero },
                            demandQty = bm.sumOf { it.demandQty ?: zero },
                            uomName = if (bm.isNotEmpty()) bm[0].uomName else null
                        )
                    }

                RoughProdScheduleLineInfoByFg(
                    id = if (infos.isNotEmpty()) infos[0].assignDate else null,
                    code = if (infos.isNotEmpty()) infos[0].code else null,
                    name = if (infos.isNotEmpty()) infos[0].name else null,
                    type = if (infos.isNotEmpty()) infos[0].type else null,
                    availableQty = infos.sumOf { (it.availableQty ?: zero) },
                    prodQty = infos.sumOf { (it.prodQty ?: zero) },
                    lastMonthAvgSales = infos.sumOf { (it.lastMonthAvgSales ?: zero) },
                    estCloseBal = infos.sumOf { (it.estCloseBal ?: zero) },
                    priority = if (infos.isNotEmpty()) infos[0].priority else null,
                    bomMaterials = sumBomMaterials,
                    assignDate = null,
                    uomName = if (infos.isNotEmpty()) infos[0].uomName else null
                )
            }

        // Production Schedule By Date group by assign date
        val groupedProdScheduleLinesByFg = prodScheduleLineInfosByFg.groupBy { it.assignDate }

        // ---------------------------------- By BoM ----------------------------------//
        // Seven Days of Bom Materials
        val prodScheduleLinesInfoByBom7Days = prodScheduleLines.mapNotNull { line ->
            val bomMaterial = line.item.id?.let { bomMaterialRepository.findAllByBomItemIdAndDeletedIsFalse(it) }

            bomMaterial?.map { bm ->
//                val proportion =
//                    if (line.prodQty > 0 && bm.bom?.outputQty != null && (bm.bom?.outputQty ?: zero) > zero) {
//                        BigDecimal(line.prodQty).divide(bm.bom!!.outputQty, 2, RoundingMode.HALF_UP)
//                    } else {
//                        BigDecimal.ONE
//                    }

                val proportion = BigDecimal.ONE //BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP)
                val demandQty = bm.qty?.times(proportion) ?: zero
                val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) }

                RoughProdScheduleLineInfoByBomByDate(
                    id = bm.item?.id,
                    code = bm.item?.code,
                    name = bm.item?.name,
                    type = bm.item?.type,
                    availableQty = bm.item?.inventories?.sumOf {
                        (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero)
                    } ?: zero,
                    demandQty = demandQty,
                    assignDate = line.assignDate,
                    uomName = saleUnit?.uom?.udfudesc
                )
            }
        }
            .flatten()

//        println(prodScheduleLinesInfoByBom7Days)

        // Production Schedule By Bom By Date
        val prodScheduleLinesInfoByBom = prodScheduleLinesInfoByBom7Days
            .groupBy { Pair(it.assignDate, it.code) }
            .map { (key, line) ->
                RoughProdScheduleLineInfoByBomByDate(
                    id = if (line.isNotEmpty()) line[0].id else null,
                    code = if (line.isNotEmpty()) line[0].code else null,
                    name = if (line.isNotEmpty()) line[0].name else null,
                    type = if (line.isNotEmpty()) line[0].type else null,
                    availableQty = if (line.isNotEmpty()) line[0].availableQty else zero,
                    demandQty = line.sumOf { it.demandQty ?: zero },
                    assignDate = key.first,
                    uomName = if (line.isNotEmpty()) line[0].uomName else null,
                )
            }

        val groupedProdScheduleLinesByBom = prodScheduleLinesInfoByBom.groupBy { it.assignDate }

        // Sum of the Production Schedule By Bom
        val sumProdScheduleLinesInfoByBom = prodScheduleLinesInfoByBom7Days
            .groupBy { it.code }
            .map { (_, line) ->
                RoughProdScheduleLineInfoByBom(
                    id = if (line.isNotEmpty()) line[0].id else null,
                    code = if (line.isNotEmpty()) line[0].code else null,
                    name = if (line.isNotEmpty()) line[0].name else null,
                    type = if (line.isNotEmpty()) line[0].type else null,
                    availableQty = if (line.isNotEmpty()) line[0].availableQty else null,
                    totalDemandQty = line.sumOf { it.demandQty ?: zero },
                    demandQty1 = line.filter { it.assignDate == 1L }.sumOf { it.demandQty ?: zero },
                    demandQty2 = line.filter { it.assignDate == 2L }.sumOf { it.demandQty ?: zero },
                    demandQty3 = line.filter { it.assignDate == 3L }.sumOf { it.demandQty ?: zero },
                    demandQty4 = line.filter { it.assignDate == 4L }.sumOf { it.demandQty ?: zero },
                    demandQty5 = line.filter { it.assignDate == 5L }.sumOf { it.demandQty ?: zero },
                    demandQty6 = line.filter { it.assignDate == 6L }.sumOf { it.demandQty ?: zero },
                    demandQty7 = line.filter { it.assignDate == 7L }.sumOf { it.demandQty ?: zero },
                    uomName = if (line.isNotEmpty()) line[0].uomName else null,
                )
            }



        // ---------------------------------- Response ----------------------------------//
        return RoughProdScheduleWithLine(
            id = prodSchedule.id,
            scheduleAt = prodSchedule.scheduleAt,
            schedulePeriod = schedulePeriod,
            schedulePeriodTo = schedulePeriodTo,
            totalEstProdCount = BigDecimal(prodSchedule.totalEstProdCount),
            totalFGType = prodSchedule.totalFGType,
            type = prodSchedule.type,
            prodScheduleLinesByFg = sumProdScheduleLineInfosByFg,
            prodScheduleLinesByFgByDate = groupedProdScheduleLinesByFg,
            prodScheduleLinesByBom = sumProdScheduleLinesInfoByBom,
            prodScheduleLinesByBomByDate = groupedProdScheduleLinesByBom
        )
    }

    open fun detailedProdScheduleDetail(id: Long): DetailedProdScheduleWithLine {
        val sqlResult = productionScheduleRepository.findDetailedProdScheduleWithLine(id) ?: throw NoSuchElementException()

        val type = object : TypeToken<List<DetailedProdScheduleLineInfo>?>() {}.type
//        val gsonResult: List<DetailedProdScheduleLineInfo>? = gson.fromJson(sqlResult.prodScheduleLines, type)
        val gsonResult: List<DetailedProdScheduleLineInfo>? = GsonUtils.stringToJson(sqlResult.prodScheduleLines, type)


        return DetailedProdScheduleWithLine(
            id = sqlResult.id,
            produceAt = sqlResult.produceAt,
            scheduleAt = sqlResult.scheduleAt,
            totalEstProdCount = sqlResult.totalEstProdCount,
            totalFGType = sqlResult.totalFGType,
            prodScheduleLines = gsonResult
        )
    }

    @Transactional(rollbackFor = [java.lang.Exception::class])
    open fun saveProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse {
        val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException()
        val prodSchedule = prodScheduleLine.productionSchedule
        
        // Update Prod Schedule Type
        prodSchedule.apply {
            type = "manual"
        }
        productionScheduleRepository.saveAndFlush(prodSchedule)

        // Update Prod Schedule Line Prod qty
        prodScheduleLine.apply {
            prodQty = request.demandQty.toDouble()
            type = "manual"
        }
        productionScheduleLineRepository.saveAndFlush(prodScheduleLine)

        val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) }
     
        return MessageResponse(
            id = request.id,
            name = null,
            code = null,
            type = null,
            message = "Success",
            entity = mapOf("bomMaterials" to bomMaterials),
            errorPosition = null
        )
    }

    @Transactional(rollbackFor = [java.lang.Exception::class])
    open fun releaseProdSchedule(request: ReleaseProdScheduleRequest): MessageResponse {
        
        // 1. Fetch data safely. Assuming searchProdScheduleLine returns a List<Map<String, Any>>
        val data: List<Map<String, Any>> = searchProdScheduleLine(request.id)
        
        if (data.isEmpty()) {
            logger.warn("No production schedule lines found for ID: ${request.id}")
            return MessageResponse(
                id = request.id.toLong(), 
                message = "No lines to release. Parent ID: ${request.id}", 
                errorPosition = "data",
                name = "",
                code = "",
                type = ""
            )
        }

        val approver = SecurityUtils.getUser().getOrNull() // Get approver once
        var jobOrderCount = 0

        for (d in data) {
            // 2. Safely cast the ID (Int) and convert it to Long for repository lookup
            val prodScheduleLineIdInt = d["id"] as? Int 
                ?: throw IllegalStateException("Production Schedule Line ID is missing or not an Int in search result map.")
                
            val prodScheduleLineId = prodScheduleLineIdInt.toLong()
                
            val prodScheduleLine = productionScheduleLineRepository.findById(prodScheduleLineId).getOrNull()
                ?: throw NoSuchElementException("Production Schedule Line with ID $prodScheduleLineId not found.")

            // 3. Fetch BOM, handling nullability safely
            val item = prodScheduleLine.item 
                ?: throw IllegalStateException("Item object is missing for Production Schedule Line $prodScheduleLineId.")

            val itemId = item.id 
                ?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.")

            //try {                
            //    jobOrderService.jobOrderDetailByItemId(itemId)
            //    logger.info("jobOrderDetailByItemId ok itemId:$itemId")
            //} catch (e: NoSuchElementException) {
                //only do with no JO is working
                //logger.info("NoSuchElementException itemId:$itemId")
                try {
                    jobOrderService.jobOrderDetailByProdScheduleLineId(prodScheduleLineId)
                } catch (e: NoSuchElementException) {
                    val bom = bomService.findByItemId(itemId)
                    ?: throw NoSuchElementException("BOM not found for Item ID $itemId.")

                    // 4. Update Prod Schedule Line fields (keep prodQty = total = outputQty * batchNeed)
                    val outputQtyDouble = bom.outputQty?.toDouble()
                        ?: throw IllegalStateException("BOM output quantity is null for Item ID $itemId.")
                    prodScheduleLine.apply {
                        prodQty = outputQtyDouble * batchNeed.toLong().coerceAtLeast(0)
                        approverId = approver?.id
                    }
                    
                    productionScheduleLineRepository.save(prodScheduleLine)

                    // 5. Logging (optional but kept)
                    logger.info("prodScheduleLine.prodQty: ${prodScheduleLine.prodQty}")
                    logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}")

                    logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
                    //repeat(prodScheduleLine.needNoOfJobOrder) {
                        // 6. Create Job Order
                    val produceAt = prodScheduleLine.productionSchedule?.produceAt
                    val joRequest = CreateJobOrderRequest(
                        bomId = bom.id, // bom is guaranteed non-null here
                        reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())),
                        approverId = approver?.id,

                        planStart = produceAt,
                        planEnd = produceAt,

                        // CRUCIAL FIX: Use the line ID, not the parent schedule ID
                        prodScheduleLineId = prodScheduleLine.id!!
                    )

                    val jo = jobOrderCreationService.createJobOrderWithFullFlow(
                        request = joRequest,
                        productionPriority = prodScheduleLine.itemPriority.toInt()
                    )

                    jobOrderCount++
                    //}
                }
                
            //}

            // No need to fetch latest detail inside the loop
        }

        return MessageResponse(
            id = request.id.toLong(),
            //message = "Successfully created Job Orders for all production schedule lines (Parent ID: ${request.id}).",
            message = if (jobOrderCount == 0)
                "沒有建立新工單(可能因為成品已有生產中的工單)."
            else
                "成功建立 $jobOrderCount 張工單!",
            name = "",
            code = "",
            type = "",
            errorPosition = null
            // entity = mapOf("prodScheduleLines" to latestDetail?.prodScheduleLines)
        )
    }

    @Transactional(rollbackFor = [java.lang.Exception::class])
    open fun releaseProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse {
        val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException()
        val bom = prodScheduleLine.item.id?.let { bomService.findByItemId(it) }
        val approver = SecurityUtils.getUser().getOrNull()
        val isSameQty = request.demandQty.equals(prodScheduleLine.prodQty)

        // Update Prod Schedule Type
        if (!isSameQty) {
            val prodSchedule = prodScheduleLine.productionSchedule
            prodSchedule.apply {
                type = "manual"
            }
            productionScheduleRepository.save(prodSchedule)
        }

        // Update Prod Schedule Line Prod qty
        prodScheduleLine.apply {
            prodQty = request.demandQty.toDouble()
            this.type = if (isSameQty) this.type else "manual"
            approverId = approver?.id
        }
        productionScheduleLineRepository.save(prodScheduleLine)

        //compare prodQty to bom outputQty
        logger.info("request.demandQty:" + request.demandQty);
        logger.info("prodScheduleLine.prodQty:" + prodScheduleLine.prodQty);
        logger.info("bom?.outputQty:" + bom?.outputQty + "" + bom?.outputQtyUom);

        logger.info("prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
        
        val prodScheduleLineId = prodScheduleLine.id!!

        try {
                jobOrderService.jobOrderDetailByPsId(prodScheduleLineId)
        } catch (e: NoSuchElementException) {
            
            
            // 3. Fetch BOM, handling nullability safely
            val item = prodScheduleLine.item 

            val itemId = item.id 
                ?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.")

            val bom = bomService.findByItemId(itemId)
                ?: throw NoSuchElementException("BOM not found for Item ID $itemId.")

            val approver = SecurityUtils.getUser().getOrNull() // Get approver once

            val produceAt = prodScheduleLine.productionSchedule?.produceAt
            val joRequest = CreateJobOrderRequest(
                bomId = bom.id, // bom is guaranteed non-null here
                reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())),
                approverId = approver?.id,

                planStart = produceAt,
                planEnd = produceAt,

                // CRUCIAL FIX: Use the line ID, not the parent schedule ID
                prodScheduleLineId = prodScheduleLine.id!!
            )

            jobOrderCreationService.createJobOrderWithFullFlow(
                request = joRequest,
                productionPriority = prodScheduleLine.itemPriority.toInt()
            )

        }
        
        // Get Latest Data
//        val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) }
        val latestDetail = prodScheduleLine.productionSchedule.id?.let { detailedProdScheduleDetail(it) }

        return MessageResponse(
            
            id = request.id,
            name = null,
            //code = "",
            //entity = null,
            type = null,
            message = "Success",
            code = "",
            entity = mapOf("prodScheduleLines" to latestDetail?.prodScheduleLines),
            errorPosition = null
             
        )
         
    }

    //====================細排相關 START====================//
    open fun searchProdScheduleLine(prodScheduleId: Int): List<Map<String, Any>> {

        val args = mapOf(
            "prodScheduleId" to prodScheduleId,
        )

        val sql = """
             SELECT 
                 psl.* 
                    FROM production_schedule ps 
                     LEFT JOIN production_schedule_line psl ON psl.prodScheduleId = ps.id 
                     WHERE ps.deleted = FALSE 
                     AND psl.deleted = FALSE 
                     AND psl.prodScheduleId = :prodScheduleId 
        """;

      
        return jdbcDao.queryForList(sql, args);
        
    }

    open fun getDailyProductionCount(assignDate: Int, selectedDate: LocalDateTime): Int {

        val args = mapOf(
            "assignDate" to assignDate,
            "selectedDate" to selectedDate.format(formatter)
        )

        val sql = StringBuilder(
            " SELECT "
                    + " SUM(psl.prodQty) AS dailyProdCount "
                    + " FROM production_schedule ps "
                    + " LEFT JOIN production_schedule_line psl ON psl.prodScheduleId = ps.id "
                    + " WHERE ps.deleted = FALSE "
                    + " AND psl.deleted = FALSE "
                    + " AND DATE(ps.scheduleAt) LIKE DATE(:selectedDate) "
                    + " AND psl.assignDate = :assignDate "
                    + " GROUP BY ps.id "
                    + " Limit 1"
        );
        return jdbcDao.queryForInt(sql.toString(), args);
        
    }

    open fun getNeedQty(): List<NeedQtyRecord> {
        val fromDate = java.time.LocalDate.now().minusDays(10)
        val toDate = java.time.LocalDate.now()
        val args = mapOf("fromDate" to fromDate.toString(), "toDate" to toDate.toString())

        val sql = """
            SELECT 
				i.outputQty,
                i.avgQtyLastMonth,
                -- (i.onHandQty -500) * 1.0 / i.avgQtyLastMonth as daysLeft,
                0 as needQty,
                i.stockQty,
                -- CASE
                --    WHEN stockQty * 1.0 / i.avgQtyLastMonth <= 1.9 THEN 
				--		CEIL((i.avgQtyLastMonth * 2.6 - stockQty) / i.outputQty)	
				--	ELSE 0
                -- END AS needNoOfJobOrder,
                -- The first date only consider how many batch pending for job order
                0 AS needNoOfJobOrder,
                0 AS batchNeed,
                i.pendingJobQty,
                ((i.stockQty * 1.0) + ifnull(i.pendingJobQty, 0) ) / i.avgQtyLastMonth as daysLeft,
                
                ifnull(i.baseScore, 0) as priority,
                -- 25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority,
                i.*
            FROM
                (SELECT 
                    COALESCE(
                        (SELECT dailyQty FROM item_daily_out WHERE itemCode = items.code),
                        (SELECT
                                ROUND(AVG(d.dailyQty))
                            FROM
                                (SELECT 
                                SUM(dol.qty) AS dailyQty
                            FROM
                                delivery_order_line dol
                            LEFT JOIN delivery_order do ON dol.deliveryOrderId = do.id
                            WHERE
                                do.deleted = 0 and
                                dol.itemId = items.id
                                    AND do.estimatedArrivalDate >= :fromDate AND do.estimatedArrivalDate <= :toDate 
                            GROUP BY do.estimatedArrivalDate) AS d)
                    ) AS avgQtyLastMonth,

                        (select sum(reqQty) from job_order where bomId = bom.id and status != 'completed') AS pendingJobQty,

                        (select count(1) from coffee_or_tea where systemType = 'coffee' and itemCode = items.code) as isCoffee,
                        (select count(1) from coffee_or_tea where systemType = 'tea' and itemCode = items.code) as isTea,
                        (select count(1) from coffee_or_tea where systemType = 'lemon' and itemCode = items.code) as isLemon,
                        
                        CASE WHEN item_fake_onhand.onHandQty is not null THEN item_fake_onhand.onHandQty
                        ELSE inventory.onHandQty END AS stockQty,
                        bom.baseScore,
                        bom.outputQty,
                        bom.outputQtyUom,
                        (SELECT 
                                udfudesc
                            FROM
                                delivery_order_line
                            LEFT JOIN uom_conversion ON delivery_order_line.uomId = uom_conversion.id
                            WHERE
                                delivery_order_line.itemId = bom.itemId
                            LIMIT 1) AS doUom,
                        items.code,
                        items.name,
                        bom.description,
                        inventory.onHandQty,
                        bom.itemId,
                        bom.id AS bomId,

                        CASE WHEN bom.isDark = 5 THEN 11
                        WHEN bom.isDark = 3 THEN 6
                        WHEN bom.isDark = 1 THEN 2
                        ELSE 0 END as markDark,

						CASE WHEN bom.isFloat = 5 THEN 11
                        WHEN bom.isFloat = 3 THEN 6
                        WHEN bom.isFloat = 1 THEN 2
                        ELSE 0 END as markFloat,

                        CASE WHEN bom.isDense = 5 THEN 11
                        WHEN bom.isDense = 3 THEN 6
                        WHEN bom.isDense = 1 THEN 2
                        ELSE 0 END as markDense,

                        bom.timeSequence as markTimeSequence,

                        bom.complexity as markComplexity,

                        CASE WHEN bom.allergicSubstances = 5 THEN 11
                        ELSE 0 END as markAS,

                        inventory.id AS inventoryId
                FROM
                    bom
                LEFT JOIN items ON bom.itemId = items.id
                LEFT JOIN inventory ON items.id = inventory.itemId
                left join item_fake_onhand on items.code = item_fake_onhand.itemCode
                WHERE bom.deleted = 0 and bom.description = 'FG'
                    -- and bom.itemId != 16771
                    ) AS i
            WHERE 1
                and i.avgQtyLastMonth is not null 
                and i.onHandQty is not null 
                -- and (i.onHandQty -500) * 1.0 / i.avgQtyLastMonth <= 1.9
                -- and avgQtyLastMonth - (onHandQty - 500) > 0

        """.trimIndent()

        val rows: List<Map<String, Any>> = jdbcDao.queryForList(sql, args)

        return rows.map { row ->
            NeedQtyRecord().apply {
                name = row["name"] as String
                id = (row["itemId"] as Number).toLong()
                needQty = (row["needQty"] as Number).toDouble()
                outputQty = (row["outputQty"] as Number).toDouble()
                stockQty = (row["stockQty"] as Number).toDouble()
                avgQtyLastMonth = (row["avgQtyLastMonth"] as Number).toDouble()
                needNoOfJobOrder = (row["needNoOfJobOrder"] as Number).toLong()
                daysLeft = (row["daysLeft"] as Number).toDouble()
                batchNeed = row["batchNeed"] as Number
                priority = row["priority"] as Number
                isCoffee = row["isCoffee"] as Number
                isTea = row["isTea"] as Number
                isLemon = row["isLemon"] as Number
            }   
        }
    }

    open fun generateDetailedScheduleByDay(assignDate: Int, selectedDate: LocalDateTime, produceAt: LocalDateTime, days: Int) {
        val detailedScheduleOutputList = ArrayList<NeedQtyRecord>()

        //increasement available
        //var warehouseCap = 22000.0
        var machineCap = 10000.0
        var needQtyList = getNeedQty()
        
        //remove the production schedule >= today 
        clearTodayAndFutureProdSchedule(produceAt)

        println("needQtyList - " + needQtyList);
        
        //##### The 22000, 10000 machine cap just means the max warehouse storage qty, not production qty cap
        //##### The total production qty of the date is 10000 due to machine cap
        //##### search all items with bom to consider need or no need production
        val args = mapOf(
            "assignDate" to assignDate,
            "selectedDate" to selectedDate.format(formatter)
        )
        val productionPriorityMap: HashMap<NeedQtyRecord, Double> = HashMap();
        for (record in needQtyList) {
            println("Object - " + record.toString());
            //val tempRecord = record;
            //val difference =
            //    -tempRecord.outputQty
            val isDark = maxOf(record.isDark.toDouble(), 0.0)
            val isFloat = maxOf(record.isFloat.toDouble(), 0.0)
            val isDense = maxOf(record.isDense.toDouble(), 0.0)
            val allergicSubstances = maxOf(record.allergicSubstances.toDouble(), 0.0)

            val priority = isDark + isFloat + isDense + allergicSubstances  
            record.itemPriority = record.priority     
            productionPriorityMap.put(record, priority)
        }

        //##### all qty should be 包, FG qty/bom outputQty
        
        //sort by difference
        val sortedEntries = productionPriorityMap.entries.sortedBy { it.value }

        var accProdCount = 0.0;
        var fgCount = 0L;

        for ((updatedScheduleRecord, priority) in sortedEntries) {
            //match id with rough schedule record to create new record
            val targetRecord = needQtyList.find { it.id == updatedScheduleRecord.id }
            //??? this should be the bom output Qty * no. of job order needed
            val prodDifference = 0
            updatedScheduleRecord.itemPriority = priority;            
            
            detailedScheduleOutputList.add(updatedScheduleRecord)
                
            if (updatedScheduleRecord.batchNeed.toInt() > 0) {
                //have enough quoter for adjustment
                machineCap -= prodDifference;
                println("[INFO] item " + updatedScheduleRecord.name + " added " + (updatedScheduleRecord.batchNeed.toInt() * updatedScheduleRecord.outputQty));
                accProdCount += updatedScheduleRecord.batchNeed.toInt() * updatedScheduleRecord.outputQty
                fgCount++
            } else {
                println("[INFO] item " + updatedScheduleRecord.name + " have bee skipped");
            }
        }

        // Sort detailedScheduleOutputList by item priority: needQty
        val sortedOutputList = detailedScheduleOutputList.sortedBy { it.needQty }.toMutableList()

        // Update itemPriority in the sorted list
        var tempPriority = 1L
        sortedOutputList.forEach { record ->
            record.itemPriority = tempPriority
            tempPriority++
        }

        println("[INFO] sortedOutputList.size " + sortedOutputList.size + " have bee skipped");
        //ignore the -1 date
        //saveDetailedScheduleOutput(sortedOutputList, accProdCount, fgCount, produceAt)

        //do for 9 days to predict
        for (i in 0..(days)) {
            val targetDate = produceAt.plusDays(i.toLong())
            val dayOfWeek = targetDate.dayOfWeek
            val isSat = dayOfWeek == DayOfWeek.SATURDAY
            val isFri = dayOfWeek == DayOfWeek.FRIDAY
            val isSunday = dayOfWeek == DayOfWeek.SUNDAY
            val isFriSat = isFri || isSat

            println("##targetDate:" + targetDate + " isFri:"+ isFri + " isSat:" + isSat + " isSunday:" + isSunday)


            fgCount = 0
            accProdCount = 0.0
            sortedOutputList.forEach { record ->
                // isCoffee > 0: only Mon, Wed, Fri; isTea > 0: only Tue, Thu, Sat; isLemon > 0: any day including Sunday
                val canProduceOnDay = when {
                    (record.isLemon.toDouble()) > 0 -> true
                    (record.isCoffee.toDouble()) > 0 -> dayOfWeek in listOf(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY)
                    (record.isTea.toDouble()) > 0 -> dayOfWeek in listOf(DayOfWeek.TUESDAY, DayOfWeek.THURSDAY, DayOfWeek.SATURDAY)
                    else -> !isSunday
                }
                if(i > 0)
                    record.stockQty = record.stockQty + (record.outputQty * record.needNoOfJobOrder.toInt()) - record.avgQtyLastMonth
                else
                    record.stockQty = record.stockQty + (record.outputQty * record.needNoOfJobOrder.toInt())

                //compare if less than 1.9 days                
                record.daysLeft = record.stockQty / record.avgQtyLastMonth
                println("name: " + record.name + " stockQty:" + record.stockQty + " avgQtyLastMonth:" + record.avgQtyLastMonth + " daysLeft:" + record.daysLeft)
                
                var safetyStockDay = 2.0
                var redLine = 1.9

                // isFriSat inflated thresholds not suitable for lemon; lemon always uses standard values
                if (isFriSat && (record.isLemon.toDouble()) <= 0) {
                    safetyStockDay = 3.1
                    redLine = 2.7
                }

                if(record.daysLeft < redLine){
                    record.needQty = (record.avgQtyLastMonth * safetyStockDay) - record.stockQty;

                    if(canProduceOnDay && record.needQty > 0.0){
                        record.batchNeed = ceil(record.needQty / record.outputQty)
                        record.needNoOfJobOrder = record.batchNeed

                        fgCount += 1
                        accProdCount += record.outputQty * record.batchNeed.toInt()
                    }else{
                        record.needNoOfJobOrder = 0
                        record.batchNeed = 0
                    }
                }else{
                    record.needQty = 0.0
                    record.needNoOfJobOrder = 0
                    record.batchNeed = 0
                }

                println(record.name + " record.batchNeed: " + record.batchNeed + " record.stockQty:" + record.stockQty + " record.daysLeft:" + record.daysLeft)

            }

            saveDetailedScheduleOutputPredict(sortedOutputList, accProdCount, fgCount, produceAt.plusDays(i.toLong()))
        }
    }
    
    open fun saveDetailedScheduleOutput(
        sortedEntries: List<NeedQtyRecord>,
        accProdCount: Double,
        fgCount: Long,
        produceAt: LocalDateTime,
    ) {
        val tempObj = ProductionSchedule()
        tempObj.id = -1;
        tempObj.scheduleAt = LocalDateTime.now()
        tempObj.produceAt = produceAt;
        tempObj.totalFGType = fgCount;
        tempObj.totalEstProdCount = accProdCount;
        tempObj.type = "detailed"
        tempObj.id = saveProductionScheduleToDatabase(tempObj);

        for (detailedScheduleRecord in sortedEntries) {
            if(detailedScheduleRecord.batchNeed.toInt() > 0)
                saveDetailedScheduleLineToDatabase(tempObj.id ?: -1, detailedScheduleRecord)
        }
    }

    open fun saveDetailedScheduleOutputPredict(
        sortedEntries: List<NeedQtyRecord>,
        accProdCount: Double,
        fgCount: Long,
        produceAt: LocalDateTime,
    ) {
        val tempObj = ProductionSchedule()
        tempObj.id = -1;
        tempObj.scheduleAt = LocalDateTime.now()
        tempObj.produceAt = produceAt;
        tempObj.totalFGType = fgCount;
        tempObj.totalEstProdCount = accProdCount;
        tempObj.type = "detailed"
        tempObj.id = saveProductionScheduleToDatabase(tempObj);

        for (detailedScheduleRecord in sortedEntries) {
            if(detailedScheduleRecord.batchNeed.toInt() > 0)
                saveDetailedScheduleLineToDatabase(tempObj.id ?: -1, detailedScheduleRecord)
        }
    }

    private fun saveDetailedScheduleLineToDatabase(parentId: Long, detailedScheduleObj: NeedQtyRecord) {
        try {
            val prodSchedule = productionScheduleRepository.findById(parentId).get()
            val item = detailedScheduleObj.id?.let { itemService.findById(it) } ?: Items()
            var savedItem = ProductionScheduleLine()
            print("###detailedScheduleObj.stockQty:" + detailedScheduleObj.stockQty)
            savedItem.id = -1;
//            savedItem.prodScheduleId = parentId;
            savedItem.productionSchedule = prodSchedule;
            savedItem.item = item;
            savedItem.lastMonthAvgSales = detailedScheduleObj.avgQtyLastMonth ?: 0.0;
            savedItem.refScheduleId = detailedScheduleObj.id;
            savedItem.approverId = null;
            savedItem.estCloseBal = detailedScheduleObj.outputQty;
            // prodQty = total production = batchQty (outputQty per batch) * batchNeed
            savedItem.prodQty = detailedScheduleObj.outputQty * detailedScheduleObj.batchNeed.toInt().coerceAtLeast(0)
            savedItem.type = "detailed"
            savedItem.assignDate = LocalDateTime.now().dayOfMonth.toLong();
            savedItem.weightingRef = detailedScheduleObj.needQty
            savedItem.itemPriority = detailedScheduleObj.priority.toLong()
            savedItem.outputQty = detailedScheduleObj.outputQty
            savedItem.avgQtyLastMonth = detailedScheduleObj.avgQtyLastMonth
            savedItem.stockQty = detailedScheduleObj.stockQty
            savedItem.daysLeft = detailedScheduleObj.daysLeft
            savedItem.batchNeed = detailedScheduleObj.batchNeed.toInt()
            savedItem.needNoOfJobOrder = detailedScheduleObj.needNoOfJobOrder.toInt()

            productionScheduleLineRepository.saveAndFlush(savedItem)

        } catch (e: Exception) {
            throw RuntimeException("Error saving production schedule line: ${e.message}", e)
        }
    }
    //====================細排相關 END====================//
    open class NeedQtyRecord {
        //SQL record in general with item name
        open var name: String = "" //item name
        open var avgQtyLastMonth: Double = 0.0
        open var needNoOfJobOrder: Number = 0
        open var id: Long = 0
        open var needQty: Double = 0.0
        open var outputQty: Double = 0.0
        open var itemPriority: Number = 0
        open var isDark: Number = 0
        open var isFloat: Number = 0
        open var isDense: Number = 0
        open var allergicSubstances: Number = 0
        open var stockQty: Double = 0.0
        open var daysLeft: Double = 0.0
        open var batchNeed: Number = 0
        open var priority: Number = 0
        open var isCoffee: Number = 0
        open var isTea: Number = 0
        open var isLemon: Number = 0

        override fun toString(): String {
            return "NeedQtyRecord(name=${name}," +            
                    " avgQtyLastMonth=$avgQtyLastMonth" +
                    " stockQty=$stockQty" +
                    " daysLeft=$daysLeft" +
                    " batchNeed=$batchNeed" +
                    " priority=$priority" +
                    " itemId=$id" +
                    " needNoOfJobOrder=$needNoOfJobOrder" +
                    " outputQty=$outputQty" +
                    " needQty=$needQty" +
                    " itemPriority=$itemPriority"+
                    " isDark=$isDark"  +
                    " isFloat=$isFloat"  +
                    " isDense=$isDense"  +
                    " allergicSubstances=$allergicSubstances"          
        }
    }

    open class ProductionScheduleRecord : ProductionScheduleLine() {
        //SQL record in general with item name
        open var name: String = "" //item name
        open var isDark: Number = 0
        open var isFloat: Number = 0
        open var isDense: Number = 0
        open var targetMinStock: Double = 0.0;

        override fun toString(): String {
            return "ProductionScheduleLine(prodScheduleId=${productionSchedule.id}," +
                    " itemId=${item.id}, lastMonthAvgSales=$lastMonthAvgSales," +
                    " prodQty=$prodQty, estCloseBal=$estCloseBal, type='$type'," +
                    " approverId=$approverId, refScheduleId=$refScheduleId," +
                    " assignDate=$assignDate, itemPriority=$itemPriority)" +
                    " productionScheduleRecord(name='$name', targetMinStock=$targetMinStock)" +
                    " isDark=$isDark, isFloat=$isFloat, isDense=$isDense, weightingRef=$weightingRef"
        }
    }

    open fun getProductionScheduleRecord(args: Map<String, Any>): List<ProductionScheduleRecord> {

        val sql = StringBuilder(
            " SELECT "
                    + " psl.id, "
                    + " psl.itemPriority, "
                    + " psl.itemId, "
                    + " i.name, "
                    + " COALESCE(b.isDark, 0) AS isDark, "
                    + " COALESCE(b.isFloat, 0) AS isFloat, "
                    + " COALESCE(b.isDense, 0) AS isDense, "
                    + " psl.lastMonthAvgSales, "
                    + " psl.lastMonthAvgSales*2 AS targetMinStock, "
                    + " psl.prodQty, "
                    + " psl.estCloseBal, "
                    + " psl.`type`, "
                    + " psl.assignDate "
                    + " FROM production_schedule ps "
                    + " LEFT JOIN production_schedule_line psl ON psl.prodScheduleId = ps.id "
                    + " LEFT JOIN items i ON psl.itemId = i.id "
                    + " LEFT JOIN bom b ON i.id = b.itemId "
                    + " WHERE ps.deleted = FALSE "
                    + " AND psl.deleted = FALSE "
                    + " AND ps.`type` = 'rough' "
        );

        if (args.containsKey("selectedDate")) {
            sql.append(" AND DATE(ps.scheduleAt) LIKE DATE(:selectedDate) AND ps.id = (select max(ps1.id) from production_schedule ps1 where DATE(ps1.scheduleAt) LIKE DATE(:selectedDate) AND ps1.`type` = 'rough') ");
        }
        if (args.containsKey("name")) {
            sql.append(" AND i.name LIKE :name ");
        }
        if (args.containsKey("assignDate")) {
            sql.append(" AND psl.assignDate = :assignDate ");
        }
        if (args.containsKey("id")) {
            sql.append(" AND i.id = :id ");
        }

        sql.append(" ORDER BY psl.assignDate, psl.itemPriority ASC ");

        print(sql)
        val resultList = jdbcDao.queryForList(sql.toString(), args);
        print(resultList)

        //TODO: From Global config
        val DARK_MAX_VALUE = 1;
        val FLOAT_MAX_VALUE = 5;
        val DENSE_MAX_VALUE = 5;
        val DARK_WEIGHTING = 0.34;
        val FLOAT_WEIGHTING = 0.33;
        val DENSE_WEIGHTING = 0.33;

        // Convert the list of maps to a list of ProductionScheduleRecord
        return resultList.map { row ->
            ProductionScheduleRecord().apply {
                this.id = row["id"].toString().toLong()
                this.itemPriority = row["itemPriority"].toString().toLong()
                this.item.id = row["itemId"].toString().toLong()
                this.name = row["name"] as String
                this.lastMonthAvgSales = row["lastMonthAvgSales"].toString().toDouble()
                this.targetMinStock = row["targetMinStock"].toString().toDouble() // Ensure correct type
                this.prodQty = row["prodQty"].toString().toDouble()
                this.estCloseBal = row["estCloseBal"].toString().toDouble()
                this.type = row["type"] as String
                this.assignDate = row["assignDate"].toString().toLong()

                this.isDark = row["isDark"].toString().toInt()
                this.isDense = row["isDense"].toString().toInt()
                this.isFloat = row["isFloat"].toString().toInt()

                val isDarkValue = row["isDark"].toString().toInt()
                val isDenseValue = row["isDense"].toString().toInt()
                val isFloatValue = row["isFloat"].toString().toInt()

                this.weightingRef =
                    if (isDarkValue > 0) {
                        (DARK_MAX_VALUE + 1 - isDarkValue) / DARK_MAX_VALUE.toDouble() * DARK_WEIGHTING
                    } else {
                        0.0
                    } +
                            if (isDenseValue > 0) {
                                (DENSE_MAX_VALUE + 1 - isDenseValue) / DENSE_MAX_VALUE.toDouble() * DENSE_WEIGHTING
                            } else {
                                0.0
                            } +
                            if (isFloatValue > 0) {
                                (FLOAT_MAX_VALUE + 1 - isFloatValue) / FLOAT_MAX_VALUE.toDouble() * FLOAT_WEIGHTING
                            } else {
                                0.0
                            }
            }
        }
    }

    //====================粗排相關 START====================//
    open fun convertToFinishedGoodList(): ArrayList<FinishedGood> {
        val roughScheduleList = itemService.getRoughScheduleList() // Retrieve the list from your existing method
        val finishedGoodList = ArrayList<FinishedGood>()

        for (item in roughScheduleList) {
            val finishedGood = FinishedGood(
                id = item["itemId"].toString().toLong(), // Cast to Long
                name = item["itemName"] as String?, // Cast to String
                receipeId = 0, // Assuming itemNo is a Long
                openBalance = 0.0, // Set default value or calculate if needed
                lastMonthAvgSalesCount = item["lastMonthAvgSalesCount"].toString().toDouble(), // Cast to Double
                fgProductionLimit = item["fgProductionLimit"].toString().toDouble() // Default production limit
            )
            finishedGoodList.add(finishedGood)
        }

        return finishedGoodList
    }

    open fun generateRoughScheduleByWeek(fgList: ArrayList<FinishedGood>): HashMap<RoughScheduleObj, Double> {
        val roughScheduleOutput: HashMap<RoughScheduleObj, Double> = HashMap();

        var itemCount = 1
        for (fg: FinishedGood in fgList) {
            val roughScheduleRecord: RoughScheduleObj = roughScheduleByItem(fg);
            itemCount++
            roughScheduleOutput.put(roughScheduleRecord, roughScheduleRecord.totalDifference);
        }

        //完成初步fg粗排後檢查調整
        for (i in 0 until 12) {
            var accProductionCount = 0L;

            // Sort by totalDifference before processing
            val sortedEntries = roughScheduleOutput.entries.sortedBy { it.value }

            //原排產
            var itemCount = 1L;
            for ((roughScheduleRecord, totalDifference) in sortedEntries) {
                //記錄粗排優先順序(以最少庫存缺口最大優先)
                roughScheduleRecord.itemPriority[i] = itemCount;
                itemCount++;

                if (accProductionCount + ceil(roughScheduleRecord.productionSchedule[i]).toLong() <= 22000) { //沒有超過每日產量
                    accProductionCount += ceil(roughScheduleRecord.productionSchedule[i]).toLong() //分開日子算

                    roughScheduleRecord.totalDifference += roughScheduleRecord.productionSchedule[i]
                    roughScheduleOutput[roughScheduleRecord] = roughScheduleRecord.totalDifference

                } else {//超過每日產量
                    var maxExtraProduction = Math.max((22000L - accProductionCount), 0L);
                    roughScheduleRecord.totalForgoneCount =
                        roughScheduleRecord.totalForgoneCount + roughScheduleRecord.productionSchedule[i] - maxExtraProduction;
                    roughScheduleRecord.productionSchedule[i] = maxExtraProduction.toDouble();
                    accProductionCount += maxExtraProduction;

                    //update close balance
                    roughScheduleRecord.closeBalance[i] =
                        roughScheduleRecord.closeBalance[i] - roughScheduleRecord.productionSchedule[i] + maxExtraProduction.toDouble();
                    for (j in i + 1 until 12) {
                        roughScheduleRecord.closeBalance[j] =
                            roughScheduleRecord.closeBalance[j] - roughScheduleRecord.productionSchedule[i] + maxExtraProduction.toDouble();
                    }

                    // Update totalDifference in roughScheduleRecord
                    roughScheduleRecord.totalDifference += (roughScheduleRecord.productionSchedule[i] - maxExtraProduction.toDouble());
                    roughScheduleOutput[roughScheduleRecord] = roughScheduleRecord.totalDifference
                }

            }

            //解決拖欠的數量
            for ((roughScheduleRecord, totalDifference) in sortedEntries) {
                if (accProductionCount + ceil(roughScheduleRecord.productionSchedule[i]).toLong() <= 22000) { //沒有超過每日產量
                    if (roughScheduleRecord.totalForgoneCount > 0.0) {
                        if (accProductionCount + ceil(roughScheduleRecord.totalForgoneCount).toLong() <= 22000) {
                            //可以全做
                            roughScheduleRecord.productionSchedule[i] =
                                roughScheduleRecord.productionSchedule[i] + ceil(roughScheduleRecord.totalForgoneCount).toLong();

                            accProductionCount += ceil(roughScheduleRecord.totalForgoneCount).toLong();

                            roughScheduleRecord.totalDifference += roughScheduleRecord.totalForgoneCount;
                            roughScheduleOutput[roughScheduleRecord] = roughScheduleRecord.totalDifference

                            //update close balance
                            for (j in i until 12) {
                                roughScheduleRecord.closeBalance[j] =
                                    roughScheduleRecord.closeBalance[j] + roughScheduleRecord.totalForgoneCount;
                            }
                            roughScheduleRecord.totalForgoneCount = 0.0;
                        } else {
                            //只能做部分
                            var maxExtraProduction = Math.max((22000L - accProductionCount), 0L);
                            roughScheduleRecord.totalForgoneCount =
                                Math.max((roughScheduleRecord.totalForgoneCount - maxExtraProduction), 0.0);

                            roughScheduleRecord.productionSchedule[i] =
                                roughScheduleRecord.productionSchedule[i] + maxExtraProduction;
                            accProductionCount += maxExtraProduction;
                            roughScheduleRecord.totalDifference += maxExtraProduction;

                            //update close balance
                            for (j in i until 12) {
                                roughScheduleRecord.closeBalance[j] =
                                    roughScheduleRecord.closeBalance[j] + maxExtraProduction;
                            }
                        }
                    }

                }
            }

            //debug use
            for ((roughScheduleRecord, totalDifference) in sortedEntries) {
                println("[RUN" + i + "][index:" + totalDifference + "] - " + roughScheduleRecord.fgDetail?.name + " - " + roughScheduleRecord.productionSchedule[i]);
            }
            println("Total Production Count $accProductionCount");
            println("========================================");

        }

        return roughScheduleOutput
    }

    open fun roughScheduleByItem(fg: FinishedGood): RoughScheduleObj {
        val minStockCount: Double = fg.lastMonthAvgSalesCount * 2 //TODO: change to multiple by user config setting
        val stockDifference: Double = minStockCount - fg.openBalance
        val productionSchedule: Array<Double> = Array<Double>(12, { 0.0 })

        //首日粗排產量 —＞ 取結果，或是fg本身生產上限,並不可小於0
        productionSchedule[0] = Math.max(
            ceil(
                Math.min(
                    (stockDifference + fg.lastMonthAvgSalesCount),
                    fg.fgProductionLimit
                )
            ),
            0.0
        )

        val closeBalance: Array<Double> = Array<Double>(12, { 0.0 })
        closeBalance[0] = /*Math.floor(*/fg.openBalance + productionSchedule[0] - fg.lastMonthAvgSalesCount/*)*/;

        for (i in 1 until 12) {
            //最少庫存-closeBalance 有可能小於0，所以必須要確保輸出>=0
            productionSchedule[i] = Math.max(
                ceil(
                    Math.min(
                        (minStockCount - closeBalance[i - 1] + fg.lastMonthAvgSalesCount),
                        fg.fgProductionLimit
                    )
                ),
                0.0
            )
            closeBalance[i] = /*Math.floor(*/
                closeBalance[i - 1] + productionSchedule[i] - fg.lastMonthAvgSalesCount/*)*/;
        }

        var totalProductionCount: Double = 0.0

        for (i in 5 until 12) {
            totalProductionCount = totalProductionCount + productionSchedule[i];
        }

        val totalDifference: Double = -(totalProductionCount - fg.openBalance);


        return RoughScheduleObj(
            fg,
            productionSchedule,
            closeBalance,
            totalDifference,
            totalProductionCount,
            0.0
        );
    }

    open fun saveRoughScheduleOutput(
        sortedEntries: List<Map.Entry<RoughScheduleObj, Double>>,
        accProdCount: Double,
        fgCount: Long
    ) {
        val tempObj = ProductionSchedule()
        tempObj.id = -1;
        tempObj.scheduleAt = LocalDateTime.now()
        tempObj.totalFGType = fgCount;
        tempObj.totalEstProdCount = accProdCount;
        tempObj.type = "rough"
        tempObj.id = saveProductionScheduleToDatabase(tempObj);

        for ((roughScheduleRecord, totalDifference) in sortedEntries) {
            saveRoughScheduleLineToDatabase(tempObj.id ?: -1, roughScheduleRecord)
        }
    }

    private fun saveProductionScheduleToDatabase(newRecord: ProductionSchedule): Long? {
        try {
            val savedItem = productionScheduleRepository.saveAndFlush(newRecord)
            return savedItem.id
        } catch (e: Exception) {
            throw RuntimeException("Error saving production schedule: ${e.message}", e)
        }
    }

    private fun saveRoughScheduleLineToDatabase(parentId: Long, roughScheduleObj: RoughScheduleObj) {
        for (i in 5 until 12) {
            try {
                val prodSchedule = productionScheduleRepository.findById(parentId).get()
                val item = roughScheduleObj.fgDetail?.id?.let { itemService.findById(it) }
                var savedItem = ProductionScheduleLine()
                savedItem.id = -1;
//                savedItem.prodScheduleId = parentId;
                savedItem.productionSchedule = prodSchedule;
                if (item != null) {
                    savedItem.item = item
                };
                savedItem.lastMonthAvgSales = roughScheduleObj.fgDetail?.lastMonthAvgSalesCount ?: 0.0;
                savedItem.refScheduleId = null;
                savedItem.approverId = null;
                savedItem.estCloseBal = roughScheduleObj.closeBalance[i]
                savedItem.prodQty = roughScheduleObj.productionSchedule[i]
                savedItem.type = "rough"
                savedItem.assignDate = i - 4L;
                savedItem.itemPriority = roughScheduleObj.itemPriority[i]
                productionScheduleLineRepository.saveAndFlush(savedItem)

            } catch (e: Exception) {
                throw RuntimeException("Error saving production schedule line: ${e.message}", e)
            }
        }

    }
    //====================粗排相關 END====================//

    open class FinishedGood {
        //var fgDetail: FinishedGood? = null
        var id: Long? = null
        var name: String? = null
        var receipeId: Long? = null
        var openBalance: Double = 0.0
        var lastMonthAvgSalesCount: Double = 0.0
        var fgProductionLimit: Double = 22000.0

        constructor(
            id: Long?,
            name: String?,
            receipeId: Long?,
            openBalance: Double,
            lastMonthAvgSalesCount: Double,
            fgProductionLimit: Double
        ) {
            this.id = id
            this.name = name
            this.receipeId = receipeId
            this.openBalance = openBalance
            this.lastMonthAvgSalesCount = lastMonthAvgSalesCount
            this.fgProductionLimit = fgProductionLimit
        }
    }

    open class RoughScheduleObj {
        var fgDetail: FinishedGood? = null
        var productionSchedule: Array<Double> = Array<Double>(12, { 0.0 })
        var closeBalance: Array<Double> = Array(12) { 0.0 };
        var itemPriority: Array<Long> = Array(12) { 0 };
        var totalDifference: Double = 0.0
        var totalProductionCount: Double = 0.0
        var totalForgoneCount: Double = 0.0


        constructor(
            fgDetail: FinishedGood?,
            productionSchedule: Array<Double>,
            closeBalance: Array<Double>,
            totalDifference: Double,
            totalProductionCount: Double,
            totalForgoneCount: Double
        ) {
            this.fgDetail = fgDetail
            this.productionSchedule = productionSchedule
            this.closeBalance = closeBalance
            this.totalDifference = totalDifference
            this.totalProductionCount = totalProductionCount
            this.totalForgoneCount = totalForgoneCount
        }

        override fun toString(): String {
            return "RoughScheduleObj(fgDetail=$fgDetail, productionSchedule=${productionSchedule.contentToString()}, closeBalance=${closeBalance.contentToString()}, totalDifference=$totalDifference, totalProductionCount=$totalProductionCount, totalForgoneCount=$totalForgoneCount)"
        }

    }

    fun exportProdScheduleToExcel(
        fromDate: LocalDate,
        lines: List<Map<String, Any>>,
        lineMats: List<Map<String, Any>>,
        lineDailys: List<Map<String, Any>>
    ): ByteArray {
        val workbook = XSSFWorkbook()

        // ── Common Styles ──
        val headerStyle = workbook.createCellStyle().apply {
            fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
            fillPattern = FillPatternType.SOLID_FOREGROUND
            wrapText = true
            verticalAlignment = VerticalAlignment.CENTER
            val font = workbook.createFont()
            font.bold = true
            setFont(font)
        }

        val wrapStyle = workbook.createCellStyle().apply {
            wrapText = true
            verticalAlignment = VerticalAlignment.TOP
        }

        // Number style with thousands separator (integer)
        val numberStyle = workbook.createCellStyle().apply {
            wrapText = true
            verticalAlignment = VerticalAlignment.TOP
            dataFormat = workbook.createDataFormat().getFormat("#,##0")
        }

        val numberDigitStyle = workbook.createCellStyle().apply {
            wrapText = true
            verticalAlignment = VerticalAlignment.TOP
            dataFormat = workbook.createDataFormat().getFormat("#,##0.0")
        }

        val daysLeftLowStyle = workbook.createCellStyle().apply {
            cloneStyleFrom(numberDigitStyle)
            fillForegroundColor = IndexedColors.RED.index
            fillPattern = FillPatternType.SOLID_FOREGROUND
            val font = workbook.createFont()
            font.color = IndexedColors.WHITE.index
            font.bold = true
            setFont(font)
        }

        // ── Group production lines by date ──
        val groupedData = lines.groupBy {
            val produceAt = it["produceAt"]
            when (produceAt) {
                is LocalDateTime -> produceAt.toLocalDate().toString()
                is Timestamp -> produceAt.toLocalDateTime().toLocalDate().toString()
                is String -> produceAt.take(10)
                else -> produceAt?.toString()?.substring(0, 10) ?: "Unknown_Date"
            }
        }

        // ── Production sheets (one per date) ──
        groupedData.forEach { (dateKey, dailyLines) ->
            val safeSheetName = dateKey.replace(Regex("[/\\\\?*:\\[\\]]"), "-").take(31)
            val sheet = workbook.createSheet(safeSheetName)

            val headers = listOf(
                "物料編號", "物料名稱", "每日平均出貨量", "出貨前預計存貨量", "可用日",
                "每批數量", "生產量(批)", "優先度"
            )

            // Header row
            val headerRow = sheet.createRow(0)
            headers.forEachIndexed { i, title ->
                headerRow.createCell(i).apply {
                    setCellValue(title)
                    cellStyle = headerStyle
                }
            }

            // Data rows
            dailyLines.forEachIndexed { index, line ->
                val row = sheet.createRow(index + 1)
                row.heightInPoints = 35f

                var j = 0
                row.createCell(j++).apply { setCellValue(line["itemCode"]?.toString() ?: ""); cellStyle = wrapStyle }
                row.createCell(j++).apply { setCellValue(line["itemName"]?.toString() ?: ""); cellStyle = wrapStyle }
                row.createCell(j++).apply { setCellValue(asDouble(line["avgQtyLastMonth"])); cellStyle = numberStyle }
                row.createCell(j++).apply { setCellValue(asDouble(line["stockQty"])); cellStyle = numberStyle }
                val daysLeftVal = asDouble(line["daysLeft"])
                row.createCell(j++).apply {
                    setCellValue(daysLeftVal)
                    cellStyle = if (daysLeftVal < 1.0) daysLeftLowStyle else numberDigitStyle
                }
                row.createCell(j++).apply { setCellValue(asDouble(line["outputdQty"] ?: line["outputQty"])); cellStyle = numberStyle }
                row.createCell(j++).apply { setCellValue(asDouble(line["batchNeed"])); cellStyle = numberStyle }
                row.createCell(j++).apply { setCellValue(asDouble(line["itemPriority"])); cellStyle = numberStyle }
            }

            // ── FIXED COLUMN WIDTHS ── (much wider, especially for text columns)
            sheet.setColumnWidth(0, 24 * 256)   // 物料編號     - wider for codes like ABC-12345-XYZ
            sheet.setColumnWidth(1, 90 * 256)   // 物料名稱     - very generous for long Chinese descriptions
            sheet.setColumnWidth(2, 20 * 256)   // 每日平均出貨量
            sheet.setColumnWidth(3, 22 * 256)   // 出貨前預計存貨量
            sheet.setColumnWidth(4, 14 * 256)   // 可用日
            sheet.setColumnWidth(5, 18 * 256)   // 每批數量
            sheet.setColumnWidth(6, 18 * 256)   // 生產量(批)
            sheet.setColumnWidth(7, 14 * 256)   // 優先度

            // Optional: still allow a little auto-sizing on the longest text column
            sheet.autoSizeColumn(1)
            // But cap it so it doesn't become excessively wide
            if (sheet.getColumnWidth(1) > 110 * 256) {
                sheet.setColumnWidth(1, 110 * 256)
            }

            // ── Print setup - Portrait ──
            val printSetup = sheet.printSetup
            printSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE
            printSetup.landscape = false
            printSetup.fitWidth = 1.toShort()
            printSetup.fitHeight = 0.toShort()
            sheet.fitToPage = true
            sheet.horizontallyCenter = true
            sheet.setMargin(Sheet.LeftMargin, 0.5)
            sheet.setMargin(Sheet.RightMargin, 0.5)
            sheet.setMargin(Sheet.TopMargin, 0.7)
            sheet.setMargin(Sheet.BottomMargin, 0.7)
        }

        // ── MATERIAL SUMMARY SHEET ──
        val matSheet = workbook.createSheet("物料用量總結")

        val matHeaders = listOf(
            "物料編號", "物料名稱", "需求量", "總用量",
            "單位", "數量(採購單位)", "採購單位",
            "已採購數量", "庫存數", "庫存單位",
            "相關成品編號", "相關成品名稱"
        )

        val matHeaderRow = matSheet.createRow(0)
        matHeaders.forEachIndexed { i, title ->
            matHeaderRow.createCell(i).apply {
                setCellValue(title)
                cellStyle = headerStyle
            }
        }

        lineMats.forEachIndexed { index, rowData ->
            val row = matSheet.createRow(index + 1)
            row.heightInPoints = 35f

            val totalNeed = asDouble(rowData["totalMatQtyNeed"])
            val purchaseQtyNeed = asDouble(rowData["purchaseQtyNeed"])
            val purchased = asDouble(rowData["purchasedQty"])
            val onHand = asDouble(rowData["onHandQty"])
            val requiredQty = (totalNeed - purchased - onHand).coerceAtLeast(0.0)

            val values = listOf<Any>(
                rowData["matCode"]?.toString() ?: "",
                rowData["matName"]?.toString() ?: "",
                requiredQty,
                totalNeed,
                rowData["uomName"]?.toString() ?: "",
                purchaseQtyNeed,
                rowData["purchaseUnit"]?.toString() ?: "",
                purchased,
                onHand,
                rowData["stockUnit"]?.toString() ?: "",
                rowData["itemCode"]?.toString() ?: "",
                rowData["itemName"]?.toString() ?: ""
            )

            values.forEachIndexed { i, value ->
                val cell = row.createCell(i)
                when (value) {
                    is String -> {
                        cell.setCellValue(value)
                        cell.cellStyle = wrapStyle
                    }
                    is Number -> {
                        cell.setCellValue(value.toDouble())
                        cell.cellStyle = when (i) {
                            2, 3, 5, 7, 8 -> numberStyle
                            else -> wrapStyle
                        }
                    }
                    else -> {
                        cell.setCellValue("")
                        cell.cellStyle = wrapStyle
                    }
                }
            }
        }

        // Column widths for material summary
        matSheet.setColumnWidth(0, 20 * 256)
        matSheet.setColumnWidth(1, 50 * 256)   // 物料名稱
        matSheet.setColumnWidth(2, 16 * 256)
        matSheet.setColumnWidth(3, 16 * 256)
        matSheet.setColumnWidth(4, 12 * 256)
        matSheet.setColumnWidth(5, 16 * 256)
        matSheet.setColumnWidth(6, 12 * 256)
        matSheet.setColumnWidth(7, 16 * 256)
        matSheet.setColumnWidth(8, 16 * 256)
        matSheet.setColumnWidth(9, 12 * 256)
        matSheet.setColumnWidth(10, 22 * 256)
        matSheet.setColumnWidth(11, 60 * 256)  // 相關成品名稱 - wider

        // Portrait print setup
        val matPrintSetup = matSheet.printSetup
        matPrintSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE
        matPrintSetup.landscape = false
        matPrintSetup.fitWidth = 1.toShort()
        matPrintSetup.fitHeight = 0.toShort()
        matSheet.fitToPage = true
        matSheet.horizontallyCenter = true
        matSheet.setMargin(Sheet.LeftMargin, 0.5)
        matSheet.setMargin(Sheet.RightMargin, 0.5)
        matSheet.setMargin(Sheet.TopMargin, 0.7)
        matSheet.setMargin(Sheet.BottomMargin, 0.7)

        // ── DAILY MATERIAL NEEDS SHEET ──
        val dailySheet = workbook.createSheet("每天物料用量")

        val dateFormatterDayMonth = DateTimeFormatter.ofPattern("d MMM", Locale.ENGLISH)
        val dateFormatterDayName = DateTimeFormatter.ofPattern("EEEE", Locale.ENGLISH)

        val dateInfo = (1..9).map { offset ->
            val date = fromDate.plusDays(offset.toLong())
            Pair(date.format(dateFormatterDayMonth), date.format(dateFormatterDayName))
        }

        // Two-row headers
        val headerRow1 = dailySheet.createRow(0)
        val headerRow2 = dailySheet.createRow(1)

        val baseHeaders = listOf("物料編號", "物料名稱", "採購單位")

        baseHeaders.forEachIndexed { i, title ->
            headerRow1.createCell(i).apply { setCellValue(title); cellStyle = headerStyle }
            headerRow2.createCell(i).apply { cellStyle = headerStyle }
        }

        dateInfo.forEachIndexed { index, (dayMonth, dayName) ->
            val col = baseHeaders.size + index
            headerRow1.createCell(col).apply { setCellValue(dayMonth); cellStyle = headerStyle }
            headerRow2.createCell(col).apply { setCellValue(dayName); cellStyle = headerStyle }
        }

        for (i in baseHeaders.indices) {
            dailySheet.addMergedRegion(CellRangeAddress(0, 1, i, i))
        }

        // Data rows
        lineDailys.forEachIndexed { index, rowData ->
            val row = dailySheet.createRow(index + 2)
            row.heightInPoints = 30f

            val baseValues = listOf<Any>(
                rowData["matCode"]?.toString() ?: "",
                rowData["matName"]?.toString() ?: "",
                rowData["uom"]?.toString() ?: ""
            )

            val dailyQty = (1..9).map { offset ->
                asDouble(rowData["day$offset"])
            }

            val allValues = baseValues + dailyQty

            allValues.forEachIndexed { i, value ->
                val cell = row.createCell(i)
                when (value) {
                    is String -> {
                        cell.setCellValue(value)
                        cell.cellStyle = wrapStyle
                    }
                    is Number -> {
                        cell.setCellValue(value.toDouble())
                        cell.cellStyle = if (i >= baseHeaders.size) numberStyle else wrapStyle
                    }
                    else -> {
                        cell.setCellValue(0.0)
                        cell.cellStyle = numberStyle
                    }
                }
            }
        }

        // Column widths for daily sheet
        dailySheet.setColumnWidth(0, 20 * 256)   // 物料編號
        dailySheet.setColumnWidth(1, 55 * 256)   // 物料名稱
        dailySheet.setColumnWidth(2, 12 * 256)   // 採購單位
        for (i in baseHeaders.size until baseHeaders.size + 9) {
            dailySheet.setColumnWidth(i, 16 * 256)   // daily qty columns
        }

        // Print setup
        val dailyPrintSetup = dailySheet.printSetup
        dailyPrintSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE
        dailyPrintSetup.landscape = false
        dailyPrintSetup.fitWidth = 1.toShort()
        dailyPrintSetup.fitHeight = 0.toShort()
        dailySheet.fitToPage = true
        dailySheet.horizontallyCenter = true
        dailySheet.setMargin(Sheet.TopMargin, 0.7)
        dailySheet.setMargin(Sheet.BottomMargin, 0.7)

        // ── Write to ByteArray ──
        val out = ByteArrayOutputStream()
        workbook.use { wb ->
            wb.write(out)
        }
        workbook.close()

        return out.toByteArray()
    }

    private fun asDouble(value: Any?): Double {
        return when (value) {
            is Number -> value.toDouble()
            is String -> value.toDoubleOrNull() ?: 0.0
            else -> 0.0
        }
    }

    //====================細排相關 START====================//
    open fun searchExportProdSchedule(fromDate: LocalDate): List<Map<String, Any>> {

        val args = mapOf(
            "fromDate" to fromDate,
        )

        val sql = """
             select 
                it.code as itemCode,
                it.name as itemName,
                ps.produceAt,
                psl.* 
                
            from production_schedule_line psl
            left join production_schedule ps on psl.prodScheduleId = ps.id
            left join items it on psl.itemId = it.id

            where ps.produceAt >= :fromDate

            and ps.id = (select id from production_schedule where produceAt = ps.produceAt order by id desc limit 1)

            order by ps.produceAt asc, psl.itemPriority desc, itemCode asc
        """;

      
        return jdbcDao.queryForList(sql, args);
        
    }


    open fun searchExportProdScheduleMaterial(fromDate: LocalDate): List<Map<String, Any>> {

        val args = mapOf(
            "fromDate" to fromDate,
        )

        val sql = """
             select 
                group_concat(distinct it.code) as itemCode,
                group_concat(distinct it.name) as itemName,
                itm.code as matCode,
                itm.name as matName,
                sum(bm.qty * psl.batchNeed) as totalMatQtyNeed,
                iv.onHandQty,
                iv.unavailableQty,
                (select sum(qty) from purchase_order_line
                left join purchase_order on purchase_order_line.purchaseOrderId = purchase_order.id
                where purchase_order_line.itemId = itm.id and date(purchase_order.estimatedArrivalDate) >= date(now()) and purchase_order.completeDate is null) as purchasedQty,
                bm.uomName,
                (select ratioD/ratioN from item_uom where purchaseUnit = 1 and itemId = it.id and item_uom.deleted = 0
                    ) as purchaseRatio,
				(select ceil(ratioD/ratioN*sum(bm.qty * psl.batchNeed)) from item_uom where purchaseUnit = 1 and itemId = it.id and item_uom.deleted = 0
                    ) as purchaseQtyNeed,
                (select code from item_uom 
                left join uom_conversion on item_uom.uomId = uom_conversion.id
                where item_uom.purchaseUnit = 1 and item_uom.itemId = it.id and item_uom.deleted = 0
                    ) as purchaseUnit,
                min(ps.produceAt) as toProdcueFrom,
                max(ps.produceAt) as toProdcueAt,
                ucs.udfudesc as stockUnit,
                psl.* 
                
            from production_schedule_line psl
            left join production_schedule ps on psl.prodScheduleId = ps.id
            left join items it on psl.itemId = it.id
            left join bom on it.id = bom.itemId
            left join bom_material bm on bom.id = bm.bomId
            left join items itm on bm.itemId = itm.id 
            left join inventory iv on itm.id = iv.itemId
            left join item_uom ius on itm.id = ius.itemId and ius.stockUnit = 1
            left join uom_conversion ucs on ius.uomId = ucs.id
           
            where ps.produceAt >= :fromDate
            and ps.id = (select id from production_schedule where produceAt = ps.produceAt order by id desc limit 1)
            -- This is water
            AND it.code != 'GI3236'
            --  order by ps.produceAt asc, psl.itemPriority desc, itemCode asc
                group by matCode
        """;

      
        return jdbcDao.queryForList(sql, args);
        
    }

    open fun searchExportDailyMaterial(fromDate: LocalDate): List<Map<String, Any>> {

        val args = mapOf(
            "fromDate" to fromDate,
        )

        val sql = """
             WITH latest_supplier_uom AS (
                SELECT x.materialId, x.uomIdM18
                FROM (
                    SELECT
                        pol.itemId AS materialId,
                        pol.uomIdM18,
                        ROW_NUMBER() OVER (
                            PARTITION BY pol.itemId
                            ORDER BY COALESCE(po.orderDate, po.created) DESC, pol.id DESC
                        ) AS rn
                    FROM purchase_order_line pol
                    JOIN purchase_order po ON po.id = pol.purchaseOrderId
                    WHERE pol.deleted = 0
                      AND po.deleted = 0
                      AND pol.uomIdM18 IS NOT NULL
                ) x
                WHERE x.rn = 1
            ),
            daily_needs AS (
                SELECT
                    itm.id AS materialId,
                    itm.code AS matCode,
                    itm.name AS matName,
                    COALESCE(uomM18.code, uomP.code) AS uom,
                    ceil((iv.onHandQty * (itsm.ratioN / itsm.ratioD))  * (COALESCE(ium18.ratioD, itum.ratioD) / COALESCE(ium18.ratioN, itum.ratioN))) as onHandQty,
                    ceil((iv.unavailableQty * (itsm.ratioN / itsm.ratioD))  * (COALESCE(ium18.ratioD, itum.ratioD) / COALESCE(ium18.ratioN, itum.ratioN))) as unavailableQty,
                    COALESCE((
                        SELECT ceil(SUM(pol.qty * (itum.ratioN / itum.ratioD) * (COALESCE(ium18.ratioD, itum.ratioD) / COALESCE(ium18.ratioN, itum.ratioN))))
                        FROM purchase_order_line pol
                        JOIN purchase_order po ON pol.purchaseOrderId = po.id
                        WHERE pol.itemId = itm.id
                        AND DATE(po.estimatedArrivalDate) >= :fromDate
                        AND po.completeDate IS NULL
                    ), 0) AS purchasedQty,
                    DATE(ps.produceAt) AS produceDate,
                    ceil( SUM(bm.baseQty * psl.batchNeed) * (COALESCE(ium18.ratioD, itum.ratioD) / COALESCE(ium18.ratioN, itum.ratioN)) ) AS qtyNeeded
                FROM production_schedule_line psl
                JOIN production_schedule ps ON psl.prodScheduleId = ps.id
                JOIN items it ON psl.itemId = it.id
                JOIN bom ON it.id = bom.itemId
                JOIN bom_material bm ON bom.id = bm.bomId
                JOIN items itm ON bm.itemId = itm.id
                JOIN item_uom itum ON itm.id = itum.itemId and itum.purchaseUnit = 1
                JOIN uom_conversion uomP ON itum.uomId = uomP.id
                LEFT JOIN latest_supplier_uom lsu ON lsu.materialId = itm.id
                LEFT JOIN item_uom ium18 ON ium18.itemId = itm.id AND ium18.uomId = lsu.uomIdM18 AND ium18.deleted = 0
                LEFT JOIN uom_conversion uomM18 ON uomM18.id = ium18.uomId
                JOIN item_uom itsm ON itm.id = itsm.itemId and itsm.stockUnit = 1
                LEFT JOIN inventory iv ON itm.id = iv.itemId
                WHERE DATE(ps.produceAt) >= DATE_ADD(:fromDate, INTERVAL 1 DAY)
                AND DATE(ps.produceAt) <  DATE_ADD(:fromDate, INTERVAL 8 DAY)
                AND ps.id = (
                    SELECT ps2.id
                    FROM production_schedule ps2
                    WHERE DATE(ps2.produceAt) = DATE(ps.produceAt)
                    ORDER BY ps2.id DESC
                    LIMIT 1
                )
                AND bm.itemId IS NOT NULL
                AND itm.code != 'GI3236'
                GROUP BY 
                    itm.id, 
                    DATE(ps.produceAt)
            )

            SELECT
                matCode,
                matName,
                uom,
                onHandQty AS currentStock,
                purchasedQty AS incomingPO,
                (onHandQty + purchasedQty) AS grossAvailable,
                
                -- Dynamic columns for next 7 days
                COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(:fromDate, INTERVAL 1 DAY) THEN qtyNeeded END), 0) AS day1,
                COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(:fromDate, INTERVAL 2 DAY) THEN qtyNeeded END), 0) AS day2,
                COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(:fromDate, INTERVAL 3 DAY) THEN qtyNeeded END), 0) AS day3,
                COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(:fromDate, INTERVAL 4 DAY) THEN qtyNeeded END), 0) AS day4,
                COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(:fromDate, INTERVAL 5 DAY) THEN qtyNeeded END), 0) AS day5,
                COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(:fromDate, INTERVAL 6 DAY) THEN qtyNeeded END), 0) AS day6,
                COALESCE(MAX(CASE WHEN produceDate = DATE_ADD(:fromDate, INTERVAL 7 DAY) THEN qtyNeeded END), 0) AS day7,
                
                -- Total and net
                COALESCE(SUM(qtyNeeded), 0) AS totalNeed7Days,
                (onHandQty + purchasedQty - COALESCE(SUM(qtyNeeded), 0)) AS netAfter7Days

            FROM daily_needs
            GROUP BY 
                materialId, matCode, matName, uom, onHandQty, purchasedQty
            ORDER BY netAfter7Days ASC, totalNeed7Days DESC;
        """;

      
        return jdbcDao.queryForList(sql, args);
        
    }

    @Transactional
    open fun clearTodayAndFutureProdSchedule(produceAt: LocalDateTime) {
        val deleteLinesSql = """
            DELETE FROM production_schedule_line 
            WHERE prodScheduleId IN (
                SELECT id FROM production_schedule 
                WHERE DATE(produceAt) >= DATE(:produceAt)
            )
        """.trimIndent()

        val deleteSchedulesSql = """
            DELETE FROM production_schedule 
            WHERE DATE(produceAt) >= DATE(:produceAt)
        """.trimIndent()

        val params = mapOf("produceAt" to produceAt)

        // Execute child delete first
        jdbcDao.executeUpdate(deleteLinesSql, params)

        // Then delete parent schedules
        jdbcDao.executeUpdate(deleteSchedulesSql, params)

        // Optional: log the action (if you have logging setup)
        // logger.info("Cleared all production schedules with produceAt >= today")
    }
    
}