package com.ffii.fpsms.modules.deliveryOrder.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByDeliveryOrderPickOrderIdRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByLaneRequest
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import com.ffii.fpsms.modules.stock.service.StockOutLineWorkbenchService
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotWorkbenchService
import com.ffii.fpsms.modules.user.entity.User
import com.ffii.fpsms.modules.user.entity.UserRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.sql.Time
import java.time.LocalDateTime
import java.time.LocalTime

@Service
open class DoWorkbenchDopoAssignmentService(
    private val jdbcDao: JdbcDao,
    private val userRepository: UserRepository,
    private val pickOrderRepository: PickOrderRepository,
    private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService,
    private val stockOutLineWorkbenchService: StockOutLineWorkbenchService,
) {

    @Transactional
    open fun assignByDeliveryOrderPickOrderId(request: AssignByDeliveryOrderPickOrderIdRequest): MessageResponse {
        val user = userRepository.findById(request.userId).orElse(null)
            ?: return userNotFound()

        userBusyResponse(request.userId)?.let { return it }

        val headerRows = jdbcDao.queryForList(
            """
            SELECT id, ticketStatus
            FROM fpsmsdb.delivery_order_pick_order
            WHERE id = :id AND deleted = 0
            """.trimIndent(),
            mapOf("id" to request.deliveryOrderPickOrderId)
        )
        if (headerRows.isEmpty()) {
            return MessageResponse(
                id = null, code = "NOT_FOUND", name = null, type = null,
                message = "delivery_order_pick_order not found", errorPosition = null, entity = null
            )
        }
        val headerRow = headerRows.first()
        val stKey = headerRow.keys.find { it.equals("ticketStatus", true) }
        val status = stKey?.let { headerRow[it]?.toString() } ?: ""
        if (status !in listOf("pending", "released")) {
            return MessageResponse(
                id = null, code = "INVALID_STATUS", name = null, type = null,
                message = "Ticket not assignable", errorPosition = null, entity = null
            )
        }

        return applyDopoAssignment(user, request.userId, request.deliveryOrderPickOrderId, mode = AssignMode.ATOMIC)
    }

    /** Legacy assign (V1): old FG-style (no atomic conflict guard). */
    @Transactional
    open fun assignByDeliveryOrderPickOrderIdV1(request: AssignByDeliveryOrderPickOrderIdRequest): MessageResponse {
        val user = userRepository.findById(request.userId).orElse(null)
            ?: return userNotFound()

        userBusyResponse(request.userId)?.let { return it }

        val headerRows = jdbcDao.queryForList(
            """
            SELECT id, ticketStatus
            FROM fpsmsdb.delivery_order_pick_order
            WHERE id = :id AND deleted = 0
            """.trimIndent(),
            mapOf("id" to request.deliveryOrderPickOrderId)
        )
        if (headerRows.isEmpty()) {
            return MessageResponse(
                id = null, code = "NOT_FOUND", name = null, type = null,
                message = "delivery_order_pick_order not found", errorPosition = null, entity = null
            )
        }
        val headerRow = headerRows.first()
        val stKey = headerRow.keys.find { it.equals("ticketStatus", true) }
        val status = stKey?.let { headerRow[it]?.toString() } ?: ""
        if (status !in listOf("pending", "released")) {
            return MessageResponse(
                id = null, code = "INVALID_STATUS", name = null, type = null,
                message = "Ticket not assignable", errorPosition = null, entity = null
            )
        }

        return applyDopoAssignment(user, request.userId, request.deliveryOrderPickOrderId, mode = AssignMode.LEGACY_V1)
    }

    /**
     * Same UX as [DoPickOrderAssignmentService.assignByLane] but candidates come from
     * [delivery_order_pick_order] (+ unassigned [pick_order]), not [do_pick_order].
     */
    @Transactional
    open fun assignByLaneForWorkbench(request: AssignByLaneRequest): MessageResponse {
        val user = userRepository.findById(request.userId).orElse(null)
            ?: return userNotFound()

        userBusyResponse(request.userId)?.let { return it }

        val actualStoreId = when (request.storeId) {
            "2/F" -> "2/F"
            "4/F" -> "4/F"
            else -> request.storeId
        }
        println("    DEBUG: assignByLaneForWorkbench storeId=$actualStoreId date=${request.requiredDate} lane=${request.truckLanceCode} dep=${request.truckDepartureTime} seq=${request.loadingSequence}")

        val params = mutableMapOf<String, Any>(
            "storeId" to actualStoreId,
            "lance" to request.truckLanceCode.trim(),
        )
        val sql = StringBuilder(
            """
            SELECT dop.id
            FROM fpsmsdb.delivery_order_pick_order dop
            WHERE dop.deleted = 0
              AND dop.storeId = :storeId
              AND dop.truckLanceCode = :lance
              AND dop.ticketStatus IN ('pending', 'released')
              AND dop.handledBy IS NULL
              AND EXISTS (
                SELECT 1 FROM fpsmsdb.pick_order po
                WHERE po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0 AND po.assignTo IS NULL
              )
            """.trimIndent()
        )
        if (request.requiredDate != null) {
            sql.append(" AND dop.requiredDeliveryDate = :reqDate ")
            params["reqDate"] = request.requiredDate
        }
        val depSqlTime = parseDepartureTimeToSql(request.truckDepartureTime)
        if (depSqlTime != null) {
            sql.append(" AND dop.truckDepartureTime = :depTime ")
            params["depTime"] = depSqlTime
        }
        if (request.loadingSequence != null) {
            sql.append(" AND dop.loadingSequence = :loadingSequence ")
            params["loadingSequence"] = request.loadingSequence
        }
        if (isisExtraReleaseType(request.releaseType)) {
            sql.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ")
        }
        // Fetch a batch of candidates and try atomic-assign sequentially.
        // This avoids forcing the frontend to refresh when a single picked candidate is concurrently assigned.
        val candidateLimit = 50
        val maxRounds = 3

        val shouldOrderBySequence = actualStoreId == "2/F" && request.loadingSequence == null
        if (shouldOrderBySequence) {
            sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.loadingSequence ASC, dop.id ASC LIMIT $candidateLimit ")
        } else {
            sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.id ASC LIMIT $candidateLimit ")
        }

        fun extractIds(rows: List<Map<String, Any?>>): List<Long> {
            if (rows.isEmpty()) return emptyList()
            return rows.mapNotNull { r ->
                val idKey = r.keys.find { it.equals("id", true) } ?: return@mapNotNull null
                (r[idKey] as? Number)?.toLong()
            }
        }

        var round = 0
        while (round < maxRounds) {
            round++
            val candidates = try {
                jdbcDao.queryForList(sql.toString(), params)
            } catch (e: Exception) {
                println("❌ assignByLaneForWorkbench query: ${e.message}")
                emptyList()
            }
            val ids = extractIds(candidates)
            println("    DEBUG: assignByLaneForWorkbench round=$round candidates=${ids.size}")
            if (ids.isEmpty()) break

            for (dopoId in ids) {
                val res = applyDopoAssignment(user, request.userId, dopoId, mode = AssignMode.ATOMIC)
                val code = res.code?.trim()?.uppercase()
                when (code) {
                    "SUCCESS" -> return res
                    "CONFLICT" -> {
                        // Someone else took this ticket; try the next candidate.
                        continue
                    }
                    else -> {
                        // Unexpected error (e.g. UPDATE_FAILED in other modes). Return as-is.
                        return res
                    }
                }
            }
            // All candidates were concurrently assigned; loop to re-query a fresh batch.
        }

        return MessageResponse(
            id = null, code = "NO_ORDERS", name = null, type = null,
            message = "No available workbench ticket for this lane.", errorPosition = null, entity = null
        )
    }

    /** Legacy lane assign (V1): old FG-style (no atomic conflict guard). */
    @Transactional
    open fun assignByLaneForWorkbenchV1(request: AssignByLaneRequest): MessageResponse {
        val user = userRepository.findById(request.userId).orElse(null)
            ?: return userNotFound()

        userBusyResponse(request.userId)?.let { return it }

        val actualStoreId = when (request.storeId) {
            "2/F" -> "2/F"
            "4/F" -> "4/F"
            else -> request.storeId
        }
        println("    DEBUG: assignByLaneForWorkbenchV1 storeId=$actualStoreId date=${request.requiredDate} lane=${request.truckLanceCode} dep=${request.truckDepartureTime} seq=${request.loadingSequence}")

        val params = mutableMapOf<String, Any>(
            "storeId" to actualStoreId,
            "lance" to request.truckLanceCode.trim(),
        )
        val sql = StringBuilder(
            """
            SELECT dop.id
            FROM fpsmsdb.delivery_order_pick_order dop
            WHERE dop.deleted = 0
              AND dop.storeId = :storeId
              AND dop.truckLanceCode = :lance
              AND dop.ticketStatus IN ('pending', 'released')
              AND EXISTS (
                SELECT 1 FROM fpsmsdb.pick_order po
                WHERE po.deliveryOrderPickOrderId = dop.id AND po.deleted = 0 AND po.assignTo IS NULL
              )
            """.trimIndent()
        )
        if (request.requiredDate != null) {
            sql.append(" AND dop.requiredDeliveryDate = :reqDate ")
            params["reqDate"] = request.requiredDate
        }
        val depSqlTime = parseDepartureTimeToSql(request.truckDepartureTime)
        if (depSqlTime != null) {
            sql.append(" AND dop.truckDepartureTime = :depTime ")
            params["depTime"] = depSqlTime
        }
        if (request.loadingSequence != null) {
            sql.append(" AND dop.loadingSequence = :loadingSequence ")
            params["loadingSequence"] = request.loadingSequence
        }
        if (isisExtraReleaseType(request.releaseType)) {
            sql.append(" AND LOWER(COALESCE(dop.releaseType, '')) = 'isExtra' ")
        }
        val shouldOrderBySequenceV1 = actualStoreId == "2/F" && request.loadingSequence == null
        if (shouldOrderBySequenceV1) {
            sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.loadingSequence ASC, dop.id ASC LIMIT 1 ")
        } else {
            sql.append(" ORDER BY dop.requiredDeliveryDate ASC, dop.truckDepartureTime ASC, dop.id ASC LIMIT 1 ")
        }

        val candidates = try {
            jdbcDao.queryForList(sql.toString(), params)
        } catch (e: Exception) {
            println("❌ assignByLaneForWorkbenchV1 query: ${e.message}")
            emptyList()
        }
        println("    DEBUG: assignByLaneForWorkbenchV1 found ${candidates.size} candidate delivery_order_pick_order row(s)")
        if (candidates.isEmpty()) {
            return MessageResponse(
                id = null, code = "NO_ORDERS", name = null, type = null,
                message = "No available workbench ticket for this lane.", errorPosition = null, entity = null
            )
        }
        val idKey = candidates.first().keys.find { it.equals("id", true) } ?: return MessageResponse(
            id = null, code = "NO_ORDERS", name = null, type = null,
            message = "No candidate id", errorPosition = null, entity = null
        )
        val dopoId = (candidates.first()[idKey] as? Number)?.toLong()
            ?: return MessageResponse(
                id = null, code = "NO_ORDERS", name = null, type = null,
                message = "Invalid candidate id", errorPosition = null, entity = null
            )

        return applyDopoAssignment(user, request.userId, dopoId, mode = AssignMode.LEGACY_V1)
    }

    private fun userNotFound() = MessageResponse(
        id = null, code = "USER_NOT_FOUND", name = null, type = null,
        message = "User not found", errorPosition = null, entity = null
    )

    private fun userBusyResponse(userId: Long): MessageResponse? {
        val user = userRepository.findById(userId).orElse(null) ?: return null
        val busy = pickOrderRepository.findAllByAssignToIdAndStatusIn(
            userId,
            listOf(PickOrderStatus.PENDING, PickOrderStatus.RELEASED)
        ).filter { it.deliveryOrder != null }
        return if (busy.isNotEmpty()) {
            MessageResponse(
                id = null, code = "USER_BUSY", name = null, type = null,
                message = "User already has pick order(s) in progress.",
                errorPosition = null,
                entity = mapOf("count" to busy.size)
            )
        } else null
    }

    private fun isisExtraReleaseType(releaseType: String?): Boolean {
        val n = releaseType?.trim()?.lowercase().orEmpty()
        return n == "isExtra"
    }

    private fun parseDepartureTimeToSql(raw: String?): Time? {
        if (raw.isNullOrBlank()) return null
        val s = raw.trim()
        val lt = runCatching { LocalTime.parse(s) }.getOrNull()
            ?: runCatching { LocalTime.parse("$s:00") }.getOrNull()
            ?: return null
        return Time.valueOf(lt)
    }

    private enum class AssignMode { ATOMIC, LEGACY_V1 }

    private fun applyDopoAssignment(user: User, userId: Long, dopoId: Long, mode: AssignMode): MessageResponse {
        val now = LocalDateTime.now()
        val (sql, failCode, failMsg) = when (mode) {
            AssignMode.ATOMIC -> Triple(
                """
                UPDATE fpsmsdb.delivery_order_pick_order
                SET handledBy = :handledBy,
                    handlerName = :handlerName,
                    ticketStatus = 'released',
                    ticketReleaseTime = :trt,
                    modified = :modified,
                    modifiedBy = :modifiedBy
                WHERE id = :id AND deleted = 0
                  AND ticketStatus IN ('pending','released')
                  AND handledBy IS NULL
                """.trimIndent(),
                "CONFLICT",
                "Ticket already assigned by another user; please refresh and retry",
            )
            AssignMode.LEGACY_V1 -> Triple(
                """
                UPDATE fpsmsdb.delivery_order_pick_order
                SET handledBy = :handledBy,
                    handlerName = :handlerName,
                    ticketStatus = 'released',
                    ticketReleaseTime = :trt,
                    modified = :modified,
                    modifiedBy = :modifiedBy
                WHERE id = :id AND deleted = 0
                  AND ticketStatus IN ('pending','released')
                """.trimIndent(),
                "UPDATE_FAILED",
                "Could not update delivery_order_pick_order",
            )
        }
    
        val updated = jdbcDao.executeUpdate(
            sql,
            mapOf(
                "handledBy" to userId,
                "handlerName" to (user.name ?: ""),
                "trt" to now,
                "modified" to now,
                "modifiedBy" to "workbench",
                "id" to dopoId,
            )
        )
        if (updated <= 0) {
            return MessageResponse(
                id = null, code = failCode, name = null, type = null,
                message = failMsg, errorPosition = null, entity = null
            )
        }
    
        // 1) 取该 ticket 下所有 pick_order id（给后续 SPL/SOL 使用）
        val poIds = pickOrderRepository.findIdsByDeliveryOrderPickOrderId(dopoId)
    
        // 2) 一次性 bulk assign + released（取代 N 次 findById/save）
        if (poIds.isNotEmpty()) {
            pickOrderRepository.bulkAssignWorkbenchTicket(
                dopoId = dopoId,
                userId = userId,
                status = PickOrderStatus.RELEASED.value, // 若 enum 无 value，就传 "released"
                modified = now,
                modifiedBy = "workbench",
            )
        }
    
        // 下方逻辑先保持不变（你后续再做第二阶段批次化）
        val storeIdRow = jdbcDao.queryForList(
            "SELECT storeId FROM fpsmsdb.delivery_order_pick_order WHERE id = :id AND deleted = 0",
            mapOf("id" to dopoId),
        ).firstOrNull()
        val storeIdKey = storeIdRow?.keys?.find { it.equals("storeId", true) }
        val storeId = storeIdKey?.let { storeIdRow[it]?.toString() }?.trim()?.takeIf { it.isNotEmpty() }
    
        for (poId in poIds) {
            suggestedPickLotWorkbenchService.primeNextSingleLotSuggestionsForPickOrder(
                pickOrderId = poId,
                storeId = storeId,
                excludeWarehouseCodes = null,
            )
            stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(poId, userId)
        }
    
        val ticketNoRow = jdbcDao.queryForList(
            "SELECT ticketNo FROM fpsmsdb.delivery_order_pick_order WHERE id = :id AND deleted = 0",
            mapOf("id" to dopoId)
        ).firstOrNull()
        val ticketNoKey = ticketNoRow?.keys?.find { it.equals("ticketNo", true) }
        val ticketNo = ticketNoKey?.let { ticketNoRow[it]?.toString() }
    
        return MessageResponse(
            id = dopoId,
            code = "SUCCESS",
            name = null,
            type = null,
            message = "Assigned workbench ticket from lane",
            errorPosition = null,
            entity = mapOf(
                "deliveryOrderPickOrderId" to dopoId,
                "ticketNo" to ticketNo,
                "numberOfPickOrders" to poIds.size,
                "pickOrderIds" to poIds
            )
        )
    }
}
