package com.ffii.fpsms.modules.jobOrder.service

import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.jobOrder.web.model.PrintRequest
import com.ffii.fpsms.modules.jobOrder.web.model.LaserRequest
import org.springframework.stereotype.Service
import java.time.LocalDate

import com.ffii.core.support.JdbcDao
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.IndexedColors
import org.apache.poi.ss.usermodel.VerticalAlignment
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.ByteArrayOutputStream
import java.math.BigDecimal
import java.math.RoundingMode

@Service
open class PSService(
    private val jdbcDao: JdbcDao,
) {
    
    /** Default: 6 days before today to 1 day after today. */
    fun getItemDailyOut(fromDate: LocalDate? = null, toDate: LocalDate? = null): List<Map<String, Any>> {
        val defaultToday = LocalDate.now()
        val to = toDate ?: defaultToday.plusDays(1)
        val from = fromDate ?: defaultToday.minusDays(6)
        val args = mapOf(
            "fromDate" to from.toString(),
            "toDate" to to.toString(),
        )
        val sql = """
            SELECT
                (SELECT dailyQty FROM item_daily_out WHERE itemCode = items.code) AS dailyQty,
                (SELECT
                    IFNULL(ROUND(AVG(d.dailyQty)), 0)
                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 AND deleted = 0) AS isCoffee,
                (SELECT COUNT(1) FROM coffee_or_tea WHERE systemType = 'tea' AND itemCode = items.code AND deleted = 0) AS isTea,
                (SELECT COUNT(1) FROM coffee_or_tea WHERE systemType = 'lemon' AND itemCode = items.code AND deleted = 0) AS isLemon,
                CASE WHEN item_fake_onhand.onHandQty IS NOT NULL THEN item_fake_onhand.onHandQty
                ELSE inventory.onHandQty - 500 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 AS itemCode,
                items.name AS itemName,
                uc_stock.udfudesc AS unit,
                bom.description,
                inventory.onHandQty,
                item_fake_onhand.onHandQty AS fakeOnHandQty,
                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
            LEFT JOIN item_uom iu ON iu.itemId = items.id AND iu.stockUnit = 1
            LEFT JOIN uom_conversion uc_stock ON uc_stock.id = iu.uomId
            WHERE bom.deleted = 0 and bom.description = 'FG'
        """.trimIndent()
        return jdbcDao.queryForList(sql, args)
    }

    /** Update or insert dailyQty for itemCode. */
    fun setDailyQtyOut(itemCode: String, dailyQty: Number) {
        val args = mapOf("itemCode" to itemCode, "dailyQty" to dailyQty)
        val updated = jdbcDao.executeUpdate(
            "UPDATE item_daily_out SET dailyQty = :dailyQty WHERE itemCode = :itemCode",
            args
        )
        if (updated == 0) {
            jdbcDao.executeUpdate(
                "INSERT INTO item_daily_out (itemCode, dailyQty) VALUES (:itemCode, :dailyQty)",
                args
            )
        }
    }

    /** Remove dailyQty override for itemCode (delete row from item_daily_out). */
    fun clearDailyQtyOut(itemCode: String) {
        jdbcDao.executeUpdate(
            "DELETE FROM item_daily_out WHERE itemCode = :itemCode",
            mapOf("itemCode" to itemCode)
        )
    }

    /** Set or clear item_fake_onhand for itemCode. If onHandQty is null, delete the row; else update or insert. */
    fun setFakeOnHand(itemCode: String, onHandQty: Number?) {
        if (onHandQty == null) {
            jdbcDao.executeUpdate(
                "DELETE FROM item_fake_onhand WHERE itemCode = :itemCode",
                mapOf("itemCode" to itemCode)
            )
            return
        }
        val args = mapOf("itemCode" to itemCode, "onHandQty" to onHandQty)
        val updated = jdbcDao.executeUpdate(
            "UPDATE item_fake_onhand SET onHandQty = :onHandQty WHERE itemCode = :itemCode",
            args
        )
        if (updated == 0) {
            jdbcDao.executeUpdate(
                "INSERT INTO item_fake_onhand (itemCode, onHandQty) VALUES (:itemCode, :onHandQty)",
                args
            )
        }
    }

    /**
     * Bulk import fake on-hand: clear table, then insert rows from Excel.
     * rows: list of maps with keys like "itemCode" and "onHandQty".
     */
    fun importFakeOnHand(rows: List<Map<String, Any?>>) {
        // 1. Clear existing overrides
        jdbcDao.executeUpdate("DELETE FROM item_fake_onhand", emptyMap<String, Any>())

        // 2. Normalise and de-duplicate by itemCode (last one wins)
        val byItemCode = LinkedHashMap<String, Number>()

        rows.forEach { row ->
            val itemCode = row["itemCode"]?.toString()?.trim()
            if (itemCode.isNullOrBlank()) return@forEach

            val rawQty = row["onHandQty"]
            val qty: Number? = when (rawQty) {
                null -> null
                is Number -> rawQty
                else -> rawQty.toString().toDoubleOrNull()
            }
            if (qty == null) return@forEach

            byItemCode[itemCode] = qty
        }

        // 3. Insert one row per itemCode
        byItemCode.forEach { (itemCode, qty) ->
            jdbcDao.executeUpdate(
                "INSERT INTO item_fake_onhand (itemCode, onHandQty) VALUES (:itemCode, :onHandQty)",
                mapOf("itemCode" to itemCode, "onHandQty" to qty)
            )
        }
    }

    /** Set or clear coffee_or_tea for itemCode + systemType (coffee / tea / lemon). */
    /**
     * Recalculate [inventory.onHandQty] / hold / unavailable from [inventory_lot_line] for FG BOM items.
     * Same aggregation as pick-issue manual inventory sync.
     */
    fun refreshInventoryOnHandForFgBomItems(): Int {
        val sql = """
            UPDATE inventory i
            INNER JOIN (
                SELECT DISTINCT b.itemId
                FROM bom b
                WHERE b.deleted = 0 AND b.description = 'FG'
            ) bom_items ON bom_items.itemId = i.itemId
            LEFT JOIN (
                SELECT
                    il.itemId,
                    SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS totalOnHandQty,
                    SUM(COALESCE(ill.holdQty, 0)) AS totalOnHoldQty,
                    SUM(CASE
                        WHEN ill.status = 'unavailable'
                        THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)
                        ELSE 0
                    END) AS totalUnavailableQty
                FROM inventory_lot_line ill
                INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = 0
                WHERE ill.deleted = 0
                GROUP BY il.itemId
            ) calc ON calc.itemId = i.itemId
            SET
                i.onHandQty = COALESCE(calc.totalOnHandQty, 0),
                i.onHoldQty = COALESCE(calc.totalOnHoldQty, 0),
                i.unavailableQty = COALESCE(calc.totalUnavailableQty, 0),
                i.status = IF(
                    COALESCE(calc.totalOnHandQty, 0) - COALESCE(calc.totalOnHoldQty, 0) - COALESCE(calc.totalUnavailableQty, 0) > 0,
                    'available',
                    'unavailable'
                ),
                i.modified = NOW(),
                i.modifiedBy = 'ps-refresh-onhand'
            WHERE i.deleted = 0
        """.trimIndent()
        return jdbcDao.executeUpdate(sql, emptyMap<String, Any>())
    }

    fun setCoffeeOrTea(itemCode: String, systemType: String, enabled: Boolean) {
        val args = mapOf("itemCode" to itemCode, "systemType" to systemType)
        jdbcDao.executeUpdate(
            "DELETE FROM coffee_or_tea WHERE itemCode = :itemCode AND systemType = :systemType",
            args
        )
        if (enabled) {
            jdbcDao.executeUpdate(
                "INSERT INTO coffee_or_tea (itemCode, systemType) VALUES (:itemCode, :systemType)",
                args
            )
        }
    }

    fun searchProductionSchedules(produceAt: String): List<Map<String, Any>> {
        val args = mapOf(
            "produceAt" to produceAt,
        )

        val sql = """
            SELECT 
                ps.*
            FROM production_schedule ps
            WHERE 1 
            and ps.produceAt >= :produceAt
            ORDER BY produceAt 
        """.trimIndent()
        
        return jdbcDao.queryForList(sql, args)
    }

    fun getProductionScheduleLines(psId: Int): List<Map<String, Any>> {
        val args = mapOf(
            "psId" to psId,
        )

        val sql = """
            SELECT 
                (select group_concat(distinct code) from job_order where prodScheduleLineId = psl.id) as joCode,
                it.code as itemCode,
                it.name as itemName,
                uc.udfudesc as stockUnit,
                psl.*
            FROM production_schedule_line psl
            left join items it on psl.itemId = it.id
            left join item_uom ib on it.id = ib.itemId and ib.stockUnit = 1
            left join uom_conversion uc on ib.uomId = uc.id
            WHERE psl.prodScheduleId = :psId
            ORDER BY psl.itemPriority DESC, itemCode ASC
        """.trimIndent()
        
        return jdbcDao.queryForList(sql, args)
    }

    /**
     * Items with at least one BOM (deleted = 0), plus stock-unit label.
     */
    fun listBomItemsWithStockUnit(): List<Map<String, Any>> {
        val sql = """
            SELECT DISTINCT
                items.code AS itemCode,
                items.name AS itemName,
                uc_stock.udfudesc AS stockUnit
            FROM bom
            INNER JOIN items ON bom.itemId = items.id AND items.deleted = 0
            LEFT JOIN item_uom iu_stock ON iu_stock.itemId = items.id AND iu_stock.stockUnit = 1 AND iu_stock.deleted = 0
            LEFT JOIN uom_conversion uc_stock ON uc_stock.id = iu_stock.uomId
            WHERE bom.deleted = 0
            ORDER BY items.code
        """.trimIndent()
        return jdbcDao.queryForList(sql, emptyMap<String, Any>())
    }

    /**
     * Sum of [delivery_order_line.qty] (already stored in stock unit) by item and ETA date.
     * Only items that have a BOM.
     */
    fun sumDeliveryOrderQtyByItemAndDate(fromDate: LocalDate, toDate: LocalDate): List<Map<String, Any>> {
        val args = mapOf(
            "fromDate" to fromDate.toString(),
            "toDate" to toDate.toString(),
        )
        val sql = """
            SELECT
                items.code AS itemCode,
                DATE(do.estimatedArrivalDate) AS shipDate,
                SUM(COALESCE(dol.qty, 0)) AS qtySum
            FROM delivery_order do
            INNER JOIN delivery_order_line dol ON dol.deliveryOrderId = do.id AND dol.deleted = 0
            INNER JOIN items ON items.id = dol.itemId AND items.deleted = 0
            WHERE do.deleted = 0
                AND do.estimatedArrivalDate IS NOT NULL
                AND DATE(do.estimatedArrivalDate) >= :fromDate
                AND DATE(do.estimatedArrivalDate) <= :toDate
                AND EXISTS (
                    SELECT 1 FROM bom b
                    WHERE b.itemId = items.id AND b.deleted = 0
                )
            GROUP BY items.code, DATE(do.estimatedArrivalDate)
            ORDER BY items.code, shipDate
        """.trimIndent()
        return jdbcDao.queryForList(sql, args)
    }

    fun exportDeliveryOrderQtyByDateExcel(fromDate: LocalDate, toDate: LocalDate): ByteArray {
        require(!fromDate.isAfter(toDate)) { "fromDate must be on or before toDate" }
        val dayCount = java.time.temporal.ChronoUnit.DAYS.between(fromDate, toDate) + 1
        require(dayCount in 1..366) { "Date range must be between 1 and 366 days" }

        val dates = generateSequence(fromDate) { it.plusDays(1) }.takeWhile { !it.isAfter(toDate) }.toList()
        val items = listBomItemsWithStockUnit()
        val qtyRows = sumDeliveryOrderQtyByItemAndDate(fromDate, toDate)

        val qtyByItemDate = mutableMapOf<String, MutableMap<LocalDate, BigDecimal>>()
        qtyRows.forEach { row ->
            val itemCode = row["itemCode"]?.toString() ?: return@forEach
            val shipDate = parseSqlDate(row["shipDate"]) ?: return@forEach
            val qty = toBigDecimal(row["qtySum"])
            qtyByItemDate.getOrPut(itemCode) { mutableMapOf() }[shipDate] = qty
        }

        val workbook = XSSFWorkbook()
        val sheet = workbook.createSheet("DO Qty by Date")

        val headerStyle = workbook.createCellStyle().apply {
            fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
            fillPattern = FillPatternType.SOLID_FOREGROUND
            verticalAlignment = VerticalAlignment.CENTER
            val font = workbook.createFont()
            font.bold = true
            setFont(font)
        }
        val textStyle = workbook.createCellStyle().apply {
            verticalAlignment = VerticalAlignment.CENTER
        }
        val numberStyle = workbook.createCellStyle().apply {
            verticalAlignment = VerticalAlignment.CENTER
            dataFormat = workbook.createDataFormat().getFormat("#,##0")
        }

        val headerRow = sheet.createRow(0)
        val headers = mutableListOf("Item Code", "Item Name", "UOM")
        headers.addAll(dates.map { it.toString() })
        headers.forEachIndexed { col, title ->
            headerRow.createCell(col).apply {
                setCellValue(title)
                cellStyle = headerStyle
            }
        }

        items.forEachIndexed { index, item ->
            val row = sheet.createRow(index + 1)
            val itemCode = item["itemCode"]?.toString() ?: ""
            val itemName = item["itemName"]?.toString() ?: ""
            val stockUnit = item["stockUnit"]?.toString() ?: ""

            row.createCell(0).apply { setCellValue(itemCode); cellStyle = textStyle }
            row.createCell(1).apply { setCellValue(itemName); cellStyle = textStyle }
            row.createCell(2).apply { setCellValue(stockUnit); cellStyle = textStyle }

            val dateQtyMap = qtyByItemDate[itemCode]
            dates.forEachIndexed { dateIdx, date ->
                val qty = (dateQtyMap?.get(date) ?: BigDecimal.ZERO)
                    .setScale(0, RoundingMode.HALF_UP)
                row.createCell(3 + dateIdx).apply {
                    setCellValue(qty.toLong().toDouble())
                    cellStyle = numberStyle
                }
            }
        }

        for (col in 0 until headers.size) {
            sheet.autoSizeColumn(col)
        }

        ByteArrayOutputStream().use { out ->
            workbook.write(out)
            workbook.close()
            return out.toByteArray()
        }
    }

    private fun parseSqlDate(value: Any?): LocalDate? = when (value) {
        null -> null
        is LocalDate -> value
        is java.sql.Date -> value.toLocalDate()
        is java.time.LocalDateTime -> value.toLocalDate()
        else -> {
            val text = value.toString().trim()
            if (text.length >= 10) LocalDate.parse(text.substring(0, 10)) else null
        }
    }

    private fun toBigDecimal(value: Any?): BigDecimal = when (value) {
        null -> BigDecimal.ZERO
        is BigDecimal -> value
        is Number -> BigDecimal.valueOf(value.toDouble())
        else -> value.toString().toBigDecimalOrNull() ?: BigDecimal.ZERO
    }

}