package com.ffii.fpsms.modules.pickOrder.service

import com.ffii.core.support.JdbcDao
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.PickOrderStatus
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderType
import com.ffii.fpsms.modules.pickOrder.web.models.PickOrderLineLotDetailResponse
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

@Service
open class PickOrderWorkbenchService(
    private val jdbcDao: JdbcDao,
    private val pickOrderRepository: PickOrderRepository,
    private val pickOrderLineRepository: PickOrderLineRepository,
    private val userService: UserService,
    private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService,
    private val stockOutLineWorkbenchService: StockOutLineWorkbenchService,
) {
    private fun toBigDecimal(v: Any?): BigDecimal? = when (v) {
        null -> null
        is BigDecimal -> v
        is Number -> BigDecimal(v.toString())
        is String -> v.toBigDecimalOrNull()
        else -> null
    }

    private fun toLong(v: Any?): Long? = when (v) {
        null -> null
        is Number -> v.toLong()
        is String -> v.toLongOrNull()
        else -> null
    }

    private fun toLocalDate(v: Any?): LocalDate? = when (v) {
        null -> null
        is LocalDate -> v
        is java.sql.Date -> v.toLocalDate()
        is java.sql.Timestamp -> v.toLocalDateTime().toLocalDate()
        is String -> runCatching { LocalDate.parse(v) }.getOrNull()
        else -> null
    }

    /**
     * Workbench assign V2 for consumable pick orders:
     * - Assign selected pick orders to user and mark ASSIGNED.
     * - Do NOT create suggestion/stock_out_line here (created on Tab3 line select).
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun assignPickOrdersV2(pickOrderIds: List<Long>, assignTo: Long): MessageResponse {
        try {
            val pickOrders = pickOrderRepository.findAllById(pickOrderIds)
            if (pickOrders.isEmpty()) {
                return MessageResponse(
                    id = null,
                    name = "No pick orders found",
                    code = "ERROR",
                    type = "pickorder_workbench",
                    message = "No pick orders found with the provided IDs",
                    errorPosition = null
                )
            }

            val user = userService.find(assignTo).orElse(null)
            pickOrders.forEach { pickOrder ->
                pickOrder.assignTo = user
                pickOrder.status = PickOrderStatus.ASSIGNED
            }
            pickOrderRepository.saveAll(pickOrders)
            val assignedIds = pickOrders.mapNotNull { it.id }

            return MessageResponse(
                id = null,
                name = "Workbench assign v2 success",
                code = "SUCCESS",
                type = "pickorder_workbench",
                message = "Assigned pick orders for workbench (no suggestion/stock-out at assign stage)",
                errorPosition = null,
                entity = mapOf(
                    "pickOrderIds" to assignedIds
                )
            )
        } catch (e: Exception) {
            return MessageResponse(
                id = null,
                name = "Workbench assign v2 failed",
                code = "ERROR",
                type = "pickorder_workbench",
                message = "Failed to assign workbench v2: ${e.message}",
                errorPosition = null
            )
        }
    }

    /**
     * Workbench release V2 for consumable pick orders:
     * no suggestion/hold side effects, only status transition to RELEASED.
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun releasePickOrdersV2(pickOrderIds: List<Long>, assignTo: Long): MessageResponse {
        try {
            val pickOrders = pickOrderRepository.findAllById(pickOrderIds)
            if (pickOrders.isEmpty()) {
                return MessageResponse(
                    id = null,
                    name = "No pick orders found",
                    code = "ERROR",
                    type = "pickorder_workbench",
                    message = "No pick orders found with the provided IDs",
                    errorPosition = null
                )
            }
            val user = userService.find(assignTo).orElse(null)
            pickOrders.forEach { po ->
                po.assignTo = user
                po.status = PickOrderStatus.RELEASED
            }
            pickOrderRepository.saveAll(pickOrders)
            return MessageResponse(
                id = null,
                name = "Workbench release v2 success",
                code = "SUCCESS",
                type = "pickorder_workbench",
                message = "Released pick orders with no-hold flow",
                errorPosition = null,
                entity = mapOf("pickOrderIds" to pickOrders.mapNotNull { it.id })
            )
        } catch (e: Exception) {
            return MessageResponse(
                id = null,
                name = "Workbench release v2 failed",
                code = "ERROR",
                type = "pickorder_workbench",
                message = "Failed to release workbench v2: ${e.message}",
                errorPosition = null
            )
        }
    }

    /**
     * Consumable workbench hierarchical payload for Tab3.
     * Uses no-hold available qty semantics (inQty - outQty).
     */
    open fun getAllPickOrderLotsWithDetailsHierarchicalWorkbench(userId: Long): Map<String, Any?> {
        val pickOrders = pickOrderRepository.findAllByAssignToIdAndStatusIn(
            assignToId = userId,
            statuses = listOf(PickOrderStatus.ASSIGNED, PickOrderStatus.RELEASED, PickOrderStatus.PICKING),
        ).filter {
            it.type?.value?.lowercase() != "do" && it.type?.value?.lowercase() != "jo"
        }

        if (pickOrders.isEmpty()) {
            return mapOf("fgInfo" to null, "pickOrders" to emptyList<Any>())
        }

        val out = pickOrders.map { po ->
            val poId = po.id ?: 0L
            val lines = pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(poId)
            val linePayload = lines.map { pol ->
                val polId = pol.id ?: 0L
                val sql = """
                    SELECT
                      sol.id AS stockOutLineId,
                      sol.status AS stockOutLineStatus,
                      COALESCE(sol.qty, 0) AS stockOutLineQty,
                      sol.inventoryLotLineId AS inventoryLotLineId,
                      ill.id AS lotId,
                      il.lotNo AS lotNo,
                      il.stockInLineId AS stockInLineId,
                      DATE_FORMAT(il.expiryDate, '%Y-%m-%d') AS expiryDate,
                      w.code AS location,
                      COALESCE(ill.inQty,0) AS inQty,
                      COALESCE(ill.outQty,0) AS outQty,
                      COALESCE(ill.holdQty,0) AS holdQty,
                      (COALESCE(ill.inQty,0) - COALESCE(ill.outQty,0)) AS availableQty,
                      ill.status AS lotStatus,
                      spl.suggestedPickLotId AS suggestedPickLotId,
                      COALESCE(spl.splQty, sol.qty, 0) AS suggestedQty
                    FROM fpsmsdb.stock_out_line sol
                    LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = sol.inventoryLotLineId
                    LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId
                    LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
                    LEFT JOIN (
                        SELECT
                          s.stockOutLineId AS solId,
                          MAX(s.id) AS suggestedPickLotId,
                          SUM(COALESCE(s.qty, 0)) AS splQty
                        FROM fpsmsdb.suggested_pick_lot s
                        WHERE s.pickOrderLineId = :pickOrderLineId
                          AND s.deleted = false
                          AND s.stockOutLineId IS NOT NULL
                        GROUP BY s.stockOutLineId
                    ) spl ON spl.solId = sol.id
                    WHERE sol.pickOrderLineId = :pickOrderLineId
                      AND sol.deleted = false
                    ORDER BY sol.id
                """.trimIndent()
                val lotRows = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to polId))
                val zero = BigDecimal.ZERO
                val lots = lotRows.map { r ->
                    val isNoLot = r["inventoryLotLineId"] == null
                    val inQty = toBigDecimal(r["inQty"]) ?: zero
                    val outQty = toBigDecimal(r["outQty"]) ?: zero
                    val availableQty = toBigDecimal(r["availableQty"]) ?: inQty.subtract(outQty)
                    val status = (r["stockOutLineStatus"]?.toString() ?: "").lowercase()
                    val lotStatusRaw = (r["lotStatus"]?.toString() ?: "").lowercase()
                    val suggestedQty = toBigDecimal(r["suggestedQty"]) ?: zero
                    val stockOutLineQty = toBigDecimal(r["stockOutLineQty"]) ?: zero
                    val lotAvailability = when {
                        isNoLot && status != "completed" -> "insufficient_stock"
                        status == "rejected" -> "rejected"
                        !isNoLot && lotStatusRaw == "unavailable" -> "status_unavailable"
                        !isNoLot && availableQty <= zero && status != "completed" -> "insufficient_stock"
                        else -> "available"
                    }
                    mapOf(
                        "id" to r["lotId"],
                        "lotNo" to r["lotNo"],
                        "expiryDate" to r["expiryDate"],
                        "location" to r["location"],
                        "stockUnit" to (pol.uom?.udfudesc ?: pol.uom?.code ?: ""),
                        "availableQty" to if (isNoLot) null else availableQty,
                        "requiredQty" to suggestedQty,
                        "actualPickQty" to stockOutLineQty,
                        "inQty" to if (isNoLot) null else inQty,
                        "outQty" to if (isNoLot) null else outQty,
                        "holdQty" to if (isNoLot) null else toBigDecimal(r["holdQty"]),
                        "lotStatus" to if (isNoLot) "unavailable" else r["lotStatus"],
                        "lotAvailability" to lotAvailability,
                        "noLot" to isNoLot,
                        "processingStatus" to (r["stockOutLineStatus"] ?: "pending"),
                        "suggestedPickLotId" to r["suggestedPickLotId"],
                        "stockOutLineId" to r["stockOutLineId"],
                        "stockOutLineStatus" to r["stockOutLineStatus"],
                        "stockOutLineQty" to stockOutLineQty,
                        "stockInLineId" to r["stockInLineId"],
                    )
                }
                mapOf(
                    "id" to polId,
                    "requiredQty" to pol.qty,
                    "status" to pol.status?.value,
                    "item" to mapOf(
                        "id" to pol.item?.id,
                        "code" to pol.item?.code,
                        "name" to pol.item?.name,
                        "uomId" to pol.uom?.id,
                        "uomCode" to pol.uom?.code,
                        "uomDesc" to pol.uom?.udfudesc,
                    ),
                    "lots" to lots,
                )
            }
            mapOf(
                "pickOrderId" to poId,
                "pickOrderCode" to (po.code ?: ""),
                "status" to po.status?.value,
                "targetDate" to po.targetDate?.toLocalDate()?.toString(),
                "pickOrderLines" to linePayload,
            )
        }

        return mapOf(
            "fgInfo" to mapOf("mode" to "consumable-workbench", "userId" to userId),
            "pickOrders" to out,
        )
    }

    /**
     * Workbench line detail V2 (consumable):
     * - Returns only lot rows already linked by suggestion/stock_out_line for this pick order line.
     * - Uses no-hold availability semantics: available = inQty - outQty.
     * - Includes no-lot stock_out_line rows.
     */
    open fun getWorkbenchPickOrderLineLotDetailsV2(pickOrderLineId: Long): List<PickOrderLineLotDetailResponse> {
        val zero = BigDecimal.ZERO
        val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return emptyList()
        val uomDesc = pol.uom?.udfudesc ?: pol.uom?.code

        val lotSql = """
            SELECT
              ill.id AS lotId,
              il.stockInLineId AS stockInLineId,
              il.lotNo AS lotNo,
              il.expiryDate AS expiryDate,
              w.code AS location,
              COALESCE(ill.inQty,0) AS inQty,
              COALESCE(ill.outQty,0) AS outQty,
              COALESCE(ill.holdQty,0) AS holdQty,
              (COALESCE(ill.inQty,0) - COALESCE(ill.outQty,0)) AS availableQty,
              spl.suggestedPickLotId AS suggestedPickLotId,
              spl.requiredQty AS suggestedQty,
              ill.status AS lotStatus,
              sol.stockOutLineId AS stockOutLineId,
              sol.stockOutLineStatus AS stockOutLineStatus,
              COALESCE(sol.stockOutLineQty,0) AS stockOutLineQty
            FROM fpsmsdb.inventory_lot_line ill
            LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId
            LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId
            LEFT JOIN (
                SELECT
                  s.suggestedLotLineId AS lotLineId,
                  MAX(s.id) AS suggestedPickLotId,
                  SUM(COALESCE(s.qty,0)) AS requiredQty
                FROM fpsmsdb.suggested_pick_lot s
                WHERE s.pickOrderLineId = :pickOrderLineId
                  AND s.deleted = false
                  AND s.suggestedLotLineId IS NOT NULL
                GROUP BY s.suggestedLotLineId
            ) spl ON spl.lotLineId = ill.id
            LEFT JOIN (
                SELECT
                  so.inventoryLotLineId AS lotLineId,
                  MAX(so.id) AS stockOutLineId,
                  SUBSTRING_INDEX(GROUP_CONCAT(so.status ORDER BY so.id DESC), ',', 1) AS stockOutLineStatus,
                  SUM(COALESCE(so.qty,0)) AS stockOutLineQty
                FROM fpsmsdb.stock_out_line so
                WHERE so.pickOrderLineId = :pickOrderLineId
                  AND so.deleted = false
                  AND so.inventoryLotLineId IS NOT NULL
                GROUP BY so.inventoryLotLineId
            ) sol ON sol.lotLineId = ill.id
            WHERE ill.id IN (
                SELECT DISTINCT s.suggestedLotLineId
                FROM fpsmsdb.suggested_pick_lot s
                WHERE s.pickOrderLineId = :pickOrderLineId
                  AND s.deleted = false
                  AND s.suggestedLotLineId IS NOT NULL
                UNION
                SELECT DISTINCT so.inventoryLotLineId
                FROM fpsmsdb.stock_out_line so
                WHERE so.pickOrderLineId = :pickOrderLineId
                  AND so.deleted = false
                  AND so.inventoryLotLineId IS NOT NULL
            )
            ORDER BY il.expiryDate, il.lotNo
        """.trimIndent()

        val lotRows = jdbcDao.queryForList(lotSql, mapOf("pickOrderLineId" to pickOrderLineId))
        val withLot = lotRows.map { r ->
            val inQty = toBigDecimal(r["inQty"]) ?: zero
            val outQty = toBigDecimal(r["outQty"]) ?: zero
            val holdQty = toBigDecimal(r["holdQty"]) ?: zero
            val availableQty = toBigDecimal(r["availableQty"]) ?: inQty.subtract(outQty)
            val status = (r["stockOutLineStatus"]?.toString() ?: "").lowercase()
            val lotAvailability = when {
                status == "rejected" -> "rejected"
                (r["lotStatus"]?.toString() ?: "").lowercase() == "unavailable" -> "status_unavailable"
                availableQty <= zero -> "insufficient_stock"
                else -> "available"
            }
            PickOrderLineLotDetailResponse(
                lotId = toLong(r["lotId"]),
                lotNo = r["lotNo"]?.toString(),
                expiryDate = toLocalDate(r["expiryDate"]),
                location = r["location"]?.toString(),
                itemId = pol.item?.id,
                stockInLineId = toLong(r["stockInLineId"]),
                stockUnit = uomDesc,
                availableQty = availableQty,
                requiredQty = toBigDecimal(r["suggestedQty"]) ?: pol.qty ?: zero,
                inQty = inQty,
                outQty = outQty,
                holdQty = holdQty,
                actualPickQty = toBigDecimal(r["stockOutLineQty"]) ?: zero,
                suggestedPickLotId = toLong(r["suggestedPickLotId"]),
                lotStatus = r["lotStatus"]?.toString(),
                stockOutLineId = toLong(r["stockOutLineId"]),
                stockOutLineStatus = r["stockOutLineStatus"]?.toString(),
                stockOutLineQty = toBigDecimal(r["stockOutLineQty"]) ?: zero,
                totalPickedByAllPickOrders = toBigDecimal(r["stockOutLineQty"]) ?: zero,
                remainingAfterAllPickOrders = availableQty,
                lotAvailability = lotAvailability,
                noLot = false,
            )
        }

        val noLotSql = """
            SELECT
              so.id AS stockOutLineId,
              so.status AS stockOutLineStatus,
              COALESCE(so.qty,0) AS stockOutLineQty
            FROM fpsmsdb.stock_out_line so
            WHERE so.pickOrderLineId = :pickOrderLineId
              AND so.deleted = false
              AND so.inventoryLotLineId IS NULL
            ORDER BY so.id
        """.trimIndent()
        val noLotRows = jdbcDao.queryForList(noLotSql, mapOf("pickOrderLineId" to pickOrderLineId))
        val noLot = noLotRows.map { r ->
            val stockOutLineQty = toBigDecimal(r["stockOutLineQty"]) ?: zero
            PickOrderLineLotDetailResponse(
                lotId = null,
                lotNo = null,
                expiryDate = null,
                location = null,
                itemId = pol.item?.id,
                stockInLineId = null,
                stockUnit = uomDesc,
                availableQty = null,
                requiredQty = stockOutLineQty,
                inQty = null,
                outQty = null,
                holdQty = null,
                actualPickQty = stockOutLineQty,
                suggestedPickLotId = null,
                lotStatus = "unavailable",
                stockOutLineId = toLong(r["stockOutLineId"]),
                stockOutLineStatus = r["stockOutLineStatus"]?.toString(),
                stockOutLineQty = stockOutLineQty,
                totalPickedByAllPickOrders = null,
                remainingAfterAllPickOrders = null,
                lotAvailability = "insufficient_stock",
                noLot = true,
            )
        }

        return withLot + noLot
    }

    /**
     * Workbench suggest V2 for consumable pick orders (first-time suggestion path):
     * - Create/rebuild no-hold suggestions for this pick order.
     * - Ensure stock_out_line rows exist for created/reused suggestions.
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun suggestPickOrderV2(pickOrderId: Long, userId: Long): MessageResponse {
        return try {
            val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null)
                ?: return MessageResponse(
                    id = pickOrderId,
                    name = "Workbench suggest v2 failed",
                    code = "ERROR",
                    type = "pickorder_workbench",
                    message = "Pick order not found",
                    errorPosition = null
                )

            val excludeWarehouseCodes =
                if (pickOrder.type == PickOrderType.Consumable) {
                    ConsumableWorkbenchPickConstants.resolveExcludeWarehouseCodes(userId)
                } else {
                    null
                }

            val suggestSummary = suggestedPickLotWorkbenchService.primeNextSingleLotSuggestionsForPickOrder(
                pickOrderId = pickOrderId,
                storeId = null,
                excludeWarehouseCodes = excludeWarehouseCodes,
            )
            val stockOutSummary = stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(
                pickOrderId = pickOrderId,
                userId = userId,
            )

            MessageResponse(
                id = pickOrder.id,
                name = "Workbench suggest v2 success",
                code = "SUCCESS",
                type = "pickorder_workbench",
                message = "Suggestion success",
                errorPosition = null,
                entity = mapOf(
                    "pickOrderId" to pickOrderId,
                    "suggestionRowsCreated" to suggestSummary.created,
                    "suggestionRowsSkippedExisting" to suggestSummary.skippedExisting,
                    "stockOutLinesCreated" to stockOutSummary.created,
                    "stockOutLinesReused" to stockOutSummary.reused,
                )
            )
        } catch (e: Exception) {
            MessageResponse(
                id = pickOrderId,
                name = "Workbench suggest v2 failed",
                code = "ERROR",
                type = "pickorder_workbench",
                message = "Failed to prepare workbench suggestion: ${e.message}",
                errorPosition = null
            )
        }
    }
}

