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

import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoRequest
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import org.springframework.stereotype.Service
import java.time.Instant
import java.util.UUID
import java.sql.SQLException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.min
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoResult
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrder
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLine
import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository
import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.DayOfWeek
import com.ffii.fpsms.modules.deliveryOrder.web.models.AssignByDoPickOrderIdRequest
import com.ffii.fpsms.modules.user.entity.UserRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository
import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import jakarta.persistence.OptimisticLockException
import org.hibernate.StaleObjectStateException
import org.springframework.orm.ObjectOptimisticLockingFailureException

/**
 * Batch DO release: retry [RELEASE_RETRY_MAX] times when Hibernate optimistic locking fails
 * on shared [com.ffii.fpsms.modules.stock.entity.InventoryLotLine] rows (concurrent suggest/hold).
 *
 * **How to reproduce contention (manual / test ideas):**
 * - Run two batch releases in parallel (two browsers or two API clients) that share hot lot lines; or
 * - Release truck B then immediately truck C with DOs that suggest the same `inventory_lot_line`; or
 * - Stress: many DOs in one batch all touching the same few lot lines.
 *
 * **Multiple browser tabs:** each tab starts a separate async job. Without a gate, up to [pool] threads
 * run batch releases in parallel and hammer the same `inventory_lot_line` rows → optimistic locks + deadlocks.
 * [batchReleaseConcurrencyGate] serializes batch jobs (per JVM) so trucks are released one job at a time.
 */
private const val RELEASE_RETRY_MAX = 3

private fun isOptimisticLockFailure(t: Throwable?): Boolean {
    var c: Throwable? = t
    while (c != null) {
        when (c) {
            is StaleObjectStateException -> return true
            is OptimisticLockException -> return true
            is ObjectOptimisticLockingFailureException -> return true
        }
        if (c.message?.contains("Row was updated or deleted by another transaction", ignoreCase = true) == true) {
            return true
        }
        c = c.cause
    }
    return false
}

/** MySQL deadlock / SQLSTATE 40001 — safe to retry like optimistic lock. */
private fun isDeadlockFailure(t: Throwable?): Boolean {
    var c: Throwable? = t
    while (c != null) {
        if (c.message?.contains("Deadlock", ignoreCase = true) == true) return true
        if (c.message?.contains("try restarting transaction", ignoreCase = true) == true) return true
        (c as? SQLException)?.let { sql ->
            if (sql.sqlState == "40001") return true
            if (sql.errorCode == 1213) return true // ER_LOCK_DEADLOCK
        }
        c = c.cause
    }
    return false
}

private fun isRetriableConcurrencyFailure(t: Throwable?): Boolean =
    isOptimisticLockFailure(t) || isDeadlockFailure(t)

data class BatchReleaseJobStatus(
    val jobId: String,
    val total: Int,
    val success: AtomicInteger = AtomicInteger(0),
    val failed: MutableList<Pair<Long, String>> = mutableListOf(),
    @Volatile var running: Boolean = true,
    val startedAt: Long = Instant.now().toEpochMilli(),
    @Volatile var finishedAt: Long? = null
)

@Service
class DoReleaseCoordinatorService(
    private val deliveryOrderService: DeliveryOrderService,
    private val jdbcDao: JdbcDao,
    private val doPickOrderLineRepository: DoPickOrderLineRepository,
    private val deliveryOrderRepository: DeliveryOrderRepository,
    private val doPickOrderRepository: DoPickOrderRepository,
    private val pickExecutionIssueService: PickExecutionIssueService,
    private val userRepository: UserRepository,
    private val pickOrderRepository: PickOrderRepository,
    private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
    private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService,
) {
    private val poolSize = Runtime.getRuntime().availableProcessors()
    private val executor = Executors.newFixedThreadPool(min(poolSize, 4))
    /** Only one batch-release job runs at a time (multiple tabs queue here). */
    private val batchReleaseConcurrencyGate = Semaphore(1, true)
    private val jobs = ConcurrentHashMap<String, BatchReleaseJobStatus>()
    private fun getDayOfWeekAbbr(date: LocalDate?): String? {
        if (date == null) return null
        return when (date.dayOfWeek) {
            DayOfWeek.MONDAY -> "Mon"
            DayOfWeek.TUESDAY -> "Tue"
            DayOfWeek.WEDNESDAY -> "Wed"
            DayOfWeek.THURSDAY -> "Thu"
            DayOfWeek.FRIDAY -> "Fri"
            DayOfWeek.SATURDAY -> "Sat"
            DayOfWeek.SUNDAY -> "Sun"
        }
    }

    // 辅助方法：在 SQL 中获取星期几的缩写（MySQL）
    private fun getDayOfWeekAbbrSql(dateColumn: String): String {
        return """
            CASE DAYNAME($dateColumn)
                WHEN 'Monday' THEN 'Mon'
                WHEN 'Tuesday' THEN 'Tue'
                WHEN 'Wednesday' THEN 'Wed'
                WHEN 'Thursday' THEN 'Thu'
                WHEN 'Friday' THEN 'Fri'
                WHEN 'Saturday' THEN 'Sat'
                WHEN 'Sunday' THEN 'Sun'
                ELSE ''
            END
        """.trimIndent()
    }
    private fun updateBatchTicketNumbers() {
        try {
            val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate")
            val pfCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("s.code")
            val updateSql = """
                UPDATE fpsmsdb.do_pick_order dpo
                INNER JOIN (
                    WITH PreferredFloor AS (
                        SELECT 
                            do.id AS deliveryOrderId,
                            ${pfCases.floorStringCase} AS preferred_floor,
                            ${pfCases.storeIdNumericCase} AS preferred_store_id
                        FROM fpsmsdb.delivery_order do
                        LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0
                        WHERE do.deleted = 0
                    ),
                    TruckSelection AS (
                        SELECT 
                            do.id AS delivery_order_id,
                            do.estimatedArrivalDate,
                            pf.preferred_floor,
                            $dayOfWeekSql AS day_of_week_abbr,
                            CASE 
                                WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1 
                                     AND pf.preferred_store_id = 4 
                                     AND do.estimatedArrivalDate IS NOT NULL THEN
                                    COALESCE(
                                        (SELECT t.DepartureTime FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0 
                                           AND t.Store_id = pf.preferred_store_id
                                           AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        (SELECT t.DepartureTime FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0 
                                           AND t.Store_id = pf.preferred_store_id
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        (SELECT t.DepartureTime FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        '23:59:59'
                                    )
                                WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN
                                    COALESCE(
                                        (SELECT t.DepartureTime FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0 
                                           AND t.Store_id = pf.preferred_store_id
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        (SELECT t.DepartureTime FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        '23:59:59'
                                    )
                                ELSE 
                                    COALESCE(
                                        (SELECT t.DepartureTime FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        '23:59:59'
                                    )
                            END AS selected_departure_time,
                            CASE 
                                WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1 
                                     AND pf.preferred_store_id = 4 
                                     AND do.estimatedArrivalDate IS NOT NULL THEN
                                    COALESCE(
                                        (SELECT t.TruckLanceCode FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0 
                                           AND t.Store_id = pf.preferred_store_id
                                           AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        (SELECT t.TruckLanceCode FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0 
                                           AND t.Store_id = pf.preferred_store_id
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        (SELECT t.TruckLanceCode FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        'ZZ'
                                    )
                                WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN
                                    COALESCE(
                                        (SELECT t.TruckLanceCode FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0 
                                           AND t.Store_id = pf.preferred_store_id
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        (SELECT t.TruckLanceCode FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        'ZZ'
                                    )
                                ELSE 
                                    COALESCE(
                                        (SELECT t.TruckLanceCode FROM fpsmsdb.truck t 
                                         WHERE t.shopId = do.shopId AND t.deleted = 0
                                         ORDER BY t.DepartureTime ASC LIMIT 1),
                                        'ZZ'
                                    )
                            END AS selected_truck_lance,
                            COALESCE(
                                (SELECT t.LoadingSequence FROM fpsmsdb.truck t 
                                 WHERE t.shopId = do.shopId AND t.deleted = 0 
                                   AND t.Store_id = pf.preferred_store_id
                                   AND (pf.preferred_store_id != 4 OR do.estimatedArrivalDate IS NULL OR 
                                        t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%'))
                                 ORDER BY t.DepartureTime ASC LIMIT 1),
                                (SELECT t.LoadingSequence FROM fpsmsdb.truck t 
                                 WHERE t.shopId = do.shopId AND t.deleted = 0 
                                   AND t.Store_id = pf.preferred_store_id
                                 ORDER BY t.DepartureTime ASC LIMIT 1),
                                (SELECT t.LoadingSequence FROM fpsmsdb.truck t 
                                 WHERE t.shopId = do.shopId AND t.deleted = 0
                                 ORDER BY t.DepartureTime ASC LIMIT 1),
                                999
                            ) AS loading_sequence
                        FROM fpsmsdb.delivery_order do
                        LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id
                        WHERE do.id IN (
                            SELECT DISTINCT do_order_id 
                            FROM fpsmsdb.do_pick_order 
                            WHERE ticket_no LIKE 'TEMP-%' 
                            AND deleted = 0
                        )
                        AND do.deleted = 0
                    )
                    SELECT 
                        dpo2.id,
                        CONCAT('TI-B-',
                    DATE_FORMAT(dpo2.RequiredDeliveryDate, '%Y%m%d'),
                    '-',
                    IF(dpo2.store_id IS NULL OR TRIM(dpo2.store_id) = '',
                        'XF',
                        REPLACE(TRIM(dpo2.store_id), '/', '')),
                    '-',
                    LPAD(
                        ROW_NUMBER() OVER (
    PARTITION BY DATE(dpo2.RequiredDeliveryDate),
                 IF(dpo2.store_id IS NULL OR TRIM(dpo2.store_id) = '', 'XF', COALESCE(ts.preferred_floor, '2F'))
    ORDER BY COALESCE(ts.selected_departure_time, '23:59:59'),
             COALESCE(ts.selected_truck_lance, 'ZZ'),
             COALESCE(ts.loading_sequence, 999),
             dpo2.do_order_id
), 3, '0')
                    ) AS new_ticket_no
                    FROM fpsmsdb.do_pick_order dpo2
                    LEFT JOIN TruckSelection ts ON ts.delivery_order_id = dpo2.do_order_id
                    WHERE dpo2.ticket_no LIKE 'TEMP-%'
                      AND dpo2.deleted = 0
                ) AS ticket_mapping ON dpo.id = ticket_mapping.id
                SET dpo.ticket_no = ticket_mapping.new_ticket_no
            """.trimIndent()
            
            val rowsUpdated = jdbcDao.executeUpdate(updateSql, emptyMap<String, Any>())
            println(" Updated $rowsUpdated ticket numbers")
        } catch (e: Exception) {
            println("❌ Error updating ticket numbers: ${e.message}")
            e.printStackTrace()
        }
    }
    private fun getOrderedDeliveryOrderIds(ids: List<Long>): List<Long> {
        try {
            println("    DEBUG: Getting ordered IDs for ${ids.size} orders")
            println("    DEBUG: First 5 IDs: ${ids.take(5)}")
            val dayOfWeekSql = getDayOfWeekAbbrSql("do.estimatedArrivalDate")
            val pfCases = doFloorSupplierSettingsService.sqlPreferredFloorCases("s.code")
            val sql = """
                WITH PreferredFloor AS (
                    SELECT 
                        do.id AS deliveryOrderId,
                        ${pfCases.floorStringCase} AS preferred_floor,
                        ${pfCases.storeIdNumericCase} AS preferred_store_id
                    FROM fpsmsdb.delivery_order do
                    LEFT JOIN fpsmsdb.shop s ON s.id = do.supplierId AND s.deleted = 0
                    WHERE do.id IN (${ids.joinToString(",")})
                      AND do.deleted = 0
                ),
                TruckSelection AS (
                    SELECT 
                        do.id AS delivery_order_id,
                        do.shopId,
                        do.estimatedArrivalDate,
                        pf.preferred_floor,
                        pf.preferred_store_id,
                        $dayOfWeekSql AS day_of_week_abbr,
                        CASE 
                            WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1 
                                 AND pf.preferred_store_id = 4 
                                 AND do.estimatedArrivalDate IS NOT NULL THEN
                                COALESCE(
                                    (SELECT t.DepartureTime 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId 
                                       AND t.deleted = 0 
                                       AND t.Store_id = pf.preferred_store_id
                                       AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    (SELECT t.DepartureTime 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0 
                                       AND t.Store_id = pf.preferred_store_id
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    (SELECT t.DepartureTime 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    '23:59:59'
                                )
                            WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN
                                COALESCE(
                                    (SELECT t.DepartureTime 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0 
                                       AND t.Store_id = pf.preferred_store_id
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    (SELECT t.DepartureTime 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    '23:59:59'
                                )
                            ELSE 
                                COALESCE(
                                    (SELECT t.DepartureTime 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    '23:59:59'
                                )
                        END AS selected_departure_time,
                        CASE 
                            WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0 AND t.Store_id = pf.preferred_store_id) > 1 
                                 AND pf.preferred_store_id = 4 
                                 AND do.estimatedArrivalDate IS NOT NULL THEN
                                COALESCE(
                                    (SELECT t.TruckLanceCode 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId 
                                       AND t.deleted = 0 
                                       AND t.Store_id = pf.preferred_store_id
                                       AND t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%')
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    (SELECT t.TruckLanceCode 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0 
                                       AND t.Store_id = pf.preferred_store_id
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    (SELECT t.TruckLanceCode 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    'ZZ'
                                )
                            WHEN (SELECT COUNT(*) FROM fpsmsdb.truck t WHERE t.shopId = do.shopId AND t.deleted = 0) > 1 THEN
                                COALESCE(
                                    (SELECT t.TruckLanceCode 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0 
                                       AND t.Store_id = pf.preferred_store_id
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    (SELECT t.TruckLanceCode 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    'ZZ'
                                )
                            ELSE 
                                COALESCE(
                                    (SELECT t.TruckLanceCode 
                                     FROM fpsmsdb.truck t 
                                     WHERE t.shopId = do.shopId AND t.deleted = 0
                                     ORDER BY t.DepartureTime ASC
                                     LIMIT 1),
                                    'ZZ'
                                )
                        END AS selected_truck_lance,
                        COALESCE(
                            (SELECT t.LoadingSequence 
                             FROM fpsmsdb.truck t 
                             WHERE t.shopId = do.shopId AND t.deleted = 0 
                               AND t.Store_id = pf.preferred_store_id
                               AND (pf.preferred_store_id != 4 OR do.estimatedArrivalDate IS NULL OR 
                                    t.TruckLanceCode LIKE CONCAT('%', $dayOfWeekSql, '%'))
                             ORDER BY t.DepartureTime ASC LIMIT 1),
                            (SELECT t.LoadingSequence 
                             FROM fpsmsdb.truck t 
                             WHERE t.shopId = do.shopId AND t.deleted = 0 
                               AND t.Store_id = pf.preferred_store_id
                             ORDER BY t.DepartureTime ASC LIMIT 1),
                            (SELECT t.LoadingSequence 
                             FROM fpsmsdb.truck t 
                             WHERE t.shopId = do.shopId AND t.deleted = 0
                             ORDER BY t.DepartureTime ASC LIMIT 1),
                            999
                        ) AS loading_sequence
                    FROM fpsmsdb.delivery_order do
                    LEFT JOIN PreferredFloor pf ON pf.deliveryOrderId = do.id
                WHERE do.id IN (${ids.joinToString(",")})
                      AND do.deleted = 0
                )
                SELECT delivery_order_id AS id
                FROM TruckSelection
                ORDER BY 
                    DATE(estimatedArrivalDate),
                    preferred_floor,
                    selected_departure_time,
                    selected_truck_lance,
                    loading_sequence ASC,
                    delivery_order_id ASC
                
            """.trimIndent()
            
           // println("    DEBUG: SQL length: ${sql.length} characters")
           // println("    DEBUG: SQL first 500 chars: ${sql.take(500)}")
            
            val results = jdbcDao.queryForList(sql)
            println("    DEBUG: Results type: ${results.javaClass.name}")
            println("    DEBUG: Results size: ${results.size}")
            
            if (results.isNotEmpty()) {
                println("    DEBUG: First result keys: ${results[0].keys}")
                println("    DEBUG: First result: ${results[0]}")
            }
            
            val sortedIds = results.mapNotNull { row ->
                (row["id"] as? Number)?.toLong()
            }
            
            println("    DEBUG: Query returned ${sortedIds.size} sorted IDs")
            println("    DEBUG: First 10 sorted IDs: ${sortedIds.take(10)}")
            
            return if (sortedIds.isEmpty()) {
                println("⚠️ WARNING: No sorted IDs, using original order")
                ids
            } else {
                sortedIds
            }
        } catch (e: Exception) {
            println("❌ ERROR: ${e.message}")
            println("❌ ERROR Stack Trace:")
            e.printStackTrace()
            return ids
        }
    }
    fun startBatchReleaseAsync(ids: List<Long>, userId: Long): MessageResponse {
        if (ids.isEmpty()) {
            return MessageResponse(
                id = null, code = "NO_IDS", name = null, type = null,
                message = "No IDs provided", errorPosition = null, entity = null
            )
        }

        val jobId = UUID.randomUUID().toString()
        val status = BatchReleaseJobStatus(jobId = jobId, total = ids.size)
        jobs[jobId] = status

        executor.submit {
            batchReleaseConcurrencyGate.acquireUninterruptibly()
            try {
                try {
                println("Starting batch release for ${ids.size} orders (job $jobId)")
                val sortedIds = getOrderedDeliveryOrderIds(ids)
                println("    DEBUG: Got ${sortedIds.size} sorted orders")

                val releaseResults = mutableListOf<ReleaseDoResult>()

                // 第一步：发布所有 DO（创建 pick orders，但不创建 DoPickOrder）
              // 第一步：发布所有 DO（创建 pick orders，但不创建 DoPickOrder）
                sortedIds.forEach { id ->
                    try {
                        val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(id)
                        if (deliveryOrder?.status == DeliveryOrderStatus.COMPLETED ||
                            deliveryOrder?.status == DeliveryOrderStatus.RECEIVING
                        ) {
                            println("⏭️ DO $id is already ${deliveryOrder.status?.value}, skipping")
                            return@forEach
                        }
                        var result: ReleaseDoResult? = null
                        for (attempt in 1..RELEASE_RETRY_MAX) {
                            try {
                                result = deliveryOrderService.releaseDeliveryOrderWithoutTicket(
                                    ReleaseDoRequest(id = id, userId = userId)
                                )
                                break
                            } catch (e: Exception) {
                                if (isRetriableConcurrencyFailure(e) && attempt < RELEASE_RETRY_MAX) {
                                    val kind = when {
                                        isDeadlockFailure(e) -> "deadlock/DB lock"
                                        else -> "optimistic lock"
                                    }
                                    println(
                                        "⚠️ DO $id $kind (attempt $attempt/$RELEASE_RETRY_MAX), retrying after short backoff..."
                                    )
                                    try {
                                        Thread.sleep(50L * attempt)
                                    } catch (_: InterruptedException) {
                                        Thread.currentThread().interrupt()
                                        throw e
                                    }
                                } else {
                                    throw e
                                }
                            }
                        }
                        if (result != null) {
                            releaseResults.add(result)
                            status.success.incrementAndGet()
                            println("    DO $id -> Success")
                        } else {
                            throw IllegalStateException("DO $id release returned null after $RELEASE_RETRY_MAX attempts")
                        }
                    } catch (e: Exception) {
                        synchronized(status.failed) {
                            status.failed.add(id to (e.message ?: "Exception"))
                        }
                        println("❌ DO $id skipped: ${e.message}")

                        // Transient concurrency: avoid creating one issue per DO line (noise)
                        if (isRetriableConcurrencyFailure(e)) {
                            println("⚠️ DO $id: skipping batch-release issues for transient concurrency failure")
                            return@forEach
                        }

                        //  调用 PickExecutionIssueService 创建 issue 记录
                        try {
                            val issueCategory = when {
                                e.message?.contains("Unable to find") == true &&
                                        e.message?.contains("Warehouse") == true ->
                                    com.ffii.fpsms.modules.pickOrder.entity.IssueCategory.warehouse_issue

                                e.message?.contains("No matching truck") == true ->
                                    com.ffii.fpsms.modules.pickOrder.entity.IssueCategory.truck_issue

                                else ->
                                    com.ffii.fpsms.modules.pickOrder.entity.IssueCategory.match_issue
                            }

                            pickExecutionIssueService.createBatchReleaseIssue(
                                deliveryOrderId = id,
                                errorMessage = e.message ?: "Unknown error",
                                issueCategory = issueCategory
                            )
                        } catch (issueException: Exception) {
                            println("⚠️ Failed to create issue for DO $id: ${issueException.message}")
                        }
                    }
                }  //  forEach 循环在这里结束

                //  第二步：按日期、楼层、店铺分组（在 forEach 之后）
                val sortedResults = releaseResults.sortedWith(
                    compareBy(
                        { it.estimatedArrivalDate },
                        { it.preferredFloor },
                        { it.truckDepartureTime },
                        { it.truckLanceCode },
                        { it.loadingSequence },
                        { it.shopId }
                    )
                )

                //  然后按正确的顺序分组（保持排序后的顺序）
                val grouped = sortedResults.groupBy {
                    Triple(it.estimatedArrivalDate, it.preferredFloor, it.shopId)
                }

                println("    DEBUG: Grouped into ${grouped.size} DoPickOrders")

                // 第三步：为每组创建一个 DoPickOrder 和多条 DoPickOrderLine
                grouped.forEach { (key, group) ->
                    try {
                        createMergedDoPickOrder(group)
                        println("    DEBUG: Merged/created DoPickOrder for ${group.size} DO(s)")
                    } catch (e: Exception) {
                        println("❌ Error creating DoPickOrder: ${e.message}")
                        e.printStackTrace()
                    }
                }

                // 第四步：更新 ticket numbers
                if (status.success.get() > 0) {
                    println("🎫 Updating ticket numbers...")
                    updateBatchTicketNumbers()
                }

                println(" Batch completed: ${status.success.get()} success, ${status.failed.size} failed")

                } catch (e: Exception) {
                    println("❌ Batch release exception: ${e.javaClass.simpleName} - ${e.message}")
                    e.printStackTrace()
                } finally {
                    status.running = false
                    status.finishedAt = Instant.now().toEpochMilli()
                }
            } finally {
                batchReleaseConcurrencyGate.release()
            }
        }

        return MessageResponse(
            id = null, code = "STARTED", name = null, type = null,
            message = "Batch release started", errorPosition = null,
            entity = mapOf("jobId" to jobId, "total" to ids.size)
        )
    }

    private fun newMergedDoPickOrderEntity(first: ReleaseDoResult, storeId: String?): DoPickOrder {
        return DoPickOrder(
            storeId = storeId,
            ticketNo = "TEMP-${System.currentTimeMillis()}",
            ticketStatus = DoPickOrderStatus.pending,
            truckId = first.truckId,
            pickOrderId = null,
            doOrderId = null,
            ticketReleaseTime = null,
            shopId = first.shopId,
            handlerName = null,
            handledBy = null,
            ticketCompleteDateTime = null,
            truckDepartureTime = first.truckDepartureTime,
            truckLanceCode = first.truckLanceCode,
            shopCode = first.shopCode,
            shopName = first.shopName,
            requiredDeliveryDate = first.estimatedArrivalDate,
            createdBy = null,
            modifiedBy = null,
            pickOrderCode = null,
            deliveryOrderCode = null,
            loadingSequence = first.loadingSequence,
            releaseType = "batch"
        )
    }

    /**
     * If a batch ticket already exists for the same shop, floor ([storeId]), delivery date, and is still
     * pending or released, add new DO lines onto it instead of creating another do_pick_order.
     */
    private fun createMergedDoPickOrder(results: List<ReleaseDoResult>) {
        val first = results.first()

        val storeId: String? = when {
            first.usedDefaultTruck != false -> null
            else -> when (first.preferredFloor) {
                "2F" -> "2/F"
                "4F" -> "4/F"
                else -> "2/F"
            }
        }

        val target: DoPickOrder =
            if (first.shopId != null && first.estimatedArrivalDate != null) {
                val candidates = doPickOrderRepository.findMergeableBatchDoPickOrders(
                    shopId = first.shopId!!,
                    storeId = storeId,
                    requiredDate = first.estimatedArrivalDate!!,
                    statuses = listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released),
                )
                val existing = candidates.firstOrNull { c ->
                    c.truckId == first.truckId &&
                        c.truckDepartureTime == first.truckDepartureTime &&
                        c.truckLanceCode == first.truckLanceCode
                } ?: candidates.minByOrNull { it.id ?: Long.MAX_VALUE }
                if (existing != null) {
                    println(
                        "    DEBUG: Merging batch into existing DoPickOrder id=${existing.id}, ticket=${existing.ticketNo} " +
                            "(shop=${first.shopId}, storeId=$storeId, date=${first.estimatedArrivalDate})"
                    )
                    existing
                } else {
                    doPickOrderRepository.save(newMergedDoPickOrderEntity(first, storeId))
                }
            } else {
                doPickOrderRepository.save(newMergedDoPickOrderEntity(first, storeId))
            }

        println("    DEBUG: Target DoPickOrder - ID: ${target.id}, Ticket: ${target.ticketNo}")

        results.forEach { result ->
            val existingLines = doPickOrderLineRepository.findByPickOrderIdAndDeletedFalse(result.pickOrderId)
            if (existingLines.isNotEmpty()) {
                println("⚠️ WARNING: pick_order ${result.pickOrderId} already in do_pick_order_line, skipping")
                return@forEach
            }

            val line = DoPickOrderLine().apply {
                doPickOrderId = target.id
                pickOrderId = result.pickOrderId
                doOrderId = result.deliveryOrderId
                pickOrderCode = result.pickOrderCode
                deliveryOrderCode = result.deliveryOrderCode
                status = "pending"
            }
            doPickOrderLineRepository.save(line)
            println("    DEBUG: Created DoPickOrderLine for pick order ${result.pickOrderId}")
        }

        val hasStockIssues = checkPickOrderHasStockIssues(target.id!!)
        if (hasStockIssues) {
            val doPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(target.id!!)
            doPickOrderLines.forEach { line ->
                line.status = "issue"
            }
            doPickOrderLineRepository.saveAll(doPickOrderLines)
            println("    DEBUG: Updated ${doPickOrderLines.size} DoPickOrderLine records to 'issue' status")
        }

        println("    DEBUG: Created ${results.size} DoPickOrderLine records")
    }
    private fun checkPickOrderHasStockIssues(doPickOrderId: Long): Boolean {
        try {
            // 1. 获取 do_pick_order 的所有 pick orders 数量
            val totalPickOrdersSql = """
                SELECT COUNT(*) as total_pick_orders
                FROM fpsmsdb.do_pick_order_line dpol
                WHERE dpol.do_pick_order_id = :doPickOrderId 
                AND dpol.deleted = 0
            """.trimIndent()
            
            val totalPickOrdersResult = jdbcDao.queryForList(totalPickOrdersSql, mapOf("doPickOrderId" to doPickOrderId))
            val totalPickOrders = (totalPickOrdersResult.firstOrNull()?.get("total_pick_orders") as? Number)?.toInt() ?: 0
            
            if (totalPickOrders == 0) {
                return false  // 没有 pick orders，不算 issue
            }
            
            // 2. 获取有库存问题的 pick orders 数量（通过检查 pick_execution_issue 表）
            val issuePickOrdersSql = """
                SELECT COUNT(DISTINCT dpol.pick_order_id) as issue_pick_orders
                FROM fpsmsdb.do_pick_order_line dpol
                INNER JOIN fpsmsdb.pick_execution_issue pei ON pei.pick_order_id = dpol.pick_order_id
                WHERE dpol.do_pick_order_id = :doPickOrderId 
                AND pei.deleted = 0
                AND dpol.deleted = 0
            """.trimIndent()
            
            val issuePickOrdersResult = jdbcDao.queryForList(issuePickOrdersSql, mapOf("doPickOrderId" to doPickOrderId))
            val issuePickOrders = (issuePickOrdersResult.firstOrNull()?.get("issue_pick_orders") as? Number)?.toInt() ?: 0
            
            // 3. 只有当所有 pick orders 都有问题时才算 issue
            val hasAllPickOrdersIssues = (totalPickOrders > 0) && (issuePickOrders == totalPickOrders)
            
            println("    DEBUG: DoPickOrder $doPickOrderId - Total pick orders: $totalPickOrders, Issue pick orders: $issuePickOrders, All pick orders have issues: $hasAllPickOrdersIssues")
            
            return hasAllPickOrdersIssues
            
        } catch (e: Exception) {
            println("❌ Error checking stock issues for do pick order $doPickOrderId: ${e.message}")
            return false
        }
    }
    fun getBatchReleaseProgress(jobId: String): MessageResponse {
        val s = jobs[jobId] ?: return MessageResponse(
            id = null, code = "NOT_FOUND", name = null, type = null,
            message = "Job not found", errorPosition = null, entity = null
        )
        val finished = s.success.get() + s.failed.size
        val progress = (finished.toDouble() / s.total).coerceIn(0.0, 1.0)

        return MessageResponse(
            id = null, code = if (s.running) "RUNNING" else "FINISHED", name = null, type = null,
            message = null, errorPosition = null, entity = mapOf(
                "jobId" to s.jobId,
                "total" to s.total,
                "finished" to finished,
                "success" to s.success.get(),
                "failedCount" to s.failed.size,
                "failed" to s.failed.take(50),
                "running" to s.running,
                "progress" to progress,
                "startedAt" to s.startedAt,
                "finishedAt" to s.finishedAt
            )
        )
    }

    fun startBatchReleaseAsyncSingle(doId: Long, userId: Long): MessageResponse {
        val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(doId)
            ?: return MessageResponse(
                id = null, code = "NOT_FOUND", name = null, type = null,
                message = "Delivery Order not found", errorPosition = null, entity = null
            )
    
        executor.submit {
            try {
                println("📦 Starting single release for DO $doId")
                
                // 调用 releaseDeliveryOrderWithoutTicket 创建 pick order
                val result = deliveryOrderService.releaseDeliveryOrderWithoutTicket(
                    ReleaseDoRequest(id = doId, userId = userId)
                )
                
                // 确定 storeId
                val storeId: String? = when {
                    result.usedDefaultTruck!=false -> null
                    else -> when (result.preferredFloor) {
                        "2F" -> "2/F"
                        "4F" -> "4/F"
                        else -> "2/F"
                    }
                }
                
                // 查找是否已有相同 shop、storeId 和 releaseType='single' 的 do_pick_order
                val existingDoPickOrder = doPickOrderRepository
                    .findByShopIdAndStoreIdAndReleaseTypeAndTicketStatusAndDeletedFalse(
                        result.shopId,
                        storeId,
                        "single",
                        DoPickOrderStatus.pending
                    )
                    .firstOrNull { 
                        it.requiredDeliveryDate == result.estimatedArrivalDate &&
                        it.truckDepartureTime == result.truckDepartureTime &&
                        it.truckLanceCode == result.truckLanceCode
                    }
                
                if (existingDoPickOrder != null) {
                    // 如果已存在，创建 do_pick_order_line 关联到已存在的 do_pick_order
                    val existingLine = doPickOrderLineRepository
                        .findByPickOrderIdAndDeletedFalse(result.pickOrderId)
                        .firstOrNull { it.doPickOrderId == existingDoPickOrder.id }
                    
                    if (existingLine == null) {
                        val line = DoPickOrderLine().apply {
                            doPickOrderId = existingDoPickOrder.id
                            pickOrderId = result.pickOrderId
                            doOrderId = result.deliveryOrderId
                            pickOrderCode = result.pickOrderCode
                            deliveryOrderCode = result.deliveryOrderCode
                            status = "pending"
                        }
                        doPickOrderLineRepository.save(line)
                        println("    DEBUG: Created DoPickOrderLine for existing DoPickOrder ${existingDoPickOrder.id}")
                    }
                // 替换第 748-764 行
            } else {
                // 如果不存在，创建新的 do_pick_order
                val doPickOrder = DoPickOrder(
                    storeId = storeId,
                    ticketNo = "TEMP-${System.currentTimeMillis()}",
                    ticketStatus = DoPickOrderStatus.pending,
                    truckId = result.truckId,
                    pickOrderId = null,
                    doOrderId = null,
                    ticketReleaseTime = null,
                    shopId = result.shopId,
                    handlerName = null,
                    handledBy = null,
                    ticketCompleteDateTime = null,
                    truckDepartureTime = result.truckDepartureTime,
                    truckLanceCode = result.truckLanceCode,
                    shopCode = result.shopCode,
                    shopName = result.shopName,
                    requiredDeliveryDate = result.estimatedArrivalDate,
                    createdBy = null,
                    modifiedBy = null,
                    pickOrderCode = null,
                    deliveryOrderCode = null,
                    loadingSequence = result.loadingSequence,
                    releaseType = "single"
                )
                val saved = doPickOrderRepository.save(doPickOrder)
                    
                    // 创建 do_pick_order_line
                    val line = DoPickOrderLine().apply {
                        doPickOrderId = saved.id
                        pickOrderId = result.pickOrderId
                        doOrderId = result.deliveryOrderId
                        pickOrderCode = result.pickOrderCode
                        deliveryOrderCode = result.deliveryOrderCode
                        status = "pending"
                    }
                    doPickOrderLineRepository.save(line)
                    println("    DEBUG: Created new DoPickOrder with releaseType=single")
                }
                
                // 更新 ticket numbers（只更新 single 类型的）
                updateSingleTicketNumbers()
                
            } catch (e: Exception) {
                println("❌ Single release exception: ${e.message}")
                e.printStackTrace()
            }
        }
        
        return MessageResponse(
            id = null, code = "STARTED", name = null, type = null,
            message = "Single release started", errorPosition = null,
            entity = mapOf("doId" to doId)
        )
    }
    
    private fun updateSingleTicketNumbers() {
        try {
            // 1. 查找所有 TEMP- 开头的 single release type 订单
            val tempTickets = doPickOrderRepository.findAll()
                .filter { 
                    it.ticketNo?.startsWith("TEMP-") == true && 
                    it.releaseType == "single" && 
                    !it.deleted 
                }
            
            if (tempTickets.isEmpty()) {
                println("    No single release type tickets to update")
                return
            }
            
            println("    DEBUG: Found ${tempTickets.size} single release type tickets to update")
            
            // 2. 按日期和 storeId 分组
            val grouped = tempTickets.groupBy { ticket ->
                val date = ticket.requiredDeliveryDate
                val storeId = ticket.storeId?.replace("/", "") ?: "2F"
                Pair(date, storeId)
            }
            
            // 3. 为每个组生成 ticket numbers
            grouped.forEach { (dateStorePair, tickets) ->
                val (date, storeId) = dateStorePair
                
                if (date == null) {
                    println("⚠️ WARNING: Skipping tickets with null requiredDeliveryDate")
                    return@forEach
                }
                
                // 格式化日期为 YYYYMMDD
                val datePrefix = date.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
                
                // 4. 查询该组已存在的 ticket numbers（包括已生成的，不只是 TEMP-）
                val existingTicketPattern = "TI-S-$datePrefix-$storeId-"
                val existingTickets = doPickOrderRepository.findAll()
                    .filter {
                        it.ticketNo?.startsWith(existingTicketPattern) == true &&
                        it.releaseType == "single" &&
                        !it.deleted &&
                        it.requiredDeliveryDate == date &&
                        it.storeId?.replace("/", "") == storeId
                    }
                    .mapNotNull { it.ticketNo }
                
                // 提取已存在的序号
                val existingNumbers = existingTickets.mapNotNull { ticketNo ->
                    val parts = ticketNo.split("-")
                    if (parts.size >= 4) {
                        parts.lastOrNull()?.toIntOrNull()
                    } else null
                }.toMutableSet()
                
                println("    DEBUG: Group ($date, $storeId) - Existing ticket numbers: $existingTickets, Existing numbers: $existingNumbers")
                
                // 5. 在组内按 truckDepartureTime, truckLanceCode, loadingSequence, doOrderId 排序
                val sortedTickets = tickets.sortedWith(
                    compareBy<DoPickOrder>(
                        { it.truckDepartureTime ?: java.time.LocalTime.of(23, 59, 59) },
                        { it.truckLanceCode ?: "ZZ" },
                        { it.loadingSequence ?: 999 },
                        { it.doOrderId ?: 0L }
                    )
                )
                
                // 6. 为每个订单生成序号并更新 ticket_no（跳过已存在的序号）
                var nextNumber = 1
                sortedTickets.forEach { ticket ->
                    // 找到下一个可用的序号
                    while (existingNumbers.contains(nextNumber)) {
                        nextNumber++
                    }
                    
                    val sequenceNumber = nextNumber.toString().padStart(3, '0')
                    val newTicketNo = "TI-S-$datePrefix-$storeId-$sequenceNumber"
                    
                    ticket.ticketNo = newTicketNo
                    doPickOrderRepository.save(ticket)
                    
                    // 将新生成的序号添加到已存在集合中，避免同一批次内重复
                    existingNumbers.add(nextNumber)
                    nextNumber++
                    
                    println("    DEBUG: Updated ticket ${ticket.id} to $newTicketNo")
                }
            }
            
            println("✅ Updated ${tempTickets.size} single release type ticket numbers")
            
        } catch (e: Exception) {
            println("❌ Error updating single ticket numbers: ${e.message}")
            e.printStackTrace()
        }
    }

    fun assignByDoPickOrderId(request: AssignByDoPickOrderIdRequest): MessageResponse {
        val user = userRepository.findById(request.userId).orElse(null)
            ?: return MessageResponse(id = null, code = "USER_NOT_FOUND", name = null, type = null, message = null, errorPosition = null, entity = null)
        
        val doPickOrder = doPickOrderRepository.findById(request.doPickOrderId).orElse(null)
            ?: return MessageResponse(id = null, code = "NOT_FOUND", name = null, type = null, message = null, errorPosition = null, entity = null)
        
        // 檢查狀態是否可 assign
        if (doPickOrder.ticketStatus !in listOf(DoPickOrderStatus.pending, DoPickOrderStatus.released)) {
            return MessageResponse(id = null, code = "INVALID_STATUS", name = null, type = null, message = null, errorPosition = null, entity = null)
        }
        
        // 檢查是否有 non-issue lines（可重用 checkDoPickOrderHasNonIssueLines）
        if (!checkDoPickOrderHasNonIssueLines(doPickOrder.id!!)) {
            return MessageResponse(id = null, code = "NO_ORDERS", name = null, type = null, message = "All lines are issues", errorPosition = null, entity = null)
        }
        
        // 更新 do_pick_order
        doPickOrder.handledBy = request.userId
        doPickOrder.handlerName = user.name
        doPickOrder.ticketStatus = DoPickOrderStatus.released
        doPickOrder.ticketReleaseTime = LocalDateTime.now()
        doPickOrderRepository.save(doPickOrder)
        
        // 更新關聯的 pick orders
        val lines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrder.id!!)
        lines.forEach { line ->
            line.pickOrderId?.let { pickOrderId ->
                pickOrderRepository.findById(pickOrderId).ifPresent { po ->
                    po.assignTo = user
                    po.status = PickOrderStatus.RELEASED
                    pickOrderRepository.save(po)
                }
            }
        }
        
        // 更新 do_pick_order_record（若有）
        lines.forEach { line ->
            line.pickOrderId?.let { pickOrderId ->
                val records = doPickOrderRecordRepository.findByPickOrderId(pickOrderId)
                records.forEach { record ->
                    record.handledBy = request.userId
                    record.handlerName = user.name
                    record.ticketStatus = DoPickOrderStatus.released
                    record.ticketReleaseTime = LocalDateTime.now()
                }
                if (records.isNotEmpty()) doPickOrderRecordRepository.saveAll(records)
            }
        }
        
        return MessageResponse(
            id = doPickOrder.id,
            code = "SUCCESS",
            name = null,
            type = null,
            message = null,
            errorPosition = null,
            entity = null
        )
    }
    private fun checkDoPickOrderHasNonIssueLines(doPickOrderId: Long): Boolean {
        return try {
            val totalLinesSql = """
                SELECT COUNT(*) as total_lines
                FROM fpsmsdb.do_pick_order_line dpol
                WHERE dpol.do_pick_order_id = :doPickOrderId 
                AND dpol.deleted = 0
            """.trimIndent()
            val totalLinesResult = jdbcDao.queryForList(totalLinesSql, mapOf("doPickOrderId" to doPickOrderId))
            val totalLines = (totalLinesResult.firstOrNull()?.get("total_lines") as? Number)?.toInt() ?: 0
            if (totalLines == 0) return true
            val nonIssueLinesSql = """
                SELECT COUNT(*) as non_issue_lines
                FROM fpsmsdb.do_pick_order_line dpol
                WHERE dpol.do_pick_order_id = :doPickOrderId 
                AND dpol.deleted = 0
                AND (dpol.status IS NULL OR dpol.status != 'issue')
            """.trimIndent()
            val nonIssueLinesResult = jdbcDao.queryForList(nonIssueLinesSql, mapOf("doPickOrderId" to doPickOrderId))
            val nonIssueLines = (nonIssueLinesResult.firstOrNull()?.get("non_issue_lines") as? Number)?.toInt() ?: 0
            nonIssueLines > 0
        } catch (e: Exception) {
            println("❌ Error checking non-issue lines for do_pick_order $doPickOrderId: ${e.message}")
            true
        }
    }}