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

import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus
import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoWorkbenchResponse
import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderLotsHierarchicalWorkbenchResponse
import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoWorkbenchResponse
import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse
import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoResponse
import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsWorkbenchResponse
import com.ffii.fpsms.modules.jobOrder.web.model.StockOutLineDetailResponse
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderType
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotRepository
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.entity.StockOutRepository
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.stock.service.StockOutLineWorkbenchService
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotWorkbenchService
import com.ffii.fpsms.modules.user.service.UserService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDate
import java.util.Locale

/**
 * Job Order Workbench–only flows (assign prime, hierarchical payload for scan-pick UI).
 * [getJobOrderLotsHierarchicalByPickOrderIdWorkbench] uses **in − out** for available qty (matches workbench scan-pick).
 * Non-workbench hierarchical stays in [JoPickOrderService.getJobOrderLotsHierarchicalByPickOrderId] (in − out − hold).
 */
@Service
open class JoWorkbenchMainService(
    private val jobOrderRepository: JobOrderRepository,
    private val pickOrderRepository: PickOrderRepository,
    private val pickOrderLineRepository: PickOrderLineRepository,
    private val suggestPickLotRepository: SuggestPickLotRepository,
    private val inventoryLotLineRepository: InventoryLotLineRepository,
    private val inventoryLotRepository: InventoryLotRepository,
    private val stockOutLineRepository: StockOutLIneRepository,
    private val joPickOrderRepository: JoPickOrderRepository,
    private val userService: UserService,
    private val stockOutRepository: StockOutRepository,
    private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService,
    private val stockOutLineWorkbenchService: StockOutLineWorkbenchService,
) {
    private val workbenchDefaultExcludeWarehouseCodes: Set<String> =
        JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES

    private fun debugPrintSuggestionNullReasons(pickOrderId: Long) {
        val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: return
        val lines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId)
        if (lines.isEmpty()) return

        val today = LocalDate.now()
        val lineIds = lines.mapNotNull { it.id }
        val splByLine = suggestPickLotRepository
            .findAllByPickOrderLineIdInAndDeletedFalse(lineIds)
            .groupBy { it.pickOrderLine?.id }

        println("=== JO_WORKBENCH_SUGGEST_DEBUG pickOrderId=$pickOrderId ===")
        lines.forEach { line ->
            val lineId = line.id ?: return@forEach
            val itemId = line.item?.id
            if (itemId == null) {
                println("JO_SUGGEST_DEBUG polId=$lineId itemId=null reason=missing_item")
                return@forEach
            }

            val allItemLots = inventoryLotLineRepository.findAllByItemIdIn(listOf(itemId))
            val notDeleted = allItemLots.filter { !it.deleted }
            val availableStatus = notDeleted.filter { it.status == InventoryLotLineStatus.AVAILABLE }
            val notExpired = availableStatus.filter { it.inventoryLot?.expiryDate?.isBefore(today) != true }
            val notExcluded = notExpired.filter { lot ->
                val code = lot.warehouse?.code?.trim()?.uppercase(Locale.ROOT)
                code.isNullOrEmpty() || code !in workbenchDefaultExcludeWarehouseCodes
            }
            val positiveQty = notExcluded.filter { lot ->
                val inQ = lot.inQty ?: BigDecimal.ZERO
                val outQ = lot.outQty ?: BigDecimal.ZERO
                inQ.subtract(outQ) > BigDecimal.ZERO
            }

            val activeSpls = splByLine[lineId].orEmpty()
            val hasNonNullSuggestedLot = activeSpls.any { it.suggestedLotLine?.id != null }
            val suggestedLotLineId = activeSpls.firstOrNull { it.suggestedLotLine?.id != null }?.suggestedLotLine?.id

            println(
                "JO_SUGGEST_DEBUG polId=$lineId itemId=$itemId req=${line.qty} " +
                    "lots[item=${allItemLots.size},notDeleted=${notDeleted.size},available=${availableStatus.size}," +
                    "notExpired=${notExpired.size},notExcluded=${notExcluded.size},positiveQty=${positiveQty.size}] " +
                    "splCount=${activeSpls.size} suggestedLotLineId=${suggestedLotLineId ?: "null"}"
            )

            if (!hasNonNullSuggestedLot) {
                val reason = when {
                    allItemLots.isEmpty() -> "no_item_lot_rows"
                    notDeleted.isEmpty() -> "all_deleted"
                    availableStatus.isEmpty() -> "none_status_available"
                    notExpired.isEmpty() -> "all_expired"
                    notExcluded.isEmpty() -> "all_excluded_by_default_warehouse"
                    positiveQty.isEmpty() -> "none_positive_available_qty"
                    else -> "eligible_lot_exists_but_spl_still_null_check_sync_or_query_timing"
                }
                val sampleCodes = positiveQty.take(3).joinToString(",") { it.warehouse?.code ?: "-" }
                val sampleLots = positiveQty.take(3).joinToString(",") { it.inventoryLot?.lotNo ?: "-" }
                println(
                    "JO_SUGGEST_DEBUG_NULL polId=$lineId reason=$reason " +
                        "sampleWarehouses=[$sampleCodes] sampleLots=[$sampleLots]"
                )
            }
        }
    }

    private data class SplDebugRow(
        val splId: Long?,
        val polId: Long?,
        val lotLineId: Long?,
        val qty: BigDecimal?,
        val deleted: Boolean,
    )

    private fun loadSplDebugRowsByPol(pickOrderId: Long): Map<Long, List<SplDebugRow>> {
        val lineIds = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId)
            .mapNotNull { it.id }
        if (lineIds.isEmpty()) return emptyMap()
        return suggestPickLotRepository.findAllByPickOrderLineIdIn(lineIds)
            .map { spl ->
                SplDebugRow(
                    splId = spl.id,
                    polId = spl.pickOrderLine?.id,
                    lotLineId = spl.suggestedLotLine?.id,
                    qty = spl.qty,
                    deleted = spl.deleted,
                )
            }
            .filter { it.polId != null }
            .groupBy { it.polId!! }
    }

    private fun debugPrintAssignSplDiff(
        pickOrderId: Long,
        before: Map<Long, List<SplDebugRow>>,
        after: Map<Long, List<SplDebugRow>>,
    ) {
        println("=== JO_ASSIGN_SPL_DIFF pickOrderId=$pickOrderId ===")
        val allPolIds = (before.keys + after.keys).toSortedSet()
        allPolIds.forEach { polId ->
            val beforeRows = before[polId].orEmpty().sortedBy { it.splId ?: 0L }
            val afterRows = after[polId].orEmpty().sortedBy { it.splId ?: 0L }
            val beforeFmt = if (beforeRows.isEmpty()) {
                "[]"
            } else {
                beforeRows.joinToString(
                    prefix = "[",
                    postfix = "]",
                    separator = ", ",
                ) { r ->
                    "spl=${r.splId}|lot=${r.lotLineId ?: "null"}|qty=${r.qty ?: "null"}|del=${r.deleted}"
                }
            }
            val afterFmt = if (afterRows.isEmpty()) {
                "[]"
            } else {
                afterRows.joinToString(
                    prefix = "[",
                    postfix = "]",
                    separator = ", ",
                ) { r ->
                    "spl=${r.splId}|lot=${r.lotLineId ?: "null"}|qty=${r.qty ?: "null"}|del=${r.deleted}"
                }
            }
            println("JO_ASSIGN_SPL_DIFF polId=$polId before=$beforeFmt -> after=$afterFmt")
        }
    }


    /** Workbench / scan-pick: same as [com.ffii.fpsms.modules.deliveryOrder.service.DoWorkbenchMainService] lot availability. */
    private fun illAvailableQtyWorkbench(ill: InventoryLotLine?): BigDecimal? {
        if (ill == null || ill.deleted) return null
        val inQ = ill.inQty ?: BigDecimal.ZERO
        val outQ = ill.outQty ?: BigDecimal.ZERO
        return inQ.subtract(outQ)
    }

    /**
     * Hierarchical pick UI for JO Workbench: available qty **in − out**; stockouts include **suggestedPickQty** when SPL matches SOL lot line.
     */
    open fun getJobOrderLotsHierarchicalByPickOrderIdWorkbench(pickOrderId: Long): JobOrderLotsHierarchicalWorkbenchResponse {
        println("=== JoWorkbenchMainService.getJobOrderLotsHierarchicalByPickOrderIdWorkbench ===")
        println("pickOrderId: $pickOrderId")

        return try {
            val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null)
            if (pickOrder == null || pickOrder.deleted == true) {
                return emptyHierarchical("Pick order $pickOrderId not found or deleted")
            }

            val jobOrder = pickOrder.jobOrder
                ?: return emptyHierarchical("Pick order has no job order")

            val pickOrderLines = pickOrderLineRepository.findAllByPickOrderId(pickOrder.id!!)
                .filter { it.deleted == false }

            val itemIds = pickOrderLines.mapNotNull { it.item?.id }.distinct()
            val today = LocalDate.now()
            val totalAvailableQtyByItemId: Map<Long, Double> = if (itemIds.isNotEmpty()) {
                inventoryLotLineRepository.findAllByItemIdIn(itemIds)
                    .asSequence()
                    .filter { it.deleted == false }
                    .filter { it.status == InventoryLotLineStatus.AVAILABLE }
                    .filter { ill ->
                        val exp = ill.inventoryLot?.expiryDate
                        exp == null || !exp.isBefore(today)
                    }
                    .mapNotNull { ill ->
                        val iid = ill.inventoryLot?.item?.id ?: return@mapNotNull null
                        val remaining = illAvailableQtyWorkbench(ill) ?: return@mapNotNull null
                        if (remaining > BigDecimal.ZERO) iid to remaining else null
                    }
                    .groupBy({ it.first }, { it.second })
                    .mapValues { (_, qtys) -> qtys.fold(BigDecimal.ZERO) { acc, q -> acc + q }.toDouble() }
            } else {
                emptyMap()
            }

            val pickOrderLineIds = pickOrderLines.map { it.id!! }

            val suggestedPickLots = if (pickOrderLineIds.isNotEmpty()) {
                suggestPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds)
                    .filter { it.deleted == false }
            } else {
                emptyList()
            }

            val inventoryLotLineIds = suggestedPickLots.mapNotNull { it.suggestedLotLine?.id }

            val inventoryLotLines = if (inventoryLotLineIds.isNotEmpty()) {
                inventoryLotLineRepository.findAllByIdIn(inventoryLotLineIds)
                    .filter { it.deleted == false }
            } else {
                emptyList()
            }

            val inventoryLotIds = inventoryLotLines.mapNotNull { it.inventoryLot?.id }.distinct()
            if (inventoryLotIds.isNotEmpty()) {
                inventoryLotRepository.findAllByIdIn(inventoryLotIds).filter { it.deleted == false }
            }

            val stockOutLines = if (pickOrderLineIds.isNotEmpty() && inventoryLotLineIds.isNotEmpty()) {
                pickOrderLineIds.flatMap { polId ->
                    inventoryLotLineIds.flatMap { illId ->
                        stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(polId, illId)
                    }
                }
            } else {
                emptyList()
            }

            val stockOutLinesByPickOrderLine = pickOrderLineIds.associateWith { polId ->
                stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
            }

            val stockOutInventoryLotLineIds = stockOutLinesByPickOrderLine.values
                .flatten()
                .mapNotNull { it.inventoryLotLineId }
                .distinct()

            val stockOutInventoryLotLines = if (stockOutInventoryLotLineIds.isNotEmpty()) {
                inventoryLotLineRepository.findAllByIdIn(stockOutInventoryLotLineIds)
                    .filter { it.deleted == false }
            } else {
                emptyList()
            }

            val inventoryLotLineById = (inventoryLotLines + stockOutInventoryLotLines)
                .filter { it.id != null }
                .associateBy { it.id!! }

            val stockInLinesByInventoryLotLineId: Map<Long, Long> = buildMap {
                inventoryLotLines.forEach { ill ->
                    val id = ill.id ?: return@forEach
                    ill.inventoryLot?.stockInLine?.id?.let { put(id, it) }
                }
                stockOutInventoryLotLines.forEach { ill ->
                    val id = ill.id ?: return@forEach
                    if (!containsKey(id)) {
                        ill.inventoryLot?.stockInLine?.id?.let { put(id, it) }
                    }
                }
            }

            val joPickOrders = joPickOrderRepository.findByPickOrderId(pickOrder.id!!)
     
            val pickOrderInfo = PickOrderInfoWorkbenchResponse(
                id = pickOrder.id,
                code = pickOrder.code,
                consoCode = pickOrder.consoCode,
                targetDate = pickOrder.targetDate?.let {
                    "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}"
                },
                type = pickOrder.type?.value,
                status = pickOrder.status?.value,
                assignTo = pickOrder.assignTo?.id,
                jobOrder = JobOrderBasicInfoWorkbenchResponse(
                    id = jobOrder.id!!,
                    code = jobOrder.code ?: "",
                    name = "Job Order ${jobOrder.code}",
                    itemCode = jobOrder.bom?.code,
                    itemName = jobOrder.bom?.name,
                )
            )

            val pickOrderLinesResult = pickOrderLines.map { pol ->
                val item = pol.item
                val uom = pol.uom
                val lineId = pol.id!!
                val lineSuggestedLots = suggestedPickLots.filter { it.pickOrderLine?.id == pol.id }
                val jpo = joPickOrders.firstOrNull { it.itemId == item?.id }
                val handlerName = jpo?.handledBy?.let { uid ->
                    userService.find(uid).orElse(null)?.name
                }

                val lots = lineSuggestedLots.mapNotNull { spl ->
                    val ill = spl.suggestedLotLine
                    if (ill == null || ill.deleted == true) return@mapNotNull null

                    val il = ill.inventoryLot
                    if (il == null || il.deleted == true) return@mapNotNull null

                    val warehouse = ill.warehouse
                    val sol = stockOutLines.firstOrNull {
                        it.pickOrderLine?.id == pol.id && it.inventoryLotLine?.id == ill.id
                    }
                    val jpoInner = joPickOrders.firstOrNull { it.itemId == item?.id }
                    val handlerNameInner = jpoInner?.handledBy?.let { uid ->
                        userService.find(uid).orElse(null)?.name
                    }
                    //println("handlerName: $handlerNameInner")
                    val availableQty = if (sol?.status == "rejected") {
                        null
                    } else {
                        illAvailableQtyWorkbench(ill)
                    }

                    val lotAvailability = when {
                        il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired"
                        sol?.status == "rejected" -> "rejected"
                        ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable"
                        availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock"
                        else -> "available"
                    }

                    val processingStatus = when (sol?.status) {
                        "completed" -> "completed"
                        "rejected" -> "rejected"
                        "created" -> "pending"
                        else -> "pending"
                    }

                    val stockInLineId = ill.id?.let { illId ->
                        stockInLinesByInventoryLotLineId[illId]
                    }

                    LotDetailResponse(
                        lotId = ill.id,
                        lotNo = il.lotNo,
                        expiryDate = il.expiryDate?.let {
                            "${it.year}-${String.format("%02d", it.monthValue)}-${String.format("%02d", it.dayOfMonth)}"
                        },
                        location = warehouse?.code,
                        availableQty = availableQty?.toDouble(),
                        requiredQty = spl.qty?.toDouble() ?: 0.0,
                        actualPickQty = sol?.qty ?: 0.0,
                        processingStatus = processingStatus,
                        lotAvailability = lotAvailability,
                        pickOrderId = pickOrder.id,
                        pickOrderCode = pickOrder.code,
                        pickOrderConsoCode = pickOrder.consoCode,
                        pickOrderLineId = pol.id,
                        stockOutLineId = sol?.id,
                        stockInLineId = stockInLineId,
                        suggestedPickLotId = spl.id,
                        stockOutLineQty = sol?.qty ?: 0.0,
                        stockOutLineStatus = sol?.status,
                        routerIndex = warehouse?.order?.toString(),
                        routerArea = warehouse?.code,
                        routerRoute = warehouse?.code,
                        uomShortDesc = uom?.udfShortDesc,
                        matchStatus = jpoInner?.matchStatus?.value,
                        matchBy = jpoInner?.matchBy,
                        matchQty = jpoInner?.matchQty?.toDouble()
                    )
                }

                val stockouts = (stockOutLinesByPickOrderLine[lineId] ?: emptyList()).map { sol ->
                    val illId = sol.inventoryLotLineId
                    val ill = if (illId != null) inventoryLotLineById[illId] else null
                    val lot = ill?.inventoryLot
                    val warehouse = ill?.warehouse
                    val availableQty = if (sol.status == "rejected") {
                        null
                    } else if (ill == null || ill.deleted == true) {
                        null
                    } else {
                        illAvailableQtyWorkbench(ill)
                    }
                    val splForSol = lineSuggestedLots.firstOrNull { sp ->
                        sp.suggestedLotLine?.id != null && sp.suggestedLotLine?.id == illId
                    }

                    StockOutLineDetailResponse(
                        id = sol.id,
                        status = sol.status,
                        qty = sol.qty.toDouble(),
                        lotId = illId,
                        lotNo = sol.lotNo ?: lot?.lotNo,
                        location = warehouse?.code,
                        availableQty = availableQty?.toDouble(),
                        noLot = (illId == null),
                        suggestedPickQty = splForSol?.qty?.toDouble(),
                        suggestedPickLotId = splForSol?.id,
                    )
                }

                PickOrderLineWithLotsWorkbenchResponse(
                    id = pol.id!!,
                    itemId = item?.id,
                    itemCode = item?.code,
                    itemName = item?.name,
                    requiredQty = pol.qty?.toDouble(),
                    totalAvailableQty = item?.id?.let { totalAvailableQtyByItemId[it] },
                    uomCode = uom?.code,
                    uomDesc = uom?.udfudesc,
                    status = pol.status?.value,
                    lots = lots,
                    stockouts = stockouts,
                    handler = handlerName
                )
            }

            JobOrderLotsHierarchicalWorkbenchResponse(
                pickOrder = pickOrderInfo,
                pickOrderLines = pickOrderLinesResult
            )
        } catch (e: Exception) {
            println("❌ Error in getJobOrderLotsHierarchicalByPickOrderIdWorkbench: ${e.message}")
            e.printStackTrace()
            emptyHierarchical(e.message ?: "Error")
        }
    }

    private fun emptyHierarchical(message: String): JobOrderLotsHierarchicalWorkbenchResponse {
        println("❌ $message")
        return JobOrderLotsHierarchicalWorkbenchResponse(
            pickOrder = PickOrderInfoWorkbenchResponse(
                id = null,
                code = null,
                consoCode = null,
                targetDate = null,
                type = null,
                status = null,
                assignTo = null,
                jobOrder = JobOrderBasicInfoWorkbenchResponse(0, "", "",null,null)
            ),
            pickOrderLines = emptyList()
        )
    }

    /**
     * Workbench assign gate:
     * - only JO status = pending can trigger assign/prime
     * - after successful assign, move JO status to packaging to avoid re-assign on re-enter
     */
    @Transactional
    open fun assignJobOrderPickOrderToUserForWorkbench(pickOrderId: Long, userId: Long): MessageResponse {
        println("=== assignJobOrderPickOrderToUserForWorkbench ===")
        println("pickOrderId: $pickOrderId, userId: $userId")

        return try {
            val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null)
            if (pickOrder == null) {
                return MessageResponse(
                    id = null,
                    code = null,
                    name = null,
                    type = null,
                    message = "Pick order not found",
                    errorPosition = null
                )
            }

            if (pickOrder.type == PickOrderType.JOB_ORDER) {
                val jobOrder = pickOrder.jobOrder
                if (jobOrder == null) {
                    return MessageResponse(
                        id = pickOrder.id,
                        code = pickOrder.code,
                        name = null,
                        type = null,
                        message = "Job order not found for pick order",
                        errorPosition = null,
                    )
                }
                if (jobOrder.status != JobOrderStatus.PENDING) {
                    return MessageResponse(
                        id = pickOrder.id,
                        code = pickOrder.code,
                        name = jobOrder.bom?.name,
                        type = null,
                        message = "Skipped assign: job order status is ${jobOrder.status?.value}, only pending can assign",
                        errorPosition = null,
                    )
                }
                val splBefore = loadSplDebugRowsByPol(pickOrderId)
                suggestedPickLotWorkbenchService.primeNextSingleLotSuggestionsForPickOrder(
                    pickOrderId = pickOrderId,
                    storeId = null,
                    excludeWarehouseCodes = workbenchDefaultExcludeWarehouseCodes.toList()
                )
                stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId)
                val splAfter = loadSplDebugRowsByPol(pickOrderId)
                debugPrintAssignSplDiff(pickOrderId, splBefore, splAfter)
                debugPrintSuggestionNullReasons(pickOrderId)
                jobOrder.status = JobOrderStatus.PACKAGING
                jobOrderRepository.save(jobOrder)
            }

            MessageResponse(
                id = pickOrder.id,
                code = pickOrder.code,
                name = pickOrder.jobOrder?.bom?.name,
                type = null,
                message = "Successfully assigned",
                errorPosition = null
            )
        } catch (e: Exception) {
            println("❌ Error in assignJobOrderPickOrderToUserForWorkbench: ${e.message}")
            e.printStackTrace()
            MessageResponse(
                id = null,
                code = null,
                name = null,
                type = null,
                message = "Error occurred: ${e.message}",
                errorPosition = null
            )
        }
    }
}
