package com.ffii.fpsms.modules.report.web

import net.sf.jasperreports.engine.*
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
import org.apache.poi.ss.usermodel.BorderStyle
import org.apache.poi.ss.usermodel.CellStyle
import org.apache.poi.ss.usermodel.DataFormat
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.HorizontalAlignment
import org.apache.poi.ss.usermodel.IndexedColors
import org.apache.poi.ss.usermodel.Row
import org.apache.poi.ss.usermodel.VerticalAlignment
import org.apache.poi.ss.util.CellRangeAddress
import org.apache.poi.ss.util.WorkbookUtil
import org.apache.poi.xssf.usermodel.XSSFCellStyle
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.http.*
import org.springframework.web.bind.annotation.*
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.report.service.M18BomShopSyncReportService
import com.ffii.fpsms.modules.report.service.ReportService

@RestController
@RequestMapping("/report")
class ReportController(
     private val reportService: ReportService,
     private val m18BomShopSyncReportService: M18BomShopSyncReportService,
) {
    private data class ExcelStyles(
        val title: XSSFCellStyle,
        val subtitle: XSSFCellStyle,
        val header: XSSFCellStyle,
        val text: XSSFCellStyle,
        val center: XSSFCellStyle,
        val number: XSSFCellStyle,
        val int: XSSFCellStyle,
        val dash: XSSFCellStyle,
        val sumQty: XSSFCellStyle,
        val sumLabel: XSSFCellStyle,
        val sumEmpty: XSSFCellStyle,
        val sumHidden: XSSFCellStyle,
    )

    @GetMapping("/stock-take-rounds")
    fun getStockTakeRounds(): List<Map<String, Any>> =
        reportService.getStockTakeRoundOptions()

    @GetMapping("/print-report1")
    fun generateReport1(
        @RequestParam fromDate: String,
        @RequestParam toDate: String,
        //this is an exampe of optional param
        @RequestParam(required = false) itemType: String?
    ): ResponseEntity<ByteArray> {
        val parameters = mutableMapOf<String, Any>()
        parameters["fromDate"] = fromDate
        parameters["toDate"] = toDate
        // you may put more params to show
        parameters["param1"] = "param1Value"
        parameters["param2"] = "param2Value"

        // Query the DB to get a list of data
        val dbData = reportService.searchReport1(fromDate, toDate, itemType)

        val pdfBytes = reportService.createPdfResponse(
            "/jasper/report1.jrxml", 
            parameters, 
            dbData
        )

        val headers = HttpHeaders().apply {
            contentType = MediaType.APPLICATION_PDF
            setContentDispositionFormData("attachment", "Report1.pdf")
            set("filename", "Report1.pdf") 
        }

        return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
    }

    @GetMapping("/print-report3")
    fun generateReport3(
        //@RequestParam fromDate: String,
       // @RequestParam toDate: String,
        //this is an exampe of optional param
        @RequestParam(required = false) itemType: String?
    ): ResponseEntity<ByteArray> {
        val parameters = mutableMapOf<String, Any>()
        //parameters["lastInDateStart"] = fromDate
        //parameters["lastInDateEnd"] = toDate
        // you may put more params to show
        parameters["lastOutDateStart"] = "param1Value"
        parameters["lastOutDateEnd"] = "param2Value"

        // Query the DB to get a list of data
        val dbData = reportService.searchReport3(itemType)

        val pdfBytes = reportService.createPdfResponse(
            "/jasper/StockBalanceReport.jrxml", 
            parameters, 
            dbData
        )

        val headers = HttpHeaders().apply {
            contentType = MediaType.APPLICATION_PDF
            setContentDispositionFormData("attachment", "Report1.pdf")
            set("filename", "Report1.pdf") 
        }

        return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
    }

    @GetMapping("/print-stock-in-traceability")
    fun generateStockInTraceabilityReport(
        @RequestParam(required = false) stockCategory: String?,
        @RequestParam(required = false) itemCode: String?,
        @RequestParam(required = false) lastInDateStart: String?,
        @RequestParam(required = false) lastInDateEnd: String?
    ): ResponseEntity<ByteArray> {
        val parameters = mutableMapOf<String, Any>()
        
        // Set report header parameters
        parameters["itemNo"] = itemCode ?: "All"
        parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
        parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))
        parameters["lastInDateStart"] = lastInDateStart ?: ""
        parameters["lastInDateEnd"] = lastInDateEnd ?: ""

        // Query the DB to get a list of data
        val dbData = reportService.searchStockInTraceabilityReport(
            stockCategory,
            itemCode,
            lastInDateStart,
            lastInDateEnd
        )

        val pdfBytes = reportService.createPdfResponse(
            "/jasper/StockInTraceabilityReport.jrxml", 
            parameters, 
            dbData
        )

        val headers = HttpHeaders().apply {
            contentType = MediaType.APPLICATION_PDF
            setContentDispositionFormData("attachment", "StockInTraceabilityReport.pdf")
            set("filename", "StockInTraceabilityReport.pdf") 
        }

        return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
    }

    @GetMapping("/print-stock-in-traceability-excel")
    fun exportStockInTraceabilityReportExcel(
        @RequestParam(required = false) stockCategory: String?,
        @RequestParam(required = false) itemCode: String?,
        @RequestParam(required = false) lastInDateStart: String?,
        @RequestParam(required = false) lastInDateEnd: String?,
    ): ResponseEntity<ByteArray> {
        val dbData = reportService.searchStockInTraceabilityReport(
            stockCategory,
            itemCode,
            lastInDateStart,
            lastInDateEnd,
        )

        val excelBytes = createStockInTraceabilityExcel(
            dbData = dbData,
            lastInDateStart = lastInDateStart ?: "",
            lastInDateEnd = lastInDateEnd ?: "",
        )

        val headers = HttpHeaders().apply {
            contentType = MediaType.parseMediaType(
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            )
            setContentDispositionFormData("attachment", "StockInTraceabilityReport.xlsx")
            set("filename", "StockInTraceabilityReport.xlsx")
        }
        return ResponseEntity(excelBytes, headers, HttpStatus.OK)
    }
    @GetMapping("/print-fg-delivery-report")
    fun generateFGDeliveryReport(
        @RequestParam(required = false) stockCategory: String?,
        @RequestParam(required = false) stockSubCategory: String?,
        @RequestParam(required = false) itemCode: String?,
        @RequestParam(required = false) year: String?,
        @RequestParam(required = false) lastOutDateStart: String?,
        @RequestParam(required = false) lastOutDateEnd: String?
    ): ResponseEntity<ByteArray> {
        val parameters = mutableMapOf<String, Any>()
        
        // 设置报表头部参数
        parameters["stockCategory"] = stockCategory ?: "All"
        parameters["stockSubCategory"] = stockSubCategory ?: "All"
        parameters["itemNo"] = itemCode ?: "All"
        parameters["year"] = year ?: LocalDate.now().year.toString()
        parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
        parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))
        parameters["lastOutDateStart"] = lastOutDateStart ?: ""
        parameters["lastOutDateEnd"] = lastOutDateEnd ?: ""
        parameters["deliveryPeriodStart"] = ""
        parameters["deliveryPeriodEnd"] = ""
    
        val dbData = reportService.searchFGDeliveryReport(
            stockCategory, 
            stockSubCategory, 
            itemCode, 
            year, 
            lastOutDateStart, 
            lastOutDateEnd
        )
    
        val pdfBytes = reportService.createPdfResponse(
            "/jasper/FGDeliveryReport.jrxml", 
            parameters, 
            dbData
        )
    
        val headers = HttpHeaders().apply {
            contentType = MediaType.APPLICATION_PDF
            setContentDispositionFormData("attachment", "FGDeliveryReport.pdf")
            set("filename", "FGDeliveryReport.pdf") 
        }
    
        return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
    }

    @GetMapping("/print-fg-delivery-report-excel")
    fun exportFGDeliveryReportExcel(
        @RequestParam(required = false) stockCategory: String?,
        @RequestParam(required = false) stockSubCategory: String?,
        @RequestParam(required = false) itemCode: String?,
        @RequestParam(required = false) year: String?,
        @RequestParam(required = false) lastOutDateStart: String?,
        @RequestParam(required = false) lastOutDateEnd: String?,
    ): ResponseEntity<ByteArray> {
        val dbData = reportService.searchFGDeliveryReport(
            stockCategory,
            stockSubCategory,
            itemCode,
            year,
            lastOutDateStart,
            lastOutDateEnd,
        )

        val excelBytes = createFGDeliveryExcel(
            dbData = dbData,
            year = year ?: LocalDate.now().year.toString(),
            lastOutDateStart = lastOutDateStart ?: "",
            lastOutDateEnd = lastOutDateEnd ?: "",
        )

        val headers = HttpHeaders().apply {
            contentType = MediaType.parseMediaType(
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            )
            setContentDispositionFormData("attachment", "FGDeliveryReport.xlsx")
            set("filename", "FGDeliveryReport.xlsx")
        }
        return ResponseEntity(excelBytes, headers, HttpStatus.OK)
    }
    @GetMapping("/print-stock-balance")
    fun generateStockBalanceReport(
        @RequestParam(required = false) stockCategory: String?,
        @RequestParam(required = false) itemCode: String?,
        @RequestParam(required = false) stockDate: String?,
        @RequestParam(required = false) balanceFilterStart: String?,
        @RequestParam(required = false) balanceFilterEnd: String?,
        @RequestParam(required = false) storeLocation: String?,
        @RequestParam(required = false) lastInDateStart: String?,
        @RequestParam(required = false) lastInDateEnd: String?,
        @RequestParam(required = false) lastOutDateStart: String?,
        @RequestParam(required = false) lastOutDateEnd: String?,
        @RequestParam(required = false, defaultValue = "0") stockTakeRoundId: Long,
    ): ResponseEntity<ByteArray> {
        val parameters = mutableMapOf<String, Any>()
        parameters["stockCategory"] = stockCategory ?: "All"
        parameters["itemNo"] = itemCode ?: "All"
        parameters["reportDate"] = (stockDate ?: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
        parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))
        parameters["storeLocation"] = storeLocation ?: ""
        parameters["balanceFilterStart"] = balanceFilterStart ?: ""
        parameters["balanceFilterEnd"] = balanceFilterEnd ?: ""
        parameters["lastInDateStart"] = lastInDateStart ?: ""
        parameters["lastInDateEnd"] = lastInDateEnd ?: ""
        parameters["lastOutDateStart"] = lastOutDateStart ?: ""
        parameters["lastOutDateEnd"] = lastOutDateEnd ?: ""

        val dbData =
            if (!stockDate.isNullOrBlank()) {
                reportService.searchStockBalanceReportByDate(
                    stockCategory = stockCategory,
                    itemCode = itemCode,
                    stockDate = stockDate,
                    balanceFilterStart = balanceFilterStart,
                    balanceFilterEnd = balanceFilterEnd,
                    storeLocation = storeLocation,
                )
            } else {
                reportService.searchStockBalanceReport(
                    stockCategory,
                    itemCode,
                    balanceFilterStart,
                    balanceFilterEnd,
                    storeLocation,
                    lastInDateStart,
                    lastInDateEnd,
                    lastOutDateStart,
                    lastOutDateEnd,
                    stockTakeRoundId
                )
            }

        val pdfBytes = reportService.createPdfResponse(
            "/jasper/StockBalanceReport.jrxml",
            parameters,
            dbData
        )

        val headers = HttpHeaders().apply {
            contentType = MediaType.APPLICATION_PDF
            setContentDispositionFormData("attachment", "StockBalanceReport.pdf")
            set("filename", "StockBalanceReport.pdf")
        }

        return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
    }

    @GetMapping("/print-stock-balance-excel")
    fun exportStockBalanceReportExcel(
        @RequestParam(required = false) stockCategory: String?,
        @RequestParam(required = false) itemCode: String?,
        @RequestParam(required = false) stockDate: String?,
        @RequestParam(required = false) balanceFilterStart: String?,
        @RequestParam(required = false) balanceFilterEnd: String?,
        @RequestParam(required = false) storeLocation: String?,
        @RequestParam(required = false) lastInDateStart: String?,
        @RequestParam(required = false) lastInDateEnd: String?,
        @RequestParam(required = false) lastOutDateStart: String?,
        @RequestParam(required = false) lastOutDateEnd: String?,
        @RequestParam(required = false, defaultValue = "0") stockTakeRoundId: Long,
    ): ResponseEntity<ByteArray> {
        val dbData =
            if (!stockDate.isNullOrBlank()) {
                reportService.searchStockBalanceReportByDate(
                    stockCategory = stockCategory,
                    itemCode = itemCode,
                    stockDate = stockDate,
                    balanceFilterStart = balanceFilterStart,
                    balanceFilterEnd = balanceFilterEnd,
                    storeLocation = storeLocation,
                )
            } else {
                reportService.searchStockBalanceReport(
                    stockCategory,
                    itemCode,
                    balanceFilterStart,
                    balanceFilterEnd,
                    storeLocation,
                    lastInDateStart,
                    lastInDateEnd,
                    lastOutDateStart,
                    lastOutDateEnd,
                    stockTakeRoundId
                )
            }

        val excelBytes = createStockBalanceExcel(
            dbData = dbData,
            reportDate = (stockDate ?: LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))),
        )

        val headers = HttpHeaders().apply {
            contentType = MediaType.parseMediaType(
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
            )
            setContentDispositionFormData("attachment", "StockBalanceReport.xlsx")
            set("filename", "StockBalanceReport.xlsx")
        }
        return ResponseEntity(excelBytes, headers, HttpStatus.OK)
    }

    private fun createCommonWorkbookStyles(workbook: XSSFWorkbook): ExcelStyles {
        val titleStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.CENTER
            verticalAlignment = VerticalAlignment.CENTER
            val font = workbook.createFont().apply {
                bold = true
                fontHeightInPoints = 14
            }
            setFont(font)
        }
        val subtitleStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.LEFT
            verticalAlignment = VerticalAlignment.CENTER
            val font = workbook.createFont().apply {
                bold = true
                fontHeightInPoints = 12
            }
            setFont(font)
        }
        val headerStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.CENTER
            verticalAlignment = VerticalAlignment.CENTER
            fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
            fillPattern = FillPatternType.SOLID_FOREGROUND
            borderTop = BorderStyle.THIN
            borderBottom = BorderStyle.THIN
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
            val font = workbook.createFont().apply { bold = true }
            setFont(font)
        }
        val textStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.LEFT
            verticalAlignment = VerticalAlignment.CENTER
            borderTop = BorderStyle.THIN
            borderBottom = BorderStyle.THIN
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
        }
        val centerStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.CENTER
            verticalAlignment = VerticalAlignment.CENTER
            borderTop = BorderStyle.THIN
            borderBottom = BorderStyle.THIN
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
        }
        val numberStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.RIGHT
            verticalAlignment = VerticalAlignment.CENTER
            borderTop = BorderStyle.THIN
            borderBottom = BorderStyle.THIN
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
            val df: DataFormat = workbook.createDataFormat()
            dataFormat = df.getFormat("#,##0.00")
        }
        val intStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.RIGHT
            verticalAlignment = VerticalAlignment.CENTER
            borderTop = BorderStyle.THIN
            borderBottom = BorderStyle.THIN
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
            val df: DataFormat = workbook.createDataFormat()
            dataFormat = df.getFormat("#,##0")
        }
        val dashStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.RIGHT
            verticalAlignment = VerticalAlignment.CENTER
            borderTop = BorderStyle.THIN
            borderBottom = BorderStyle.THIN
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
        }
        val summaryQtyThickBottomStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.RIGHT
            verticalAlignment = VerticalAlignment.CENTER
            val df: DataFormat = workbook.createDataFormat()
            dataFormat = df.getFormat("#,##0.00")
            borderTop = BorderStyle.THICK
            borderBottom = BorderStyle.THICK
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
            val font = workbook.createFont().apply { bold = true }
            setFont(font)
        }
        val summaryLabelThickBottomStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.RIGHT
            verticalAlignment = VerticalAlignment.CENTER
            borderTop = BorderStyle.THICK
            borderBottom = BorderStyle.THICK
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
            val font = workbook.createFont().apply { bold = true }
            setFont(font)
        }
        val summaryEmptyThickBottomStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.LEFT
            verticalAlignment = VerticalAlignment.CENTER
            borderTop = BorderStyle.THICK
            borderBottom = BorderStyle.THICK
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
        }
        val summaryHiddenKeyStyle = (workbook.createCellStyle() as XSSFCellStyle).apply {
            alignment = HorizontalAlignment.LEFT
            verticalAlignment = VerticalAlignment.CENTER
            borderTop = BorderStyle.THICK
            borderBottom = BorderStyle.THICK
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
            val font = workbook.createFont().apply { color = IndexedColors.WHITE.index }
            setFont(font)
        }
        return ExcelStyles(
            title = titleStyle,
            subtitle = subtitleStyle,
            header = headerStyle,
            text = textStyle,
            center = centerStyle,
            number = numberStyle,
            int = intStyle,
            dash = dashStyle,
            sumQty = summaryQtyThickBottomStyle,
            sumLabel = summaryLabelThickBottomStyle,
            sumEmpty = summaryEmptyThickBottomStyle,
            sumHidden = summaryHiddenKeyStyle,
        )
    }

    private fun setTextCell(row: Row, col: Int, value: Any?, style: XSSFCellStyle) {
        row.createCell(col).apply {
            setCellValue(value?.toString() ?: "")
            cellStyle = style
        }
    }

    private fun parseSignedNumber(value: Any?): Double? {
        val raw = value?.toString()?.trim().orEmpty()
        if (raw.isBlank() || raw == "-" || raw.equals("null", ignoreCase = true)) return null
        val negative = raw.startsWith("(") && raw.endsWith(")")
        val cleaned = raw.removePrefix("(").removeSuffix(")").replace(",", "").replace("%", "").trim()
        val n = cleaned.toDoubleOrNull() ?: return null
        return if (negative) -n else n
    }

    private fun setNumberCellFromFormatted(
        row: Row,
        col: Int,
        value: Any?,
        numberStyle: XSSFCellStyle,
        dashStyle: XSSFCellStyle,
        preferInt: Boolean = false,
    ) {
        val cell = row.createCell(col)
        val parsed = parseSignedNumber(value)
        if (parsed == null) {
            cell.setCellValue("-")
            cell.cellStyle = dashStyle
            return
        }
        if (parsed < 0) {
            val abs = kotlin.math.abs(parsed)
            val s = if (preferInt) "%,d".format(abs.toLong()) else "%,.2f".format(abs)
            cell.setCellValue("($s)")
            cell.cellStyle = dashStyle
            return
        }
        cell.setCellValue(parsed)
        cell.cellStyle = numberStyle
    }

    private fun createStockInTraceabilityExcel(
        dbData: List<Map<String, Any>>,
        lastInDateStart: String,
        lastInDateEnd: String,
    ): ByteArray {
        val workbook = XSSFWorkbook()
        val styles = createCommonWorkbookStyles(workbook)
        val reportTitle = "入倉追蹤報告"
        val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle))

        val headers = listOf(
            "貨品編號", "貨品名稱", "單位",
            "送貨單編號", "批號", "到期日",
            "上架數量", "入庫檢查樣品數量", "入庫檢查缺陷數量",
            "入庫檢查缺陷百分比", "入庫檢查結果", "入庫檢查備註",
            "存貨位置", "供應商名稱",
        )
        val totalColumns = headers.size
        var rowIndex = 0

        val titleRow = sheet.createRow(rowIndex++)
        titleRow.createCell(0).apply {
            setCellValue(reportTitle)
            cellStyle = styles.title
        }
        sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1))

        val reportDateTime =
            LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) +
                "(" +
                LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) +
                ")"
        val subtitleRow = sheet.createRow(rowIndex++)
        subtitleRow.createCell(0).apply {
            setCellValue("報告日期：$reportDateTime")
            cellStyle = styles.subtitle
        }
        sheet.addMergedRegion(CellRangeAddress(1, 1, 0, 5))
        subtitleRow.createCell(6).apply {
            val cap = (lastInDateStart.trim().ifBlank { "" }) + " 至 " + (lastInDateEnd.trim().ifBlank { "" })
            setCellValue("最後入倉日期：${cap.trim()}")
            cellStyle = styles.subtitle
        }
        sheet.addMergedRegion(CellRangeAddress(1, 1, 6, totalColumns - 1))
        sheet.createRow(rowIndex++)

        val headerRowIndex = rowIndex
        val headerRow = sheet.createRow(rowIndex++)
        headers.forEachIndexed { i, h ->
            headerRow.createCell(i).apply {
                setCellValue(h)
                cellStyle = styles.header
            }
        }

        fun addItemSummaryRow(
            itemNo: String,
            itemName: String,
            uom: String,
            totalStockIn: Any?,
            _totalSample: Any?,
            _totalDefect: Any?,
        ) {
            val r = sheet.createRow(rowIndex++)
            r.createCell(0).apply { setCellValue(itemNo); cellStyle = styles.sumHidden }
            r.createCell(1).apply { setCellValue(itemName); cellStyle = styles.sumHidden }
            r.createCell(2).apply { setCellValue(uom); cellStyle = styles.sumHidden }
            for (c in 3 until totalColumns) {
                r.createCell(c).apply { setCellValue(""); cellStyle = styles.sumEmpty }
            }
            // Put totals in the three adjacent qty columns to avoid displacing later columns:
            // - 上架數量 (col 6)
            // - 入庫檢查樣品數量 (col 7)
            // - 入庫檢查缺陷數量 (col 8)
            r.getCell(5).apply { setCellValue("合計："); cellStyle = styles.sumLabel }
            setNumberCellFromFormatted(r, 6, totalStockIn, styles.sumQty, styles.dash, preferInt = false)
            setNumberCellFromFormatted(r, 7, _totalSample, styles.sumQty, styles.dash, preferInt = false)
            setNumberCellFromFormatted(r, 8, _totalDefect, styles.sumQty, styles.dash, preferInt = false)
        }

        if (dbData.isEmpty()) {
            val r = sheet.createRow(rowIndex++)
            for (c in 0 until totalColumns) {
                r.createCell(c).apply { setCellValue("-"); cellStyle = styles.text }
            }
        } else {
            var currentItemNo: String? = null
            var currentItemName = ""
            var currentUom = ""
            var lastTotals: Triple<Any?, Any?, Any?> = Triple(null, null, null)

            dbData.forEach { m ->
                val itemNo = m["itemNo"]?.toString().orEmpty()
                val itemName = m["itemName"]?.toString().orEmpty()
                val uom = m["unitOfMeasure"]?.toString().orEmpty()

                if (currentItemNo != null && itemNo != currentItemNo) {
                    addItemSummaryRow(
                        currentItemNo!!,
                        currentItemName,
                        currentUom,
                        lastTotals.first,
                        lastTotals.second,
                        lastTotals.third,
                    )
                    sheet.createRow(rowIndex++)
                }

                val r = sheet.createRow(rowIndex++)
                setTextCell(r, 0, itemNo, styles.text)
                setTextCell(r, 1, itemName, styles.text)
                setTextCell(r, 2, uom, styles.center)
                setTextCell(r, 3, m["dnNo"], styles.text)
                setTextCell(r, 4, m["lotNo"], styles.text)
                setTextCell(r, 5, m["expiryDate"], styles.center)
                setNumberCellFromFormatted(r, 6, m["stockInQty"], styles.number, styles.dash, preferInt = false)
                setNumberCellFromFormatted(r, 7, m["iqcSampleQty"], styles.number, styles.dash, preferInt = false)
                setNumberCellFromFormatted(r, 8, m["iqcDefectQty"], styles.number, styles.dash, preferInt = false)
                // percentage as text with %
                val pct = (m["iqcDefectPercentage"]?.toString()?.trim().orEmpty()).let { if (it.isBlank()) "" else "$it%" }
                setTextCell(r, 9, pct, styles.dash)
                setTextCell(r, 10, m["iqcResult"], styles.center)
                setTextCell(r, 11, m["iqcRemarks"], styles.text)
                setTextCell(r, 12, m["storeLocation"], styles.center)
                setTextCell(r, 13, m["supplierName"], styles.text)

                currentItemNo = itemNo
                currentItemName = itemName
                currentUom = uom
                lastTotals = Triple(m["totalStockInQty"], m["totalIqcSampleQty"], m["totalIqcDefectQty"])
            }

            addItemSummaryRow(
                currentItemNo ?: "",
                currentItemName,
                currentUom,
                lastTotals.first,
                lastTotals.second,
                lastTotals.third,
            )
        }

        val lastRowIndex = rowIndex - 1
        if (lastRowIndex >= headerRowIndex) {
            sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0))
        }

        val widths = intArrayOf(14, 26, 10, 18, 16, 12, 14, 18, 18, 18, 14, 22, 14, 20)
        widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) }

        val out = ByteArrayOutputStream()
        workbook.use { it.write(out) }
        return out.toByteArray()
    }

    private fun createFGDeliveryExcel(
        dbData: List<Map<String, Any>>,
        year: String,
        lastOutDateStart: String,
        lastOutDateEnd: String,
    ): ByteArray {
        val workbook = XSSFWorkbook()
        val styles = createCommonWorkbookStyles(workbook)
        val reportTitle = "成品出倉報告"
        val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle))

        val headers = listOf(
            "貨品編號", "貨品名稱", "單位",
            "送貨日期", "DN編號", "送貨訂單編號", "門店名稱", "數量", "貨車",
        )
        val totalColumns = headers.size
        var rowIndex = 0

        val titleRow = sheet.createRow(rowIndex++)
        titleRow.createCell(0).apply {
            setCellValue(reportTitle)
            cellStyle = styles.title
        }
        sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1))

        val reportDateTime =
            LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) +
                "(" +
                LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) +
                ")"
        val subtitleRow = sheet.createRow(rowIndex++)
        subtitleRow.createCell(0).apply {
            setCellValue("報告日期：$reportDateTime")
            cellStyle = styles.subtitle
        }
        sheet.addMergedRegion(CellRangeAddress(1, 1, 0, 2))
        subtitleRow.createCell(3).apply {
            setCellValue("成品出倉日期：${lastOutDateStart.trim()} 至 ${lastOutDateEnd.trim()}")
            cellStyle = styles.subtitle
        }
        sheet.addMergedRegion(CellRangeAddress(1, 1, 3, 6))
        subtitleRow.createCell(7).apply {
            setCellValue("年份：$year")
            cellStyle = styles.subtitle
        }
        sheet.addMergedRegion(CellRangeAddress(1, 1, 7, totalColumns - 1))
        sheet.createRow(rowIndex++)

        val headerRowIndex = rowIndex
        val headerRow = sheet.createRow(rowIndex++)
        headers.forEachIndexed { i, h ->
            headerRow.createCell(i).apply {
                setCellValue(h)
                cellStyle = styles.header
            }
        }

        if (dbData.isEmpty()) {
            val r = sheet.createRow(rowIndex++)
            for (c in 0 until totalColumns) {
                r.createCell(c).apply { setCellValue("-"); cellStyle = styles.text }
            }
        } else {
            fun addDeliveryOrderTotalRow(totalQty: Double) {
                val r = sheet.createRow(rowIndex++)
                for (c in 0 until totalColumns) {
                    r.createCell(c).apply {
                        setCellValue("")
                        cellStyle = styles.sumEmpty
                    }
                }
                r.getCell(6).apply {
                    setCellValue("每單交貨總數：")
                    cellStyle = styles.sumLabel
                }
                r.getCell(7).apply {
                    setCellValue(totalQty)
                    cellStyle = styles.sumQty
                }
            }

            // Jasper `totalDeliveredQtyPerDeliveryOrder` is reset by Group1 = itemNo.
            // Keep Excel consistent: sort by itemNo then calculate per-item total.
            val sorted = dbData.sortedWith(
                compareBy<Map<String, Any>>(
                    { it["itemNo"]?.toString().orEmpty() },
                    { it["deliveryDate"]?.toString().orEmpty() },
                    { it["dnNo"]?.toString().orEmpty() },
                    { it["deliveryOrderNo"]?.toString().orEmpty() },
                ),
            )

            var currentItemNo: String? = null
            var itemTotalQty = 0.0

            sorted.forEach { m ->
                val itemNo = m["itemNo"]?.toString().orEmpty()
                if (currentItemNo != null && itemNo != currentItemNo) {
                    addDeliveryOrderTotalRow(itemTotalQty)
                    sheet.createRow(rowIndex++)
                    itemTotalQty = 0.0
                }

                val r = sheet.createRow(rowIndex++)
                setTextCell(r, 0, m["itemNo"], styles.text)
                setTextCell(r, 1, m["itemName"], styles.text)
                setTextCell(r, 2, m["unitOfMeasure"], styles.center)
                setTextCell(r, 3, m["deliveryDate"], styles.center)
                setTextCell(r, 4, m["dnNo"], styles.text)
                setTextCell(r, 5, m["deliveryOrderNo"], styles.text)
                setTextCell(r, 6, m["customerName"], styles.text)
                setNumberCellFromFormatted(r, 7, m["qty"], styles.int, styles.dash, preferInt = true)
                setTextCell(r, 8, m["truckNo"], styles.center)

                itemTotalQty += parseSignedNumber(m["qty"]) ?: 0.0
                currentItemNo = itemNo
            }

            if (!currentItemNo.isNullOrBlank()) {
                addDeliveryOrderTotalRow(itemTotalQty)
            }
        }

        val lastRowIndex = rowIndex - 1
        if (lastRowIndex >= headerRowIndex) {
            sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0))
        }
        val widths = intArrayOf(14, 26, 10, 12, 18, 18, 26, 10, 12)
        widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) }

        val out = ByteArrayOutputStream()
        workbook.use { it.write(out) }
        return out.toByteArray()
    }

    private fun createStockBalanceExcel(
        dbData: List<Map<String, Any>>,
        reportDate: String,
    ): ByteArray {
        val workbook = XSSFWorkbook()
        val styles = createCommonWorkbookStyles(workbook)
        val reportTitle = "庫存結餘報告"
        val sheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(reportTitle))

        val headers = listOf(
            "貨品編號", "貨品名稱", "單位",
            "期初存量", "纍計存入量", "纍計存出量",
            "錯誤輸入或遺失", "盤盈虧", "不良品棄置",
            "現存存貨", "單位均價", "庫存總價值",
        )
        val totalColumns = headers.size
        var rowIndex = 0

        val titleRow = sheet.createRow(rowIndex++)
        titleRow.createCell(0).apply {
            setCellValue(reportTitle)
            cellStyle = styles.title
        }
        sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1))

        val reportDateTime =
            reportDate +
                "(" +
                LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) +
                ")"
        val subtitleRow = sheet.createRow(rowIndex++)
        subtitleRow.createCell(0).apply {
            setCellValue("報告日期：$reportDateTime")
            cellStyle = styles.subtitle
        }
        sheet.addMergedRegion(CellRangeAddress(1, 1, 0, totalColumns - 1))
        sheet.createRow(rowIndex++)

        val headerRowIndex = rowIndex
        val headerRow = sheet.createRow(rowIndex++)
        headers.forEachIndexed { i, h ->
            headerRow.createCell(i).apply {
                setCellValue(h)
                cellStyle = styles.header
            }
        }

        if (dbData.isEmpty()) {
            val r = sheet.createRow(rowIndex++)
            for (c in 0 until totalColumns) {
                r.createCell(c).apply { setCellValue("-"); cellStyle = styles.text }
            }
        } else {
            dbData.forEach { m ->
                val r = sheet.createRow(rowIndex++)
                setTextCell(r, 0, m["itemNo"], styles.text)
                setTextCell(r, 1, m["itemName"], styles.text)
                setTextCell(r, 2, m["unitOfMeasure"], styles.center)
                setNumberCellFromFormatted(r, 3, m["totalOpeningBalance"], styles.int, styles.dash, preferInt = true)
                setNumberCellFromFormatted(r, 4, m["totalCumStockIn"], styles.int, styles.dash, preferInt = true)
                setNumberCellFromFormatted(r, 5, m["totalCumStockOut"], styles.int, styles.dash, preferInt = true)
                setNumberCellFromFormatted(r, 6, m["totalMisInputAndLost"], styles.int, styles.dash, preferInt = true)
                setNumberCellFromFormatted(r, 7, m["totalVariance"], styles.int, styles.dash, preferInt = true)
                setNumberCellFromFormatted(r, 8, m["totalDefectiveGoods"], styles.int, styles.dash, preferInt = true)
                setNumberCellFromFormatted(r, 9, m["totalCurrentBalance"], styles.int, styles.dash, preferInt = true)
                setNumberCellFromFormatted(r, 10, m["avgUnitPrice"], styles.number, styles.dash, preferInt = false)
                setNumberCellFromFormatted(r, 11, m["totalStockBalance"], styles.number, styles.dash, preferInt = false)
            }
        }

        val lastRowIndex = rowIndex - 1
        if (lastRowIndex >= headerRowIndex) {
            sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, 0))
        }
        val widths = intArrayOf(14, 26, 10, 12, 12, 12, 14, 12, 12, 12, 12, 14)
        widths.forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) }

        val out = ByteArrayOutputStream()
        workbook.use { it.write(out) }
        return out.toByteArray()
    }

    /**
     * GRN (Goods Received Note) report data for Excel export.
     * Query by receipt date range and optional item code. Returns JSON { "rows": [ ... ] }.
     */
    @GetMapping("/grn-report")
    fun getGrnReport(
        @RequestParam(required = false) receiptDateStart: String?,
        @RequestParam(required = false) receiptDateEnd: String?,
        @RequestParam(required = false) itemCode: String?,
        @RequestParam(required = false) supplier: String?,
        @RequestParam(required = false) poCode: String?,
        @RequestParam(required = false) grnCode: String?,
    ): Map<String, Any> {
        val rows = reportService.searchGrnReport(
            receiptDateStart,
            receiptDateEnd,
            itemCode,
            supplier,
            poCode,
            grnCode,
        )
        val isAdmin = SecurityUtils.isGranted("ADMIN")
        val sanitizedRows =
            if (isAdmin) {
                rows
            } else {
                rows.map { row ->
                    row.filterKeys { it !in GRN_FINANCIAL_KEYS }
                }
            }
        val listedPoAmounts =
            if (isAdmin) {
                reportService.searchGrnListedPoAmounts(
                    receiptDateStart, 
                    receiptDateEnd, 
                    itemCode,
                    supplier,
                    poCode,
                    grnCode,
                    )
            } else {
                mapOf(
                    "currencyTotals" to emptyList<Map<String, Any?>>(),
                    "byPurchaseOrder" to emptyList<Map<String, Any?>>(),
                )
            }
        return mapOf(
            "rows" to sanitizedRows,
            "listedPoAmounts" to listedPoAmounts,
        )
    }

    /**
     * M18 BOM Shop sync history (from [com.ffii.fpsms.m18.entity.M18BomShopSyncLog]).
     * JSON for Excel export on the report page: sync attempts + material lines per version.
     *
     * Example: `/report/bom-shop-sync-history?syncDateStart=2026-06-01&syncDateEnd=2026-06-13`
     */
    @GetMapping("/bom-shop-sync-history")
    fun getBomShopSyncHistoryReport(
        @RequestParam(required = false) syncDateStart: String?,
        @RequestParam(required = false) syncDateEnd: String?,
        @RequestParam(required = false) finishedItemCode: String?,
        @RequestParam(required = false) syncStatus: String?,
    ): Map<String, Any> =
        m18BomShopSyncReportService.searchBomShopSyncHistoryReport(
            syncDateStart = syncDateStart,
            syncDateEnd = syncDateEnd,
            finishedItemCode = finishedItemCode,
            syncStatus = syncStatus,
        )

    companion object {
        /** GRN report fields only users with ADMIN authority may see */
        private val GRN_FINANCIAL_KEYS = setOf("unitPrice", "lineAmount", "currencyCode")
    }

    /**
     * DRY-RUN GRN preview for M18 (shows stock qty vs converted purchase qty).
     *
     * Example: `/report/grn-preview-m18?receiptDate=2026-03-16`
     * CSV (Excel-openable): `/report/grn-preview-m18?receiptDate=2026-03-16&format=csv`
     */
    @GetMapping("/grn-preview-m18")
    fun getGrnPreviewM18(
        @RequestParam receiptDate: String,
        @RequestParam(required = false, defaultValue = "json") format: String,
    ): Any {
        val rows = reportService.searchGrnPreviewM18(receiptDate)
        if (format.equals("csv", ignoreCase = true)) {
            val headers = listOf(
                "receiptDate",
                "poCode",
                "deliveryNoteNo",
                "stockInLineId",
                "purchaseOrderLineId",
                "itemCode",
                "itemName",
                "stockQty",
                "purchaseQty",
                "stockUomDesc",
                "purchaseUomDesc",
                "status",
            )
            val sb = StringBuilder()
            sb.append(headers.joinToString(",")).append("\n")
            rows.forEach { r ->
                val line = headers.joinToString(",") { h ->
                    val v = r[h]
                    val s = (v?.toString() ?: "").replace("\"", "\"\"")
                    "\"$s\""
                }
                sb.append(line).append("\n")
            }
            val bytes = sb.toString().toByteArray(Charsets.UTF_8)
            val httpHeaders = HttpHeaders().apply {
                contentType = MediaType("text", "csv")
                set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=grn-preview-m18-$receiptDate.csv")
            }
            return ResponseEntity(bytes, httpHeaders, HttpStatus.OK)
        }
        return mapOf("rows" to rows)
    }

}