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

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoRequest
import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleaseDoResult
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.stock.service.StockOutLineWorkbenchService
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotWorkbenchService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Instant
import java.sql.SQLException
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.concurrent.atomic.AtomicInteger
import jakarta.persistence.OptimisticLockException
import org.hibernate.StaleObjectStateException
import org.springframework.orm.ObjectOptimisticLockingFailureException

private const val WORKBENCH_RELEASE_RETRY_MAX = 3

/** 與 workbench 車線摘要一致：`車線-X`（預設車）不帶樓層 `storeId`。 */
private const val WORKBENCH_DEFAULT_TRUCK_LANCE_CODE = "車線-X"

/** 票號 `TI-B-yyyyMMdd-{floor}-nnn` 中段；無樓層時用 `X` 與 2F/4F 區隔。 */
private const val WORKBENCH_TICKET_FLOOR_SEGMENT_DEFAULT_TRUCK = "X"

private fun isWorkbenchOptimisticLockFailure(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
}

private fun isWorkbenchDeadlockFailure(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
        }
        c = c.cause
    }
    return false
}

private fun isWorkbenchRetriableFailure(t: Throwable?): Boolean =
    isWorkbenchOptimisticLockFailure(t) || isWorkbenchDeadlockFailure(t)

data class WorkbenchBatchReleaseSummary(
    val total: Int,
    val success: Int,
    val failed: List<Pair<Long, String>>,
    val createdDeliveryOrderPickOrders: Int,
)

data class WorkbenchBatchReleaseJobStatus(
    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
open class DoWorkbenchReleaseService(
    private val deliveryOrderService: DeliveryOrderService,
    private val jdbcDao: JdbcDao,
    private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService,
    private val stockOutLineWorkbenchService: StockOutLineWorkbenchService,
) {
    private val poolSize = Runtime.getRuntime().availableProcessors()
    private val executor = Executors.newFixedThreadPool(kotlin.math.min(poolSize, 4))
    /** Only one workbench batch-release job runs at a time (per JVM). */
    private val batchReleaseConcurrencyGate = Semaphore(1, true)
    private val jobs = ConcurrentHashMap<String, WorkbenchBatchReleaseJobStatus>()

    private fun releaseDeliveryOrderWorkbenchWithRetries(deliveryOrderId: Long, userId: Long, useV2: Boolean): ReleaseDoResult? {
        var released: ReleaseDoResult? = null
        for (attempt in 1..WORKBENCH_RELEASE_RETRY_MAX) {
            try {
                released = if (useV2) {
                    deliveryOrderService.releaseDeliveryOrderWithoutTicketNoHoldV2(
                        ReleaseDoRequest(id = deliveryOrderId, userId = userId)
                    )
                } else {
                    deliveryOrderService.releaseDeliveryOrderWithoutTicketNoHold(
                        ReleaseDoRequest(id = deliveryOrderId, userId = userId)
                    )
                }
                break
            } catch (e: Exception) {
                if (isWorkbenchRetriableFailure(e) && attempt < WORKBENCH_RELEASE_RETRY_MAX) {
                    Thread.sleep(50L * attempt)
                } else {
                    throw e
                }
            }
        }
        return released
    }

    open fun startBatchReleaseAsync(ids: List<Long>, userId: Long): MessageResponse =
        startBatchReleaseAsyncInternal(ids, userId, useV2 = false, dopReleaseType = "batch")

    /**
     * V2: deferred suggested pick / stock out / stock out lines until [DoWorkbenchDopoAssignmentService] assigns the ticket.
     */
    open fun startBatchReleaseAsyncV2(ids: List<Long>, userId: Long): MessageResponse =
        startBatchReleaseAsyncInternal(ids, userId, useV2 = true, dopReleaseType = "batch")

    /**
     * V2 async for one (or few) DOs: [delivery_order_pick_order.releaseType] = `single`, ticket prefix `TI-S-` (aligned with legacy single DO pick tickets).
     */
    open fun startBatchReleaseAsyncSingleV2(ids: List<Long>, userId: Long): MessageResponse =
        startBatchReleaseAsyncInternal(ids, userId, useV2 = true, dopReleaseType = "single")

    private fun startBatchReleaseAsyncInternal(
        ids: List<Long>,
        userId: Long,
        useV2: Boolean,
        dopReleaseType: String,
    ): MessageResponse {
        if (ids.isEmpty()) {
            return MessageResponse(
                id = null,
                code = "NO_IDS",
                name = null,
                type = asyncJobType(useV2, dopReleaseType),
                message = "No delivery order ids provided",
                errorPosition = null,
                entity = null
            )
        }

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

        executor.submit {
            batchReleaseConcurrencyGate.acquireUninterruptibly()
            try {
                val orderedIds = getOrderedDeliveryOrderIds(ids)
                val successResults = mutableListOf<ReleaseDoResult>()

                orderedIds.forEach { deliveryOrderId ->
                    try {
                        val statusRows = jdbcDao.queryForList(
                            """
                            SELECT status
                            FROM fpsmsdb.delivery_order
                            WHERE id = :id AND deleted = 0
                            """.trimIndent(),
                            mapOf("id" to deliveryOrderId)
                        )
                        val currentStatus = statusRows.firstOrNull()?.get("status")?.toString()?.lowercase()
                        if (currentStatus == DeliveryOrderStatus.COMPLETED.value || currentStatus == DeliveryOrderStatus.RECEIVING.value) {
                            return@forEach
                        }

                        val released = releaseDeliveryOrderWorkbenchWithRetries(deliveryOrderId, userId, useV2)

                        if (released != null) {
                            successResults += released
                            status.success.incrementAndGet()
                        } else {
                            synchronized(status.failed) {
                                status.failed.add(deliveryOrderId to "Release returned null")
                            }
                        }
                    } catch (e: Exception) {
                        synchronized(status.failed) {
                            status.failed.add(deliveryOrderId to (e.message ?: "Unknown error"))
                        }
                    }
                }

                try {
                    createAndLinkDeliveryOrderPickOrders(successResults, dopReleaseType)
                } catch (e: Exception) {
                    // header-link failure shouldn't crash job; status.failed already includes per-DO failures
                    println("❌ workbench createAndLinkDeliveryOrderPickOrders failed: ${e.message}")
                }

                if (!useV2) {
                    successResults.forEach { result ->
                        try {
                            suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(result.pickOrderId)
                            stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(result.pickOrderId, userId)
                        } catch (e: Exception) {
                            synchronized(status.failed) {
                                status.failed.add(result.deliveryOrderId to ("Downstream workbench step failed: ${e.message}"))
                            }
                        }
                    }
                }
            } finally {
                status.running = false
                status.finishedAt = Instant.now().toEpochMilli()
                batchReleaseConcurrencyGate.release()
            }
        }

        return MessageResponse(
            id = null,
            code = "STARTED",
            name = null,
            type = asyncJobType(useV2, dopReleaseType),
            message = asyncJobMessage(useV2, dopReleaseType),
            errorPosition = null,
            entity = mapOf("jobId" to jobId, "total" to ids.size)
        )
    }

    open fun getBatchReleaseProgress(jobId: String): MessageResponse {
        val s = jobs[jobId] ?: return MessageResponse(
            id = null,
            code = "NOT_FOUND",
            name = null,
            type = "workbench_batch_release_progress",
            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 = "workbench_batch_release_progress",
            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
            )
        )
    }

    /**
     * New batch release flow for workbench:
     * 1) release DO -> create pick_order (existing flow),
     * 2) create grouped header in delivery_order_pick_order,
     * 3) backfill pick_order.deliveryOrderPickOrderId.
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun releaseBatch(ids: List<Long>, userId: Long): MessageResponse =
        releaseBatchInternal(ids, userId, useV2 = false)

    /**
     * Synchronous batch release V2: same as [releaseBatch] but uses [DeliveryOrderService.releaseDeliveryOrderWithoutTicketNoHoldV2]
     * and skips suggested pick + stock out line creation (done on ticket assign).
     */
    @Transactional(rollbackFor = [Exception::class])
    open fun releaseBatchV2(ids: List<Long>, userId: Long): MessageResponse =
        releaseBatchInternal(ids, userId, useV2 = true)

    private fun releaseBatchInternal(ids: List<Long>, userId: Long, useV2: Boolean): MessageResponse {
        if (ids.isEmpty()) {
            return MessageResponse(
                id = null,
                code = "NO_IDS",
                name = null,
                type = if (useV2) "workbench_batch_release_v2" else "workbench_batch_release",
                message = "No delivery order ids provided",
                errorPosition = null,
                entity = null
            )
        }

        val orderedIds = getOrderedDeliveryOrderIds(ids)
        val successResults = mutableListOf<ReleaseDoResult>()
        val failed = mutableListOf<Pair<Long, String>>()

        orderedIds.forEach { deliveryOrderId ->
            try {
                // Skip completed-like status explicitly to keep batch idempotent.
                val statusRows = jdbcDao.queryForList(
                    """
                    SELECT status
                    FROM fpsmsdb.delivery_order
                    WHERE id = :id AND deleted = 0
                    """.trimIndent(),
                    mapOf("id" to deliveryOrderId)
                )
                val currentStatus = statusRows.firstOrNull()?.get("status")?.toString()?.lowercase()
                if (currentStatus == DeliveryOrderStatus.COMPLETED.value || currentStatus == DeliveryOrderStatus.RECEIVING.value) {
                    return@forEach
                }

                val released = releaseDeliveryOrderWorkbenchWithRetries(deliveryOrderId, userId, useV2)

                if (released == null) {
                    failed += deliveryOrderId to "Release returned null"
                } else {
                    successResults += released
                }
            } catch (e: Exception) {
                failed += deliveryOrderId to (e.message ?: "Unknown error")
            }
        }

        val createdHeaders = createAndLinkDeliveryOrderPickOrders(successResults, "batch")
        if (!useV2) {
            successResults.forEach { result ->
                try {
                    suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrder(result.pickOrderId)
                    stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(result.pickOrderId, userId)
                } catch (_: Exception) {
                    // keep batch release resilient; failures are reflected by missing downstream data and can be re-triggered
                }
            }
        }
        val summary = WorkbenchBatchReleaseSummary(
            total = ids.size,
            success = successResults.size,
            failed = failed,
            createdDeliveryOrderPickOrders = createdHeaders
        )

        return MessageResponse(
            id = null,
            code = if (failed.isEmpty()) "SUCCESS" else "PARTIAL_SUCCESS",
            name = if (useV2) "workbench_batch_release_v2" else "workbench_batch_release",
            type = if (useV2) "workbench_batch_release_v2" else "workbench_batch_release",
            message = "Released ${successResults.size}/${ids.size} delivery orders",
            errorPosition = null,
            entity = summary
        )
    }

    /**
     * `TI-B-yyyyMMdd-2F-001` (batch), `TI-S-yyyyMMdd-2F-001` (single), or `TI-E-yyyyMMdd-2F-001` (Etra),
     * same suffix rules as [DoReleaseCoordinatorService] / legacy `do_pick_order`.
     */
    private fun nextDeliveryOrderPickOrderTicketNo(
        requiredDate: LocalDate,
        storeDisplay: String,
        ticketLetter: String,
    ): String {
        require(ticketLetter == "B" || ticketLetter == "S" || ticketLetter == "E") {
            "ticketLetter must be B, S or E"
        }
        val ymd = requiredDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))
        val floor = storeDisplay.replace("/", "").trim()
        val prefix = "TI-$ticketLetter-$ymd-$floor-"
        val sql = """
            SELECT ticket_no AS t FROM fpsmsdb.do_pick_order WHERE deleted = 0 AND ticket_no LIKE CONCAT(:prefix, '%')
            UNION ALL
            SELECT ticketNo AS t FROM fpsmsdb.delivery_order_pick_order WHERE deleted = 0 AND ticketNo LIKE CONCAT(:prefix, '%')
        """.trimIndent()
        val rows = try {
            jdbcDao.queryForList(sql, mapOf("prefix" to prefix))
        } catch (_: Exception) {
            emptyList()
        }
        val suffixPattern = Regex("^${Regex.escape(prefix)}(\\d+)$")
        var maxSeq = 0
        for (row in rows) {
            val key = row.keys.find { it.equals("t", ignoreCase = true) } ?: row.keys.firstOrNull() ?: continue
            val ticket = row[key]?.toString() ?: continue
            suffixPattern.find(ticket)?.groupValues?.getOrNull(1)?.toIntOrNull()?.let { if (it > maxSeq) maxSeq = it }
        }
        val next = maxSeq + 1
        return "$prefix${next.toString().padStart(3, '0')}"
    }

    private fun nextDeliveryOrderPickOrderBatchTicketNo(requiredDate: LocalDate, storeDisplay: String): String =
        nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "B")

    private fun nextDeliveryOrderPickOrderSingleTicketNo(requiredDate: LocalDate, storeDisplay: String): String =
        nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "S")

    private fun nextDeliveryOrderPickOrderEtraTicketNo(requiredDate: LocalDate, storeDisplay: String): String =
        nextDeliveryOrderPickOrderTicketNo(requiredDate, storeDisplay, "E")

    private fun asyncJobType(useV2: Boolean, dopReleaseType: String): String {
        val single = dopReleaseType.equals("single", ignoreCase = true)
        return when {
            useV2 && single -> "workbench_single_release_async_v2"
            useV2 -> "workbench_batch_release_async_v2"
            single -> "workbench_single_release_async"
            else -> "workbench_batch_release_async"
        }
    }

    private fun asyncJobMessage(useV2: Boolean, dopReleaseType: String): String {
        val single = dopReleaseType.equals("single", ignoreCase = true)
        return when {
            useV2 && single -> "Workbench single release V2 started"
            useV2 -> "Workbench batch release V2 started"
            single -> "Workbench single release started"
            else -> "Workbench batch release started"
        }
    }

    private fun getOrderedDeliveryOrderIds(ids: List<Long>): List<Long> {
        if (ids.isEmpty()) return emptyList()
        return try {
            val sql = """
                SELECT do.id
                FROM fpsmsdb.delivery_order do
                WHERE do.deleted = 0
                  AND do.id IN (${ids.joinToString(",")})
                ORDER BY DATE(do.estimatedArrivalDate), do.id
            """.trimIndent()
            val sorted = jdbcDao.queryForList(sql).mapNotNull { (it["id"] as? Number)?.toLong() }
            if (sorted.isEmpty()) ids else sorted
        } catch (_: Exception) {
            ids
        }
    }

    private fun createAndLinkDeliveryOrderPickOrders(
        results: List<ReleaseDoResult>,
        dopReleaseType: String = "batch",
    ): Int {
        if (results.isEmpty()) return 0

        val grouped = results.groupBy {
            listOf(
                it.shopId?.toString() ?: "",
                it.estimatedArrivalDate?.toString() ?: "",
                it.preferredFloor,
                it.truckId?.toString() ?: "",
                it.truckDepartureTime?.toString() ?: "",
                it.truckLanceCode ?: "",
                it.isExtra.toString(),
            ).joinToString("|")
        }

        var createdHeaders = 0
        grouped.values.forEach { group ->
            val first = group.first()
            val isDefaultTruckLane =
                first.usedDefaultTruck == true ||
                    first.truckLanceCode?.trim() == WORKBENCH_DEFAULT_TRUCK_LANCE_CODE
            val storeId: String? = if (isDefaultTruckLane) {
                null
            } else {
                when (first.preferredFloor) {
                    "2F" -> "2/F"
                    "4F" -> "4/F"
                    else -> "2/F"
                }
            }
            val ticketFloorSegment = if (isDefaultTruckLane) {
                WORKBENCH_TICKET_FLOOR_SEGMENT_DEFAULT_TRUCK
            } else {
                (storeId ?: "2/F").replace("/", "").trim()
            }
            val requiredDate = first.estimatedArrivalDate ?: LocalDate.now()
            val releaseTypeCol = if (first.isExtra) {
                "isExtra"
            } else if (dopReleaseType.equals("single", ignoreCase = true)) {
                "single"
            } else {
                "batch"
            }
            val tempTicket = if (first.isExtra) {
                nextDeliveryOrderPickOrderEtraTicketNo(requiredDate, ticketFloorSegment)
            } else if (releaseTypeCol == "single") {
                nextDeliveryOrderPickOrderSingleTicketNo(requiredDate, ticketFloorSegment)
            } else {
                nextDeliveryOrderPickOrderBatchTicketNo(requiredDate, ticketFloorSegment)
            }
            val now = LocalDateTime.now()

            // Column names must match Liquibase `01_alter_stock_take.sql` (camelCase), not snake_case.
            jdbcDao.executeUpdate(
                """
                INSERT INTO fpsmsdb.delivery_order_pick_order (
                    truckId, shopId, storeId, requiredDeliveryDate, truckDepartureTime,
                    truckLanceCode, shopCode, shopName, loadingSequence, ticketNo,
                    ticketReleaseTime, ticketStatus, releaseType, handledBy, handlerName,
                    created, createdBy, version, modified, modifiedBy, deleted
                ) VALUES (
                    :truckId, :shopId, :storeId, :requiredDeliveryDate, :truckDepartureTime,
                    :truckLanceCode, :shopCode, :shopName, :loadingSequence, :ticketNo,
                    NULL, 'pending', :releaseType, NULL, NULL,
                    :created, :createdBy, 0, :modified, :modifiedBy, 0
                )
                """.trimIndent(),
                mapOf(
                    "truckId" to first.truckId,
                    "shopId" to first.shopId,
                    "storeId" to storeId,
                    "requiredDeliveryDate" to (first.estimatedArrivalDate ?: LocalDate.now()),
                    "truckDepartureTime" to first.truckDepartureTime,
                    "truckLanceCode" to first.truckLanceCode,
                    "shopCode" to first.shopCode,
                    "shopName" to first.shopName,
                    "loadingSequence" to first.loadingSequence,
                    "ticketNo" to tempTicket,
                    "releaseType" to releaseTypeCol,
                    "created" to now,
                    "createdBy" to "system",
                    "modified" to now,
                    "modifiedBy" to "system",
                )
            )

            val headerId = jdbcDao.queryForList(
                """
                SELECT id
                FROM fpsmsdb.delivery_order_pick_order
                WHERE ticketNo = :ticketNo
                ORDER BY id DESC
                LIMIT 1
                """.trimIndent(),
                mapOf("ticketNo" to tempTicket)
            ).firstOrNull()?.get("id")?.let { (it as Number).toLong() } ?: return@forEach

            group.forEach { r ->
                jdbcDao.executeUpdate(
                    """
                    UPDATE fpsmsdb.pick_order
                    SET deliveryOrderPickOrderId = :headerId
                    WHERE id = :pickOrderId
                    """.trimIndent(),
                    mapOf(
                        "headerId" to headerId,
                        "pickOrderId" to r.pickOrderId
                    )
                )
            }
            createdHeaders++
        }

        return createdHeaders
    }
}
