package com.ffii.fpsms.modules.stock.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLogRepository
import com.ffii.fpsms.modules.stock.entity.StockInLine
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.web.model.SearchCompletedDnResult
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDate
import java.time.LocalDateTime

/**
 * Search completed stock-in lines (DN) by receipt date and process M18 GRN creation.
 * Query: receiptDate = yesterday, status = completed, purchaseOrderId not null.
 * Same PO may appear on multiple dates; each receipt date batch can produce its own M18 GRN.
 */
@Service
open class SearchCompletedDnService(
    private val jdbcDao: JdbcDao,
    private val stockInLineRepository: StockInLineRepository,
    private val stockInLineService: StockInLineService,
    private val m18GoodsReceiptNoteLogRepository: M18GoodsReceiptNoteLogRepository,
) {
    private val logger = LoggerFactory.getLogger(SearchCompletedDnService::class.java)

    /**
     * Search completed DNs by receipt date (default: yesterday).
     * Returns stock-in lines with receiptDate = date, status = completed, purchaseOrder not null.
     */
    open fun searchCompletedDn(receiptDate: LocalDate = LocalDate.now().minusDays(1)): List<StockInLine> {
        return stockInLineRepository.findCompletedDnByReceiptDate(receiptDate)
    }

    /**
     * Search completed DNs by receipt date with exact query format.
     * dnNo: empty string when original is 'DN00000'.
     */
    open fun searchCompletedDnAsResult(receiptDate: LocalDate = LocalDate.now().minusDays(1)): List<SearchCompletedDnResult> {
        val sql = """
            SELECT
                sil.purchaseOrderId as purchaseOrderId,
                sil.purchaseOrderLineId as purchaseOrderLineId,
                sil.itemId as itemId,
                sil.itemNo as itemNo,
                it.name as itemName,
                sil.stockInId as stockInId,
                sil.demandQty as demandQty,
                sil.acceptedQty as acceptedQty,
                sil.receiptDate as receiptDate,
                -- CASE WHEN sil.dnNo = 'DN00000' THEN '' ELSE sil.dnNo END as dnNo
                sil.dnNo
            FROM stock_in_line sil
            LEFT JOIN items it ON sil.itemId = it.id
            WHERE sil.receiptDate IS NOT NULL
            AND DATE(sil.receiptDate) = :receiptDate
            AND sil.status = 'completed'
            AND sil.purchaseOrderId IS NOT NULL
            AND sil.deleted = false
            ORDER BY sil.purchaseOrderId
        """.trimIndent()
        val rows = jdbcDao.queryForList(sql, mapOf("receiptDate" to receiptDate.toString()))
        return rows.map { row ->
            val getLong = { k: String -> ((row[k] ?: row[k.lowercase()]) as? Number)?.toLong() ?: 0L }
            val getBd = { k: String -> (row[k] ?: row[k.lowercase()])?.let { when (it) { is BigDecimal -> it; is Number -> BigDecimal(it.toString()); else -> BigDecimal(it.toString()) } } }
            val getTs = { k: String -> (row[k] ?: row[k.lowercase()])?.let { when (it) { is java.sql.Timestamp -> it.toLocalDateTime(); is java.sql.Date -> it.toLocalDate().atStartOfDay(); else -> null } } }
            SearchCompletedDnResult(
                purchaseOrderId = getLong("purchaseOrderId"),
                purchaseOrderLineId = getLong("purchaseOrderLineId"),
                itemId = getLong("itemId"),
                itemNo = (row["itemNo"] ?: row["itemno"])?.toString(),
                itemName = (row["itemName"] ?: row["itemname"])?.toString(),
                stockInId = getLong("stockInId"),
                demandQty = getBd("demandQty"),
                acceptedQty = getBd("acceptedQty"),
                receiptDate = getTs("receiptDate"),
                dnNo = (row["dnNo"] ?: row["dnno"])?.toString()?.takeIf { it.isNotEmpty() },
            )
        }
    }

    /**
     * Post completed DNs and process each related purchase order for M18 GRN creation.
     * Triggered by scheduler. One GRN per PO **per receipt date** for lines completed on that date (same PO may receive multiple GRNs on different days).
     * @param receiptDate Default: yesterday
     * @param skipFirst For testing/manual trigger: skip the first N POs. 1 = skip 1st, process from 2nd. 0 = process from 1st.
     * @param limitToFirst For testing/manual trigger: process only the next N POs after skip. 1 = one PO. null = all remaining POs.
     */
    @Transactional
    open fun postCompletedDnAndProcessGrn(
        receiptDate: LocalDate = LocalDate.now().minusDays(1),
        skipFirst: Int = 0,
        limitToFirst: Int? = null,
    ): Int {
        val lines = searchCompletedDn(receiptDate)
        val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L }
        val entries = byPo.entries.toList()
        val toProcess = entries.drop(skipFirst).let { if (limitToFirst != null) it.take(limitToFirst) else it }
        logger.info("[postCompletedDnAndProcessGrn] receiptDate=$receiptDate, found ${lines.size} lines, ${byPo.size} POs, processing ${toProcess.size} (skipFirst=$skipFirst, limitToFirst=$limitToFirst)")
        toProcess.forEach { (poId, silList) ->
            silList.firstOrNull()?.let { first ->
                try {
                    stockInLineService.processPurchaseOrderForGrn(first, grnReceiptDate = receiptDate)
                } catch (e: Exception) {
                    logger.error("[postCompletedDnAndProcessGrn] Failed for PO id=$poId: ${e.message}", e)
                }
            }
        }
        return toProcess.size
    }

    /**
     * Retry GRN creation for completed stock-in lines where **some** line in that PO batch (same receipt date)
     * has no SUCCESS row in `m18_goods_receipt_note_log` (matched by `stock_in_line_id`).
     * Same PO can receive multiple GRNs on different delivery/receipt dates.
     */
    @Transactional
    open fun postCompletedDnAndProcessGrnWithMissingRetry(
        receiptDate: LocalDate = LocalDate.now().minusDays(1),
        skipFirst: Int = 0,
        limitToFirst: Int? = null,
        retryLookbackDays: Int = 0,
    ): Int {
        val lookback = retryLookbackDays.coerceAtLeast(0)
        var totalProcessed = 0

        // Requirement: never process today's completed stock-in lines in retry mode.
        val today = LocalDate.now()
        var processedDayCount = 0

        // For each offset, day = receiptDate minus offset. Example: job runs 27/3 00:01, receiptDate=yesterday=26/3,
        // lookback=6 => days 26,25,24,23,22,21,20 (skip any day equal to today).
        for (offset in 0..lookback) {
            val day = receiptDate.minusDays(offset.toLong())
            if (day.isEqual(today)) {
                logger.info(
                    "[postCompletedDnAndProcessGrnWithMissingRetry] Skipping receiptDate=today=$today " +
                        "(offset=$offset) to avoid creating GRN from today's completed stock-in lines."
                )
                continue
            }

            val daySkipFirst = if (processedDayCount == 0) skipFirst else 0
            val dayLimitToFirst = if (processedDayCount == 0) limitToFirst else null

            val processedForDay = postCompletedDnAndProcessMissingGrnForReceiptDate(
                receiptDate = day,
                skipFirst = daySkipFirst,
                limitToFirst = dayLimitToFirst,
            )
            totalProcessed += processedForDay
            processedDayCount++
            logger.info(
                "[postCompletedDnAndProcessGrnWithMissingRetry] offset=$offset day=$day processedForDay=$processedForDay totalProcessed=$totalProcessed"
            )
        }

        return totalProcessed
    }

    private fun postCompletedDnAndProcessMissingGrnForReceiptDate(
        receiptDate: LocalDate,
        skipFirst: Int,
        limitToFirst: Int?,
    ): Int {
        val lines = searchCompletedDn(receiptDate)
        val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L }
        val entries = byPo.entries.toList()

        // Per delivery date: retry if any completed line for this PO on this date lacks a successful GRN log.
        val missingEntries = entries.filter { (_, silList) ->
            silList.any { sil ->
                val id = sil.id ?: return@any true
                !m18GoodsReceiptNoteLogRepository.existsByStockInLineIdAndStatusTrue(id)
            }
        }

        val toProcess = missingEntries
            .drop(skipFirst)
            .let { if (limitToFirst != null) it.take(limitToFirst) else it }

        logger.info(
            "[postCompletedDnAndProcessMissingGrnForReceiptDate] receiptDate=$receiptDate foundLines=${lines.size} " +
                "poTotal=${byPo.size} poMissingSuccess=${missingEntries.size} processing=${toProcess.size} " +
                "(skipFirst=$skipFirst, limitToFirst=$limitToFirst)"
        )

        toProcess.forEach { (poId, silList) ->
            silList.firstOrNull()?.let { first ->
                try {
                    stockInLineService.processPurchaseOrderForGrn(first, grnReceiptDate = receiptDate)
                } catch (e: Exception) {
                    logger.error("[postCompletedDnAndProcessMissingGrnForReceiptDate] Failed for PO id=$poId: ${e.message}", e)
                }
            }
        }

        return toProcess.size
    }
}
