package com.github.nullptroma.wallenc.usecases

import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournal
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalEntry
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournalMerge
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncOperation
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncPaths
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncEngine
import com.github.nullptroma.wallenc.domain.interfaces.IStorageSyncGroupStore
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlin.coroutines.coroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.time.Instant
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong
import javax.inject.Inject
import javax.inject.Singleton

/**
 * Синхронизация по журналам storage в группе.
 * Блокировка на Yandex Disk — best-effort (см. [IStorageAccessor.tryAcquireSyncLock]);
 * сериализация внутри процесса — [groupMutexes].
 */
@Singleton
class StorageSyncEngine @Inject constructor(
    private val vaultsManager: IVaultsManager,
    private val groupStore: IStorageSyncGroupStore,
    private val findStorageUseCase: FindStorageUseCase,
) : IStorageSyncEngine {
    private val holderId: String = UUID.randomUUID().toString()
    private val groupMutexes = ConcurrentHashMap<String, Mutex>()
    private val syncGeneration = AtomicLong(0)

    override suspend fun syncAllGroups(
        reportProgress: (suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit)?,
    ): Unit = withContext(Dispatchers.IO) {
        val reporter = reportProgress ?: { _: Float?, _: TaskProgressLabel? -> }
        val groups = groupStore.getGroups()
        if (groups.isEmpty()) {
            reporter(null, TaskProgressLabel.SyncNoGroups)
            return@withContext
        }
        reporter(null, TaskProgressLabel.SyncPreparing(groups.size))
        for (group in groups) {
            coroutineContext.ensureActive()
            syncGroupInternal(
                groupId = group.id,
                reportProgress = reporter,
            )
        }
        reporter(null, TaskProgressLabel.SyncCompleted)
    }

    override suspend fun syncGroup(
        groupId: String,
        reportProgress: (suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit)?,
    ): Unit = withContext(Dispatchers.IO) {
        val reporter = reportProgress ?: { _: Float?, _: TaskProgressLabel? -> }
        syncGroupInternal(
            groupId = groupId,
            reportProgress = reporter,
        )
    }

    private suspend fun syncGroupInternal(
        groupId: String,
        reportProgress: suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit,
    ) {
        reportProgress(null, TaskProgressLabel.SyncGroupPreparing(groupId))
        val mutex = groupMutexes.getOrPut(groupId) { Mutex() }
        mutex.withLock {
            val generationSnapshot = syncGeneration.incrementAndGet()
            val group = groupStore.getGroups().firstOrNull { it.id == groupId }
            if (group == null) {
                reportProgress(null, TaskProgressLabel.SyncGroupNotFound(groupId))
                return
            }
            val storages = resolveStorages(group.storageUuids)
            if (storages.size < 2) {
                reportProgress(null, TaskProgressLabel.SyncGroupSkippedTooFewStorages(groupId))
                return
            }
            val incompatible = storages.filterNot { storage ->
                isStorageCompatibleWithGroup(
                    storage = storage,
                    group = group,
                )
            }
            if (incompatible.isNotEmpty()) {
                reportProgress(
                    null,
                    TaskProgressLabel.SyncGroupSkippedIncompatibleEncryption(groupId, incompatible.size),
                )
                return
            }

            var leaseUntil = Instant.now().plusSeconds(SYNC_LOCK_LEASE_SECONDS)
            val lockedAccessors = mutableListOf<IStorageAccessor>()
            try {
                reportProgress(null, TaskProgressLabel.SyncGroupAcquiringLocks(groupId))
                for ((lockIndex, storage) in storages.withIndex()) {
                    coroutineContext.ensureActive()
                    reportProgress(
                        null,
                        TaskProgressLabel.SyncGroupLockProgress(groupId, lockIndex + 1, storages.size),
                    )
                    val locked = storage.accessor.tryAcquireSyncLock(holderId, leaseUntil)
                    if (!locked) {
                        reportProgress(null, TaskProgressLabel.SyncGroupLockFailed(groupId))
                        return
                    }
                    lockedAccessors.add(storage.accessor)
                }

                val mergedByPath = mutableMapOf<String, StorageSyncJournalEntry>()
                val entriesByStorage = mutableMapOf<UUID, StorageSyncJournal>()

                reportProgress(null, TaskProgressLabel.SyncGroupReadingJournals(groupId))
                leaseUntil = renewLocksIfNeeded(
                    groupId = groupId,
                    lockedAccessors = lockedAccessors,
                    currentLeaseUntil = leaseUntil,
                    reportProgress = reportProgress,
                ) ?: return
                if (syncGeneration.get() != generationSnapshot) {
                    reportProgress(null, TaskProgressLabel.SyncGroupCancelled(groupId))
                    return
                }
                val journalReads = coroutineScope {
                    storages.mapIndexed { journalIndex, storage ->
                        async {
                            coroutineContext.ensureActive()
                            reportProgress(
                                null,
                                TaskProgressLabel.SyncGroupJournalProgress(
                                    groupId,
                                    journalIndex + 1,
                                    storages.size,
                                ),
                            )
                            storage.accessor.flushPendingSyncJournal()
                            storage to filterSyncableJournal(storage.accessor.readSyncJournal())
                        }
                    }.awaitAll()
                }
                for ((storage, journal) in journalReads) {
                    entriesByStorage[storage.uuid] = journal
                    mergedByPath.putAll(
                        StorageSyncJournalMerge.merge(mergedByPath, journal),
                    )
                }

                val mergedEntries = mergedByPath.entries.toList()
                if (mergedEntries.isEmpty()) {
                    reportProgress(null, TaskProgressLabel.SyncGroupNoJournalEntries(groupId))
                    return
                }

                reportProgress(
                    null,
                    TaskProgressLabel.SyncGroupProcessingEntries(groupId, mergedEntries.size),
                )
                var applyFailures = 0
                for ((pathIndex, merged) in mergedEntries.withIndex()) {
                    coroutineContext.ensureActive()
                    leaseUntil = renewLocksIfNeeded(
                        groupId = groupId,
                        lockedAccessors = lockedAccessors,
                        currentLeaseUntil = leaseUntil,
                        reportProgress = reportProgress,
                    ) ?: return
                    val path = merged.key
                    val winnerEntry = merged.value
                    reportProgress(
                        null,
                        TaskProgressLabel.SyncGroupEntryProgress(groupId, pathIndex + 1, mergedEntries.size),
                    )
                    if (syncGeneration.get() != generationSnapshot) {
                        reportProgress(null, TaskProgressLabel.SyncGroupCancelled(groupId))
                        return
                    }
                    val sourceStorage = findSourceStorage(storages, entriesByStorage, path, winnerEntry)
                    if (sourceStorage == null &&
                        winnerEntry.operation == StorageSyncOperation.UPSERT
                    ) {
                        continue
                    }

                    for (target in storages) {
                        coroutineContext.ensureActive()
                        if (target.uuid == sourceStorage?.uuid) {
                            continue
                        }
                        val targetEntry = entriesByStorage[target.uuid]?.get(path)
                        if (targetEntry != null && compareEntries(targetEntry, winnerEntry) >= 0) {
                            continue
                        }
                        val applied = applyEntry(
                            source = sourceStorage,
                            target = target,
                            entry = winnerEntry,
                        )
                        if (applied) {
                            target.accessor.putSyncJournalEntries(mapOf(path to winnerEntry))
                        } else {
                            applyFailures++
                        }
                    }
                }
                if (applyFailures > 0) {
                    reportProgress(
                        null,
                        TaskProgressLabel.SyncGroupEntriesFailed(groupId, applyFailures),
                    )
                }
                reportProgress(null, TaskProgressLabel.SyncGroupCompleted(groupId))
            } finally {
                for (accessor in lockedAccessors) {
                    runCatching {
                        accessor.releaseSyncLock(holderId)
                    }
                }
            }
        }
    }

    private fun filterSyncableJournal(journal: StorageSyncJournal): StorageSyncJournal {
        return journal.filterKeys { StorageSyncPaths.isSyncableUserPath(it) }
    }

    private suspend fun renewLocksIfNeeded(
        groupId: String,
        lockedAccessors: List<IStorageAccessor>,
        currentLeaseUntil: Instant,
        reportProgress: suspend (fraction: Float?, label: TaskProgressLabel?) -> Unit,
    ): Instant? {
        val now = Instant.now()
        if (currentLeaseUntil.isAfter(now.plusSeconds(SYNC_LOCK_RENEW_MARGIN_SECONDS))) {
            return currentLeaseUntil
        }
        val nextLeaseUntil = now.plusSeconds(SYNC_LOCK_LEASE_SECONDS)
        reportProgress(null, TaskProgressLabel.SyncGroupRenewingLocks(groupId))
        for (accessor in lockedAccessors) {
            coroutineContext.ensureActive()
            val renewed = runCatching {
                accessor.tryAcquireSyncLock(holderId, nextLeaseUntil)
            }.getOrElse { false }
            if (!renewed) {
                reportProgress(null, TaskProgressLabel.SyncGroupLockRenewalFailed(groupId))
                return null
            }
        }
        return nextLeaseUntil
    }

    private fun resolveStorages(uuids: Set<UUID>): List<IStorage> {
        return uuids.mapNotNull(findStorageUseCase::find)
    }

    private fun findSourceStorage(
        storages: List<IStorage>,
        entriesByStorage: Map<UUID, StorageSyncJournal>,
        path: String,
        winnerEntry: StorageSyncJournalEntry,
    ): IStorage? {
        if (winnerEntry.operation == StorageSyncOperation.DELETE ||
            winnerEntry.operation == StorageSyncOperation.TRASH
        ) {
            return storages.firstOrNull { storage ->
                entriesByStorage[storage.uuid]?.get(path) != null
            } ?: storages.firstOrNull()
        }
        return storages.firstOrNull { storage ->
            val entry = entriesByStorage[storage.uuid]?.get(path) ?: return@firstOrNull false
            compareEntries(entry, winnerEntry) == 0
        }
    }

    private suspend fun applyEntry(
        source: IStorage?,
        target: IStorage,
        entry: StorageSyncJournalEntry,
    ): Boolean {
        val result = when (entry.operation) {
            StorageSyncOperation.DELETE -> {
                runCatching {
                    target.accessor.delete(entry.path, recordSyncJournal = false)
                }
            }

            StorageSyncOperation.TRASH -> {
                runCatching {
                    target.accessor.moveToTrash(entry.path, recordSyncJournal = false)
                }
            }

            StorageSyncOperation.UPSERT -> {
                val sourceAccessor = source?.accessor ?: return false
                runCatching {
                    sourceAccessor.openRead(entry.path).use { input ->
                        target.accessor.openWrite(entry.path, recordSyncJournal = false).use { output ->
                            input.copyTo(output)
                        }
                    }
                }
            }
        }
        val error = result.exceptionOrNull() ?: return result.isSuccess
        if (error is CancellationException) {
            throw error
        }
        System.err.println(
            "StorageSyncEngine: apply ${entry.operation} ${entry.path} " +
                "target=${target.uuid}: ${error.message}",
        )
        return false
    }

    private fun compareEntries(a: StorageSyncJournalEntry, b: StorageSyncJournalEntry): Int {
        val seqCmp = a.revision.sequence.compareTo(b.revision.sequence)
        if (seqCmp != 0) {
            return seqCmp
        }
        val actorCmp = a.revision.actorId.compareTo(b.revision.actorId)
        if (actorCmp != 0) {
            return actorCmp
        }
        return a.revision.createdAt.compareTo(b.revision.createdAt)
    }

    private companion object {
        private const val SYNC_LOCK_LEASE_SECONDS: Long = 30 * 60
        private const val SYNC_LOCK_RENEW_MARGIN_SECONDS: Long = 60
    }
}
