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

import com.ffii.fpsms.modules.pickOrder.entity.*
import com.ffii.fpsms.modules.pickOrder.support.isOptimisticLockFailure
import com.ffii.fpsms.modules.pickOrder.web.models.CreateTruckLaneSnapshotRequest
import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckRequest
import com.ffii.fpsms.modules.pickOrder.web.models.CreateTruckWithoutShopRequest
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.LocalTime
import kotlin.random.Random

enum class ScheduleApplyOutcome {
    APPLIED,
    FAILED,
    SKIPPED,
    NOT_FOUND,
}

private val lineActionApplyOrder =
    mapOf(
        TruckLaneScheduleLineAction.ENSURE_LANE to 0,
        TruckLaneScheduleLineAction.CREATE to 1,
        TruckLaneScheduleLineAction.MOVE to 2,
        TruckLaneScheduleLineAction.DELETE to 3,
    )

@Service
open class TruckLaneScheduleApplier(
    private val scheduleRepository: TruckLaneScheduleRepository,
    private val scheduleLineRepository: TruckLaneScheduleLineRepository,
    private val truckService: TruckService,
    private val truckLaneVersionService: TruckLaneVersionService,
) {
    private val logger = LoggerFactory.getLogger(javaClass)

    @Transactional(
        propagation = Propagation.REQUIRES_NEW,
        noRollbackFor = [IllegalArgumentException::class, IllegalStateException::class],
    )
    open fun applyOne(scheduleId: Long): ScheduleApplyOutcome {
        val schedule = scheduleRepository.findByIdAndDeletedFalseForUpdate(scheduleId)
            ?: return ScheduleApplyOutcome.NOT_FOUND
        if (schedule.status != TruckLaneScheduleStatus.PENDING) {
            return ScheduleApplyOutcome.SKIPPED
        }
        if (scheduleRepository.existsOtherApplying(TruckLaneScheduleStatus.APPLYING, scheduleId)) {
            return ScheduleApplyOutcome.SKIPPED
        }

        return TruckLaneScheduleLockSupport.withApplyingSchedule(scheduleId) {
            applyOneLocked(schedule)
        }
    }

    private fun applyOneLocked(schedule: TruckLaneSchedule): ScheduleApplyOutcome {
        val scheduleId = schedule.id ?: return ScheduleApplyOutcome.NOT_FOUND

        schedule.status = TruckLaneScheduleStatus.APPLYING
        scheduleRepository.saveAndFlush(schedule)

        val lines =
            scheduleLineRepository
                .findAllBySchedule_IdAndDeletedFalseOrderByIdAsc(scheduleId)
                .sortedBy { lineActionApplyOrder[it.lineAction] ?: 99 }

        val createdTruckRowIds = mutableListOf<Long>()
        var preApplySnapshotId: Long? = null

        try {
            val preSnap =
                truckLaneVersionService.createSnapshot(
                    CreateTruckLaneSnapshotRequest(
                        truckLanceCode = null,
                        note = "排程 #${schedule.id} 套用前快照 ${schedule.executeAt}",
                        createdBy = schedule.createdBy,
                    ),
                )
            preApplySnapshotId = preSnap.id
            schedule.preApplySnapshotVersionId = preApplySnapshotId
            scheduleRepository.saveAndFlush(schedule)

            val now = LocalDateTime.now()
            for (line in lines) {
                if (line.lineStatus != TruckLaneScheduleLineStatus.PENDING) continue
                applyLineWithRetry(line, createdTruckRowIds)
                line.lineStatus = TruckLaneScheduleLineStatus.APPLIED
                line.appliedAt = now
                line.errorMessage = null
                scheduleLineRepository.save(line)
            }

            var postSnapshotId: Long? = null
            try {
                val postSnap =
                    truckLaneVersionService.createSnapshot(
                        CreateTruckLaneSnapshotRequest(
                            truckLanceCode = null,
                            note = "排程 #${schedule.id} 套用 (${
                                lines.count {
                                    it.lineStatus == TruckLaneScheduleLineStatus.APPLIED
                                }
                            } 列) ${schedule.executeAt}",
                            createdBy = schedule.createdBy,
                        ),
                    )
                postSnapshotId = postSnap.id
            } catch (e: Exception) {
                logger.warn("Post-apply snapshot failed for schedule {}", scheduleId, e)
            }

            persistScheduleState(scheduleId) { fresh ->
                fresh.appliedAt = now
                fresh.status = TruckLaneScheduleStatus.APPLIED
                fresh.errorMessage = null
                if (postSnapshotId != null) {
                    fresh.snapshotVersionId = postSnapshotId
                }
            }
            return ScheduleApplyOutcome.APPLIED
        } catch (e: Exception) {
            return abortAndRestore(
                scheduleId = scheduleId,
                lines = lines,
                preApplySnapshotId = preApplySnapshotId,
                createdTruckRowIds = createdTruckRowIds,
                cause = e,
            )
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    open fun markFailed(scheduleId: Long, error: Throwable) {
        try {
            TruckLaneScheduleLockSupport.withApplyingSchedule(scheduleId) {
                val schedule = scheduleRepository.findByIdAndDeletedFalseForUpdate(scheduleId)
                if (schedule != null &&
                    (schedule.status == TruckLaneScheduleStatus.APPLYING ||
                        schedule.status == TruckLaneScheduleStatus.PENDING)
                ) {
                    abortAndRestore(
                        scheduleId = scheduleId,
                        lines =
                            scheduleLineRepository.findAllBySchedule_IdAndDeletedFalseOrderByIdAsc(
                                schedule.id ?: 0L,
                            ),
                        preApplySnapshotId = schedule.preApplySnapshotVersionId,
                        createdTruckRowIds = collectCreatedTruckIdsFromLines(schedule.id ?: 0L),
                        cause = error,
                    )
                }
            }
        } catch (e: Exception) {
            if (isOptimisticLockFailure(e)) {
                logger.warn("Schedule {} markFailed optimistic lock, rethrowing", scheduleId)
                throw e
            }
            logger.warn("Schedule {} markFailed failed", scheduleId, e)
            throw e
        }
    }

    private fun applyLineWithRetry(
        line: TruckLaneScheduleLine,
        createdTruckRowIds: MutableList<Long>,
    ) {
        var last: Exception? = null
        repeat(3) { attempt ->
            try {
                val note = applyLine(line, createdTruckRowIds)
                line.errorMessage = note
                return
            } catch (e: Exception) {
                last = e
                if (!isOptimisticLockFailure(e) || attempt == 2) throw e
                Thread.sleep(50L + Random.nextLong(150))
            }
        }
        throw last ?: IllegalStateException("apply line failed")
    }

    private fun applyLine(
        line: TruckLaneScheduleLine,
        createdTruckRowIds: MutableList<Long>,
    ): String? {
        return when (line.lineAction) {
            TruckLaneScheduleLineAction.ENSURE_LANE -> applyEnsureLane(line)
            TruckLaneScheduleLineAction.CREATE -> applyCreate(line, createdTruckRowIds)
            TruckLaneScheduleLineAction.MOVE -> applyMove(line)
            TruckLaneScheduleLineAction.DELETE -> applyDelete(line)
        }
    }

    private fun abortAndRestore(
        scheduleId: Long,
        lines: List<TruckLaneScheduleLine>,
        preApplySnapshotId: Long?,
        createdTruckRowIds: List<Long>,
        cause: Throwable,
    ): ScheduleApplyOutcome {
        val errMsg = (cause.message ?: cause.javaClass.simpleName).take(2000)
        logger.warn("Schedule {} apply failed, restoring: {}", scheduleId, errMsg, cause)

        var restoreNote: String? = null
        if (preApplySnapshotId != null) {
            // 僅還原受影響車線，避免洗掉套用期間其他車線的手動修改。
            val affectedLaneCodes =
                lines
                    .flatMap {
                        listOfNotNull(
                            it.fromTruckLanceCode?.trim(),
                            it.toTruckLanceCode?.trim(),
                        )
                    }
                    .filter { it.isNotEmpty() }
                    .toSet()
            val affectedTruckRowIds = lines.mapNotNull { it.truckRowId }.toSet()
            try {
                restoreNote =
                    truckLaneVersionService.restoreLanes(
                        preApplySnapshotId,
                        affectedLaneCodes,
                        affectedTruckRowIds,
                        skipPostSnapshot = true,
                    )
            } catch (restoreEx: Exception) {
                restoreNote = "restore failed: ${restoreEx.message}"
                logger.error(
                    "Schedule {} restore from snapshot {} failed",
                    scheduleId,
                    preApplySnapshotId,
                    restoreEx,
                )
            }
        }

        for (truckRowId in createdTruckRowIds.distinct()) {
            try {
                truckService.deleteById(truckRowId)
            } catch (delEx: Exception) {
                logger.warn(
                    "Schedule {} secondary DELETE sweep failed for truck {}",
                    scheduleId,
                    truckRowId,
                    delEx,
                )
            }
        }

        val now = LocalDateTime.now()
        lines.forEach { line ->
            if (line.lineStatus == TruckLaneScheduleLineStatus.PENDING) {
                line.lineStatus = TruckLaneScheduleLineStatus.FAILED
                line.errorMessage = errMsg
                line.appliedAt = now
                scheduleLineRepository.save(line)
            }
        }

        val restoreSuffix =
            if (preApplySnapshotId != null) {
                "；已還原 snapshot #$preApplySnapshotId${restoreNote?.let { " ($it)" } ?: ""}"
            } else {
                ""
            }
        persistScheduleState(scheduleId) { fresh ->
            fresh.status = TruckLaneScheduleStatus.FAILED
            fresh.appliedAt = now
            fresh.errorMessage = (errMsg + restoreSuffix).take(2000)
        }
        return ScheduleApplyOutcome.FAILED
    }

    private fun persistScheduleState(
        scheduleId: Long,
        patch: (TruckLaneSchedule) -> Unit,
    ) {
        var last: Exception? = null
        repeat(3) { attempt ->
            try {
                val fresh =
                    scheduleRepository.findByIdAndDeletedFalseForUpdate(scheduleId)
                        ?: return
                patch(fresh)
                scheduleRepository.saveAndFlush(fresh)
                return
            } catch (e: Exception) {
                last = e
                if (!isOptimisticLockFailure(e) || attempt == 2) throw e
                Thread.sleep(50L + Random.nextLong(150))
            }
        }
        throw last ?: IllegalStateException("persist schedule failed")
    }

    private fun collectCreatedTruckIdsFromLines(scheduleId: Long): List<Long> {
        return scheduleLineRepository
            .findAllBySchedule_IdAndDeletedFalseOrderByIdAsc(scheduleId)
            .filter {
                it.lineAction == TruckLaneScheduleLineAction.CREATE &&
                    it.truckRowId != null &&
                    it.lineStatus == TruckLaneScheduleLineStatus.APPLIED
            }
            .mapNotNull { it.truckRowId }
    }

    private fun applyEnsureLane(line: TruckLaneScheduleLine): String? {
        val toKey = truckService.toBucketKey(line)
        if (truckService.trucksInLaneBucket(toKey).isNotEmpty()) {
            return "車線已存在，跳過建立"
        }
        ensureTargetLaneIfEmpty(line, fallbackDeparture = null)
        return null
    }

    /**
     * When MOVE/CREATE targets a lane bucket with no rows yet, create a placeholder row
     * (same as ENSURE_LANE) so apply matches schedule creation rules with allowEmptyTarget.
     */
    private fun ensureTargetLaneIfEmpty(
        line: TruckLaneScheduleLine,
        fallbackDeparture: LocalTime?,
    ): String? {
        val toKey = truckService.toBucketKey(line)
        if (truckService.trucksInLaneBucket(toKey).isNotEmpty()) return null
        val departure =
            line.departureTime
                ?: fallbackDeparture
                ?: throw IllegalArgumentException(
                    "目標車線 ${toKey.truckLanceCode} 不存在或尚無任何列，且排程未提供 departureTime",
                )
        truckService.createTruckWithoutShop(
            CreateTruckWithoutShopRequest(
                store_id = toKey.storeId,
                truckLanceCode = toKey.truckLanceCode,
                departureTime = departure,
                loadingSequence = line.toLoadingSequence ?: 0,
                districtReference = line.toDistrictReference,
                remark = toKey.remark,
                logisticId = line.logisticId,
            ),
        )
        return "已自動建立空白車線 ${toKey.truckLanceCode}"
    }

    private fun applyCreate(
        line: TruckLaneScheduleLine,
        createdTruckRowIds: MutableList<Long>,
    ): String? {
        val shopId = line.shopId ?: throw IllegalArgumentException("缺少 shopId")
        val departure = line.departureTime ?: throw IllegalArgumentException("缺少 departureTime")
        val toKey = truckService.toBucketKey(line)
        val seq = line.toLoadingSequence ?: 0
        val saved =
            truckService.createTruckInShop(
                SaveTruckRequest(
                    id = null,
                    store_id = toKey.storeId,
                    truckLanceCode = toKey.truckLanceCode,
                    departureTime = departure,
                    shopId = shopId,
                    shopName = line.shopName.orEmpty(),
                    shopCode = line.shopCode.orEmpty(),
                    loadingSequence = seq,
                    remark = toKey.remark,
                    districtReference = line.toDistrictReference,
                    logisticId = line.logisticId,
                ),
            )
        val rowId = saved.id ?: throw IllegalStateException("CREATE 未取得 truck id")
        line.truckRowId = rowId
        createdTruckRowIds.add(rowId)
        return null
    }

    private fun applyMove(line: TruckLaneScheduleLine): String? {
        val truckRowId = line.truckRowId ?: throw IllegalArgumentException("缺少 truckRowId")
        var truck =
            truckService.findTruckRowByIdIncludingDeleted(truckRowId)
                ?: throw IllegalArgumentException("找不到 truck #$truckRowId")

        val notes = mutableListOf<String>()
        if (truck.deleted == true) {
            truck = truckService.restoreTruckRowIfDeleted(truck)
            notes.add("店鋪列已從刪除狀態還原")
        }

        val current = truckService.truckLaneBucketKeyOf(truck)
        val fromCode = line.fromTruckLanceCode?.trim().orEmpty()
        val fromStore = line.fromStoreId?.trim().orEmpty()
        val fromRemark = line.fromRemark
        if (current.truckLanceCode != fromCode ||
            current.storeId != fromStore ||
            (current.remark ?: "") != (fromRemark ?: "")
        ) {
            notes.add("店鋪列已不在排程建立時的來源車線，仍依 truck.id 移至目標")
            logger.info(
                "Schedule line {} truck {} moved from {} to target (was scheduled from {} {})",
                line.id,
                truckRowId,
                "${current.truckLanceCode}/${current.storeId}",
                fromCode,
                fromStore,
            )
        }

        val toKey = truckService.toBucketKey(line)
        val seq = line.toLoadingSequence ?: throw IllegalArgumentException("缺少 toLoadingSequence")
        ensureTargetLaneIfEmpty(line, fallbackDeparture = truck.departureTime)?.let { notes.add(it) }
        truckService.applyScheduledTruckMove(
            truckRowId,
            toKey,
            line.toDistrictReference,
            seq,
            line.departureTime,
        )
        return notes.takeIf { it.isNotEmpty() }?.joinToString("；")
    }

    private fun applyDelete(line: TruckLaneScheduleLine): String? {
        val truckRowId = line.truckRowId ?: throw IllegalArgumentException("缺少 truckRowId")
        truckService.deleteById(truckRowId)
        return null
    }
}
