package com.github.nullptroma.wallenc.usecases.fakes

import com.github.nullptroma.wallenc.domain.datatypes.DataPage
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.StorageSyncLock
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncOperation
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncPaths
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncRevision
import com.github.nullptroma.wallenc.domain.interfaces.IDirectory
import com.github.nullptroma.wallenc.domain.interfaces.IFile
import com.github.nullptroma.wallenc.domain.interfaces.IMetaInfo
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emptyFlow
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.time.Instant

class FakeStorageAccessor : IStorageAccessor {
    val dataFiles: MutableMap<String, ByteArray> = mutableMapOf()

    private fun norm(path: String): String = StorageSyncPaths.normalize(path)
    val trashedPaths: MutableSet<String> = mutableSetOf()
    private val systemFiles: MutableMap<String, ByteArray> = mutableMapOf()
    private val _filesUpdates = MutableSharedFlow<DataPage<IFile>>(extraBufferCapacity = 16)

    var syncJournal: StorageSyncJournal = emptyMap()
    var syncLock: StorageSyncLock? = null
    var acquireLockResult: Boolean = true
    var readSyncJournalThrows: Throwable? = null
    var openReadDelayMs: Long = 0

    override val size: StateFlow<Long?> = MutableStateFlow(0L)
    override val numberOfFiles: StateFlow<Int?> = MutableStateFlow(0)
    override val isAvailable: StateFlow<Boolean> = MutableStateFlow(true)
    override val filesUpdates: SharedFlow<DataPage<IFile>> = _filesUpdates
    override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = MutableSharedFlow()

    override suspend fun getAllFiles(): List<IFile> = emptyList()

    override suspend fun getFiles(path: String): List<IFile> = emptyList()

    override fun getFilesFlow(path: String): Flow<DataPage<IFile>> = emptyFlow()

    override suspend fun getAllDirs(): List<IDirectory> = emptyList()

    override suspend fun getDirs(path: String): List<IDirectory> = emptyList()

    override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> = emptyFlow()

    override suspend fun getFileInfo(path: String): IFile {
        val key = norm(path)
        val bytes = dataFiles[key] ?: throw IllegalStateException("File not found: $path")
        return FakeFile(key, bytes.size.toLong())
    }

    override suspend fun getDirInfo(path: String): IDirectory {
        error("Not implemented in tests")
    }

    override suspend fun setHidden(path: String, hidden: Boolean) = Unit

    override suspend fun touchFile(path: String) = Unit

    override suspend fun touchDir(path: String) = Unit

    override suspend fun delete(path: String, recordSyncJournal: Boolean) {
        dataFiles.remove(norm(path))
        if (recordSyncJournal) {
            recordDeleteJournal(path)
        }
    }

    override suspend fun openWrite(path: String, recordSyncJournal: Boolean): OutputStream {
        return object : ByteArrayOutputStream() {
            override fun close() {
                dataFiles[norm(path)] = toByteArray()
                _filesUpdates.tryEmit(
                    DataPage(
                        listOf(FakeFile(path)),
                        pageLength = 1,
                        pageIndex = 0,
                    ),
                )
            }
        }
    }

    override suspend fun openRead(path: String): InputStream {
        if (openReadDelayMs > 0) {
            delay(openReadDelayMs)
        }
        val bytes = dataFiles[norm(path)] ?: throw IllegalStateException("File not found: $path")
        return ByteArrayInputStream(bytes)
    }

    override suspend fun moveToTrash(path: String, recordSyncJournal: Boolean) {
        val key = norm(path)
        if (key in dataFiles) {
            trashedPaths.add(key)
            if (recordSyncJournal) {
                recordTrashJournal(path)
            }
        }
    }

    override suspend fun openReadSystemFile(name: String): InputStream {
        val bytes = systemFiles[name] ?: ByteArray(0)
        return ByteArrayInputStream(bytes)
    }

    override suspend fun openWriteSystemFile(name: String): OutputStream {
        return object : ByteArrayOutputStream() {
            override fun close() {
                systemFiles[name] = toByteArray()
            }
        }
    }

    override suspend fun readSyncJournal(): StorageSyncJournal {
        readSyncJournalThrows?.let { throw it }
        return syncJournal
    }

    override suspend fun flushPendingSyncJournal() = Unit

    override suspend fun putSyncJournalEntries(entries: StorageSyncJournal) {
        syncJournal = StorageSyncJournalMerge.merge(syncJournal, entries)
    }

    private suspend fun recordDeleteJournal(path: String) {
        appendJournalEntry(path, StorageSyncOperation.DELETE)
    }

    private suspend fun recordTrashJournal(path: String) {
        appendJournalEntry(path, StorageSyncOperation.TRASH)
    }

    private suspend fun appendJournalEntry(path: String, operation: StorageSyncOperation) {
        val cleaned = norm(path)
        val nextSequence = (syncJournal.values.maxOfOrNull { it.revision.sequence } ?: 0L) + 1L
        putSyncJournalEntries(
            mapOf(
                cleaned to StorageSyncJournalEntry(
                    path = cleaned,
                    operation = operation,
                    revision = StorageSyncRevision(
                        sequence = nextSequence,
                        actorId = "fake-actor",
                        createdAt = Instant.now(),
                    ),
                ),
            ),
        )
    }

    override suspend fun readSyncLock(): StorageSyncLock? = syncLock

    override suspend fun tryAcquireSyncLock(holderId: String, leaseUntil: Instant): Boolean {
        if (!acquireLockResult) return false
        syncLock = StorageSyncLock(holderId, leaseUntil, Instant.now())
        return true
    }

    override suspend fun releaseSyncLock(holderId: String) {
        if (syncLock?.holderId == holderId) {
            syncLock = null
        }
    }

    override suspend fun forceClearSyncLock() {
        syncLock = null
    }
}

class FakeFile(
    path: String,
    size: Long = 0L,
) : IFile {
    override val metaInfo: IMetaInfo = object : IMetaInfo {
        override val size: Long = size
        override val isDeleted: Boolean = false
        override val isHidden: Boolean = false
        override val lastModified: Instant = Instant.now()
        override val path: String = path
    }
}
