package com.ffii.fpsms.modules.stock.trace

import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.stock.web.model.ItemLotTraceResponse
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.server.ResponseStatusException

@Component
open class ItemLotTraceOrchestrator(
    private val lotResolver: TraceLotResolver,
    private val inboundLoader: TraceInboundLoader,
    private val movementLoader: TraceMovementLoader,
    private val outboundLoader: TraceOutboundLoader,
    private val bomLoader: TraceBomLoader,
    private val productionLoader: TraceProductionLoader,
    private val joPreludeLoader: TraceJoPreludeLoader,
    private val extendedLoader: TraceExtendedLoader,
    private val upstreamLoader: TraceUpstreamLoader,
    private val itemsRepository: ItemsRepository,
    private val traceGraphSemanticsBuilder: TraceGraphSemanticsBuilder,
) {
    open fun trace(
        stockInLineId: Long?,
        inventoryLotLineId: Long?,
        lotNo: String?,
        itemId: Long?,
        itemCode: String?,
    ): ItemLotTraceResponse {
        val resolvedItemId = itemId ?: itemCode?.trim()?.takeIf { it.isNotEmpty() }?.let { code ->
            itemsRepository.findByCodeAndDeletedFalse(code)?.id
                ?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Item code not found: $code")
        }

        val inventoryLotId = lotResolver.resolveInventoryLotId(
            stockInLineId,
            inventoryLotLineId,
            lotNo,
            resolvedItemId,
        )
        val lot = lotResolver.loadLotInfo(inventoryLotId)
        val lotLineIds = lotResolver.loadLotLineIds(inventoryLotId)
        val stockInLineIds = lotResolver.loadStockInLineIds(inventoryLotId)

        val joProvenance = upstreamLoader.resolveJoProvenance(
            inventoryLotId,
            itemId = lot.itemId,
            lotNo = lot.lotNo,
        )

        val effectiveJoSilId = lot.primaryStockInLineId?.takeIf { bomLoader.hasJoStockIn(it) }
            ?: joProvenance?.joStockInLineId
        val joId = lotResolver.resolveJobOrderIdForLot(inventoryLotId, lot.primaryStockInLineId)
            ?: joProvenance?.jobOrderId

        val warehouseLines = lotResolver.loadWarehouseLines(inventoryLotId)
        val alternateLocations = lotResolver.loadAlternateLocations(lot.itemId, lot.lotNo, inventoryLotId)
        val altLotIdSet = alternateLocations.map { it.inventoryLotId }.toSet()

        // Transfer-chain / JO upstream lots whose PO/QC/putaway should surface on the
        // destination lot when its own stock-in is only TRANSFER (helpers were unused).
        // Sibling same-lotNo lots already arrive via locationBlocks — exclude them here
        // to avoid duplicating rows in merged tabs.
        val upstreamMergeLotIds = (
            upstreamLoader.resolveUpstreamLotIds(inventoryLotId, lot.itemId, lot.lotNo) +
                (joProvenance?.upstreamLotIds.orEmpty())
            )
            .distinct()
            .filter { it != inventoryLotId && it !in altLotIdSet }

        val origins = inboundLoader.loadOriginsForLots(listOf(inventoryLotId) + upstreamMergeLotIds)
            .distinctBy { it.stockInLineId }
        val upstreamStockInLineIds = if (upstreamMergeLotIds.isNotEmpty()) {
            lotResolver.loadStockInLineIdsForLots(upstreamMergeLotIds).values.flatten().distinct()
        } else {
            emptyList()
        }
        val mergedStockInLineIds = (stockInLineIds + upstreamStockInLineIds).distinct()
        val purchaseEvents = inboundLoader.loadPurchaseEvents(mergedStockInLineIds)
            .distinctBy { it.purchaseOrderLineId }
        val qcResults = inboundLoader.loadQcResults(mergedStockInLineIds)
            .distinctBy { "${it.stockInLineId}|${it.qcItemCode}|${it.qcType}|${it.created}" }
        val putawayEvents = (
            inboundLoader.loadPutawayEvents(inventoryLotId) +
                upstreamMergeLotIds.flatMap { inboundLoader.loadPutawayEvents(it) }
            )
            .distinctBy { "${it.inventoryLotLineId}|${it.timestamp}|${it.refCode}|${it.qty}" }
        val movements = movementLoader.loadMovements(inventoryLotId, lotLineIds)
        val outboundUsage = outboundLoader.loadOutboundUsage(inventoryLotId)
        val stockTakeEvents = movementLoader.loadStockTakeEvents(lotLineIds)
        val adjustments = movementLoader.loadAdjustments(inventoryLotId, lotLineIds)
        val transfers = movementLoader.loadTransfers(inventoryLotId)
        val bomTrace = bomLoader.loadBomTrace(
            inventoryLotId,
            effectiveJoSilId ?: lot.primaryStockInLineId,
            lot.itemId,
            lotLineIds,
        )
        val joPrelude = effectiveJoSilId?.let { silId ->
            if (bomLoader.hasJoStockIn(silId)) joPreludeLoader.loadJoPrelude(silId) else null
        }
        val productionSteps = joId?.let { productionLoader.loadProductionSteps(it) } ?: emptyList()
        val byproductLots = joId?.let { productionLoader.loadByproductLots(it, lot.itemId) } ?: emptyList()
        val openMovements = movementLoader.loadOpenMovements(inventoryLotId)
        val failEvents = extendedLoader.loadFailEvents(inventoryLotId)
        val rawMovements = movements.sortedByDescending { it.timestamp ?: "" }
        val doDeliveries = resolveDoDeliveries(listOf(inventoryLotId), rawMovements)
        val movementsWithDoMeta = enrichDoMovements(rawMovements, doDeliveries)
        val returnEvents = outboundLoader.loadReturnEvents(inventoryLotId, lotLineIds)
        val replenishmentEvents = outboundLoader.loadReplenishmentEvents(inventoryLotId)
        val lotRelations = extendedLoader.loadLotRelations(inventoryLotId, lot.itemId, lot.lotNo)

        // Batch pre-load alternate location data to avoid N+1 queries
        val altLotIds = alternateLocations.map { it.inventoryLotId }
        val altLotInfoByLotId = if (altLotIds.isNotEmpty()) lotResolver.loadLotInfoBatch(altLotIds) else emptyMap()
        val altStockInLineIdsByLotId = if (altLotIds.isNotEmpty()) lotResolver.loadStockInLineIdsForLots(altLotIds) else emptyMap()
        val altLotLineIdsByLotId = if (altLotIds.isNotEmpty()) lotResolver.loadLotLineIdsForLots(altLotIds) else emptyMap()
        val altPutawayByLotId = if (altLotIds.isNotEmpty()) inboundLoader.loadPutawayEventsByLotIds(altLotIds) else emptyMap()

        // Batch pre-load origins, purchase events, and QC results across ALL alternate locations
        val altOriginsByLotId: Map<Long, List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceOrigin>> =
            if (altLotIds.isNotEmpty()) {
                inboundLoader.loadOriginsForLots(altLotIds)
                    .groupBy { origin ->
                        // Map back to lot via stockInLineId lookup
                        altStockInLineIdsByLotId.entries
                            .firstOrNull { (_, silIds) -> silIds.contains(origin.stockInLineId) }?.key
                            ?: altLotIds.first()
                    }
            } else emptyMap()

        val allAltStockInLineIds = altStockInLineIdsByLotId.values.flatten().distinct()
        val altPurchaseEventsBySilId: Map<Long, List<com.ffii.fpsms.modules.stock.web.model.ItemLotTracePurchaseEvent>> =
            if (allAltStockInLineIds.isNotEmpty()) {
                inboundLoader.loadPurchaseEvents(allAltStockInLineIds)
                    .groupBy { it.purchaseOrderLineId }
            } else emptyMap()

        val altQcResultsBySilId: Map<Long, List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceQcResult>> =
            if (allAltStockInLineIds.isNotEmpty()) {
                inboundLoader.loadQcResults(allAltStockInLineIds)
                    .groupBy { it.stockInLineId }
            } else emptyMap()

        val locationBlocks = alternateLocations.map { alt ->
            val altLot = altLotInfoByLotId[alt.inventoryLotId]
            val altSilIds = altStockInLineIdsByLotId[alt.inventoryLotId].orEmpty()
            traceLocationBlock(
                inventoryLotId = alt.inventoryLotId,
                itemCode = altLot?.itemCode ?: lot.itemCode,
                lotNo = altLot?.lotNo ?: lot.lotNo,
                itemName = altLot?.itemName ?: "",
                uom = altLot?.uom ?: "",
                expiryDate = altLot?.expiryDate,
                productionDate = altLot?.productionDate,
                stockInDate = altLot?.stockInDate,
                primaryStockInLineId = altLot?.primaryStockInLineId,
                preloadedStockInLineIds = altSilIds,
                preloadedLotLineIds = altLotLineIdsByLotId[alt.inventoryLotId],
                preloadedPutawayEvents = altPutawayByLotId[alt.inventoryLotId],
                preloadedOrigins = altOriginsByLotId[alt.inventoryLotId],
                preloadedPurchaseEvents = altSilIds.flatMap { sid -> altPurchaseEventsBySilId[sid].orEmpty() }.distinctBy { it.purchaseOrderLineId },
                preloadedQcResults = altSilIds.flatMap { sid -> altQcResultsBySilId[sid].orEmpty() },
            )
        }

        return ItemLotTraceResponse(
            lot = lot,
            warehouseLines = warehouseLines,
            origins = origins,
            purchaseEvents = purchaseEvents,
            qcResults = qcResults,
            putawayEvents = putawayEvents.sortedBy { it.timestamp ?: "" },
            movements = movementsWithDoMeta,
            outboundUsage = outboundUsage.sortedByDescending { it.timestamp ?: "" },
            stockTakeEvents = stockTakeEvents.sortedByDescending { it.timestamp ?: "" },
            adjustments = adjustments.sortedByDescending { it.timestamp ?: "" },
            transfers = transfers.sortedByDescending { it.timestamp ?: "" },
            bomTrace = bomTrace,
            joPrelude = joPrelude,
            productionSteps = productionSteps,
            byproductLots = byproductLots,
            openMovements = openMovements,
            failEvents = failEvents,
            doDeliveries = doDeliveries,
            replenishmentEvents = replenishmentEvents,
            returnEvents = returnEvents,
            lotRelations = lotRelations,
            alternateLocations = alternateLocations,
            locationBlocks = locationBlocks,
        ).let { response ->
            response.copy(traceGraph = traceGraphSemanticsBuilder.build(response))
        }
    }

    private fun resolveDoDeliveries(
        mergedLotIds: List<Long>,
        movements: List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceMovement>,
    ): List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceDoDelivery> {
        val fromLots = mergedLotIds.flatMap { outboundLoader.loadDoDeliveries(it) }
        val covered = fromLots.map { it.pickOrderCode.trim() }.filter { it.isNotEmpty() }.toSet()
        val missingCodes = movements
            .filter { it.refType.equals("DO", ignoreCase = true) }
            .map { it.refCode.trim() }
            .filter { it.isNotEmpty() && it !in covered }
            .distinct()
        val supplemental = outboundLoader.loadDoDeliveriesByPickOrderCodes(missingCodes)
        return (fromLots + supplemental).distinctBy { it.stockOutLineId }
    }

    /** FP-MTMS Version Checklist | Functions Ref. No. 24 | v1.0.0 | 2026-07-20 */
    private fun enrichDoMovements(
        movements: List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceMovement>,
        doDeliveries: List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceDoDelivery>,
    ): List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceMovement> {
        val metaByPickCode = doDeliveries.associateBy { it.pickOrderCode.trim() }
        return movements.map { movement ->
            if (!movement.refType.equals("DO", ignoreCase = true)) return@map movement
            val meta = metaByPickCode[movement.refCode.trim()] ?: return@map movement
            movement.copy(
                pickConsoCode = meta.consoCode,
                outboundTicketNo = meta.ticketNo,
                deliveryOrderPickOrderId = meta.deliveryOrderPickOrderId,
                relationshipId = meta.relationshipId,
                doOutboundIsExtra = meta.isExtra,
                doOutboundIsReplenish = meta.isReplenish,
                doOutboundQtyChanged = meta.qtyChanged,
            )
        }
    }

    /**
     * Builds a location-scoped trace block for a single alternate [inventoryLotId].
     *
     * Only includes dimensions that are location-scoped (warehouse lines, movements, origins,
     * outbound usage, stock take, etc.). Lot-level dimensions (BOM, JO prelude, production steps,
     * byproduct lots, lot relations, trace graph) are NOT included — they're already at the top
     * level of [ItemLotTraceResponse] and are shared across all locations of the same lot.
     */
    open fun traceLocationBlock(
        inventoryLotId: Long,
        itemCode: String,
        lotNo: String,
        itemName: String = "",
        uom: String = "",
        expiryDate: String? = null,
        productionDate: String? = null,
        stockInDate: String? = null,
        primaryStockInLineId: Long? = null,
        /** Pre-loaded from batch query — avoids per-location stockInLineIds query. */
        preloadedStockInLineIds: List<Long>? = null,
        /** Pre-loaded from batch query — avoids per-location lotLineIds query. */
        preloadedLotLineIds: List<Long>? = null,
        /** Pre-loaded from batch query — avoids per-location putaway query. */
        preloadedPutawayEvents: List<com.ffii.fpsms.modules.stock.web.model.ItemLotTracePutawayEvent>? = null,
        /** Pre-loaded from batch query — avoids per-location origins query. */
        preloadedOrigins: List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceOrigin>? = null,
        /** Pre-loaded from batch query — avoids per-location purchaseEvents query. */
        preloadedPurchaseEvents: List<com.ffii.fpsms.modules.stock.web.model.ItemLotTracePurchaseEvent>? = null,
        /** Pre-loaded from batch query — avoids per-location qcResults query. */
        preloadedQcResults: List<com.ffii.fpsms.modules.stock.web.model.ItemLotTraceQcResult>? = null,
    ): com.ffii.fpsms.modules.stock.web.model.ItemLotTraceLocationBlock {
        val lotLineIds = preloadedLotLineIds ?: lotResolver.loadLotLineIds(inventoryLotId)
        val stockInLineIds = preloadedStockInLineIds ?: lotResolver.loadStockInLineIds(inventoryLotId)
        val effectiveSilId = primaryStockInLineId ?: stockInLineIds.firstOrNull()
        val rawMovements = movementLoader.loadMovements(inventoryLotId, lotLineIds)
            .sortedByDescending { it.timestamp ?: "" }
        val doDeliveries = resolveDoDeliveries(listOf(inventoryLotId), rawMovements)
        val movementsWithDoMeta = enrichDoMovements(rawMovements, doDeliveries)

        return com.ffii.fpsms.modules.stock.web.model.ItemLotTraceLocationBlock(
            inventoryLotId = inventoryLotId,
            lotNo = lotNo,
            itemCode = itemCode,
            itemName = itemName,
            uom = uom,
            expiryDate = expiryDate,
            productionDate = productionDate,
            stockInDate = stockInDate,
            stockInLineId = effectiveSilId,
            warehouseLines = lotResolver.loadWarehouseLines(inventoryLotId),
            origins = preloadedOrigins ?: inboundLoader.loadOrigins(inventoryLotId),
            purchaseEvents = preloadedPurchaseEvents ?: inboundLoader.loadPurchaseEvents(stockInLineIds),
            qcResults = preloadedQcResults ?: inboundLoader.loadQcResults(stockInLineIds),
            putawayEvents = preloadedPutawayEvents ?: inboundLoader.loadPutawayEvents(inventoryLotId),
            movements = movementsWithDoMeta,
            outboundUsage = outboundLoader.loadOutboundUsage(inventoryLotId),
            stockTakeEvents = movementLoader.loadStockTakeEvents(lotLineIds),
            adjustments = movementLoader.loadAdjustments(inventoryLotId, lotLineIds),
            transfers = movementLoader.loadTransfers(inventoryLotId),
            openMovements = movementLoader.loadOpenMovements(inventoryLotId),
            failEvents = extendedLoader.loadFailEvents(inventoryLotId),
            doDeliveries = doDeliveries,
            replenishmentEvents = outboundLoader.loadReplenishmentEvents(inventoryLotId),
            returnEvents = outboundLoader.loadReturnEvents(inventoryLotId, lotLineIds),
        )
    }

    /**
     * Lazy-load entry point: resolves lot info from [inventoryLotId] then delegates to
     * [traceLocationBlock]. Used by the controller's `GET /trace/location/{inventoryLotId}` endpoint.
     */
    open fun traceLocation(
        inventoryLotId: Long,
    ): com.ffii.fpsms.modules.stock.web.model.ItemLotTraceLocationBlock {
        val lot = lotResolver.loadLotInfo(inventoryLotId)
        return traceLocationBlock(
            inventoryLotId = inventoryLotId,
            itemCode = lot.itemCode,
            lotNo = lot.lotNo,
            itemName = lot.itemName,
            uom = lot.uom,
            expiryDate = lot.expiryDate,
            productionDate = lot.productionDate,
            stockInDate = lot.stockInDate,
            primaryStockInLineId = lot.primaryStockInLineId,
        )
    }
}
