package com.github.nullptroma.wallenc.domain.vault.storages.encrypt

import com.fasterxml.jackson.module.kotlin.readValue
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.vault.storages.common.StorageSyncJournalBuffer
import com.github.nullptroma.wallenc.domain.vault.storages.common.StorageSyncJournalCodec
import com.github.nullptroma.wallenc.domain.vault.storages.common.readSystemFileBytesOrEmpty
import com.github.nullptroma.wallenc.domain.vault.utils.CloseHandledStreamExtension.Companion.onClosed
import com.github.nullptroma.wallenc.domain.vault.utils.CloseHandledStreamExtension.Companion.onClosing
import com.github.nullptroma.wallenc.domain.common.impl.CommonDirectory
import com.github.nullptroma.wallenc.domain.common.impl.CommonFile
import com.github.nullptroma.wallenc.domain.common.impl.CommonMetaInfo
import com.github.nullptroma.wallenc.domain.datatypes.DataPage
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
import com.github.nullptroma.wallenc.domain.encrypt.EncryptorWithStaticIv
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 com.github.nullptroma.wallenc.domain.datatypes.StorageSyncJournal
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncPaths
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncLock
import com.github.nullptroma.wallenc.domain.datatypes.StorageSyncOperation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.runBlocking
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.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.OutputStream
import java.time.Instant
import java.util.UUID
import kotlin.io.path.Path
import kotlin.io.path.pathString

class EncryptedStorageAccessor(
    private val source: IStorageAccessor,
    pathIv: ByteArray?,
    key: EncryptKey,
    private val systemHiddenDirName: String,
    private val scope: CoroutineScope
) : IStorageAccessor, DisposableHandle {
    private val syncActorId = UUID.randomUUID().toString()
    private val syncLockMutex = Mutex()
    private val journalBuffer = StorageSyncJournalBuffer(
        syncActorId = syncActorId,
        originStorageUuid = null,
        readJournal = { readSyncJournalUnchecked() },
        writeJournal = { writeSyncJournal(it) },
    )
    private val _size = MutableStateFlow<Long?>(null)
    override val size: StateFlow<Long?> = _size

    private val _numberOfFiles = MutableStateFlow<Int?>(null)
    override val numberOfFiles: StateFlow<Int?> = _numberOfFiles

    override val isAvailable: StateFlow<Boolean> = source.isAvailable

    private val _filesUpdates = MutableSharedFlow<DataPage<IFile>>()
    override val filesUpdates: SharedFlow<DataPage<IFile>> = _filesUpdates

    private val _dirsUpdates = MutableSharedFlow<DataPage<IDirectory>>()
    override val dirsUpdates: SharedFlow<DataPage<IDirectory>> = _dirsUpdates

    private val dataEncryptor = Encryptor(key.toAesKey())
    private val pathEncryptor: EncryptorWithStaticIv? = if(pathIv != null) EncryptorWithStaticIv(key.toAesKey(), pathIv) else null

    private var systemHiddenFilesIsActual = false

    init {
        collectSourceState()
    }
    private fun collectSourceState() {
        scope.launch {
            launch {
                try {
                    source.filesUpdates.collect { page ->
                        val files = page.data.map(::decryptEntity).filterSystemHiddenFiles()
                        _filesUpdates.emit(
                            DataPage(
                                list = files,
                                isLoading = page.isLoading,
                                isError = page.isError,
                                hasNext = page.hasNext,
                                pageLength = page.pageLength,
                                pageIndex = page.pageIndex,
                            ),
                        )
                    }
                } catch (_: Exception) {
                }
            }

            launch {
                try {
                    source.dirsUpdates.collect { page ->
                        val dirs = page.data.map(::decryptEntity).filterSystemHiddenDirs()
                        _dirsUpdates.emit(
                            DataPage(
                                list = dirs,
                                isLoading = page.isLoading,
                                isError = page.isError,
                                hasNext = page.hasNext,
                                pageLength = page.pageLength,
                                pageIndex = page.pageIndex,
                            ),
                        )
                    }
                } catch (_: Exception) {
                }
            }

            launch {
                source.numberOfFiles.collect {
                    if (it == null) {
                        _numberOfFiles.value = null
                    } else {
                        val hiddenCount = runCatching { getSystemFiles().size }.getOrNull() ?: return@collect
                        _numberOfFiles.value = it - hiddenCount
                    }
                }
            }

            launch {
                source.size.collect { sourceSize ->
                    if (sourceSize == null) {
                        _size.value = null
                    } else {
                        val hiddenBytes = runCatching {
                            getSystemFiles().sumOf { file -> file.metaInfo.size }
                        }.getOrNull() ?: return@collect
                        _size.value = sourceSize - hiddenBytes
                    }
                }
            }
        }
    }

    private suspend fun getSystemFiles(): List<IFile> {
        return source.getFiles(encryptPath(systemHiddenDirName))
    }

    private fun decryptEntity(file: IFile): IFile {
        return CommonFile(decryptMeta(file.metaInfo))
    }

    private fun decryptEntity(dir: IDirectory): IDirectory {
        return CommonDirectory(decryptMeta(dir.metaInfo), dir.elementsCount)
    }

    private fun encryptMeta(meta: IMetaInfo): CommonMetaInfo {
        return CommonMetaInfo(
            size = meta.size,
            isDeleted = meta.isDeleted,
            isHidden = meta.isHidden,
            lastModified = meta.lastModified,
            path = encryptPath(meta.path)
        )
    }

    private fun decryptMeta(meta: IMetaInfo): CommonMetaInfo {
        return CommonMetaInfo(
            size = meta.size,
            isDeleted = meta.isDeleted,
            isHidden = meta.isHidden,
            lastModified = meta.lastModified,
            path = decryptPath(meta.path)
        )
    }

    private fun encryptPath(pathStr: String): String {
        if(pathEncryptor == null)
            return pathStr
        val path = Path(pathStr)
        val segments = mutableListOf<String>()
        for (segment in path)
            segments.add(pathEncryptor.encryptString(segment.pathString))
        val res = Path("/", *(segments.toTypedArray()))
        return res.pathString
    }

    private fun decryptPath(pathStr: String): String {
        if(pathEncryptor == null)
            return pathStr

        val path = Path(pathStr)
        val segments = mutableListOf<String>()
        for (segment in path)
            segments.add(pathEncryptor.decryptString(segment.pathString))
        val res = Path("/", *(segments.toTypedArray()))
        return res.pathString
    }

    override suspend fun getAllFiles(): List<IFile> {
        return source.getAllFiles().map(::decryptEntity).filterSystemHiddenFiles()
    }

    override suspend fun getFiles(path: String): List<IFile> {
        return source.getFiles(encryptPath(path)).map(::decryptEntity).filterSystemHiddenFiles()
    }

    override fun getFilesFlow(path: String): Flow<DataPage<IFile>> {
        val flow = source.getFilesFlow(encryptPath(path)).map { page ->
            DataPage(
                list = page.data.map(::decryptEntity).filterSystemHiddenFiles(),
                isLoading = page.isLoading,
                isError = page.isError,
                hasNext = page.hasNext,
                pageLength = page.pageLength,
                pageIndex = page.pageIndex,
            )
        }
        return flow
    }

    override suspend fun getAllDirs(): List<IDirectory> {
        return source.getAllDirs().map(::decryptEntity).filterSystemHiddenDirs()
    }

    override suspend fun getDirs(path: String): List<IDirectory> {
        return source.getDirs(encryptPath(path)).map(::decryptEntity).filterSystemHiddenDirs()
    }

    override fun getDirsFlow(path: String): Flow<DataPage<IDirectory>> {
        val flow = source.getDirsFlow(encryptPath(path)).map { page ->
            DataPage(
                // включать все папки, кроме системной
                list = page.data.map(::decryptEntity).filterSystemHiddenDirs(),
                isLoading = page.isLoading,
                isError = page.isError,
                hasNext = page.hasNext,
                pageLength = page.pageLength,
                pageIndex = page.pageIndex,
            )
        }
        return flow
    }

    override suspend fun getFileInfo(path: String): IFile {
        val file = source.getFileInfo(encryptPath(path))
        val meta = decryptMeta(file.metaInfo)
        return CommonFile(meta)
    }

    override suspend fun getDirInfo(path: String): IDirectory {
        val dir = source.getDirInfo(encryptPath(path))
        val meta = decryptMeta(dir.metaInfo)
        return CommonDirectory(meta, dir.elementsCount)
    }

    override suspend fun setHidden(path: String, hidden: Boolean) {
        source.setHidden(encryptPath(path), hidden)
    }

    override suspend fun touchFile(path: String) {
        source.touchFile(encryptPath(path))
        appendSyncEntry(path = path, operation = StorageSyncOperation.UPSERT)
    }

    override suspend fun touchDir(path: String) {
        source.touchDir(encryptPath(path))
    }

    override suspend fun delete(path: String, recordSyncJournal: Boolean) {
        source.delete(encryptPath(path), recordSyncJournal = false)
        if (recordSyncJournal) {
            appendSyncEntry(path = path, operation = StorageSyncOperation.DELETE)
        }
    }

    override suspend fun openWrite(path: String, recordSyncJournal: Boolean): OutputStream =
        openWriteInternal(path, recordJournal = recordSyncJournal)

    override suspend fun openRead(path: String): InputStream {
        val stream = source.openRead(encryptPath(path))
        return dataEncryptor.decryptStream(stream)
    }

    override suspend fun moveToTrash(path: String, recordSyncJournal: Boolean) {
        source.moveToTrash(encryptPath(path), recordSyncJournal = false)
        if (recordSyncJournal) {
            appendSyncEntry(path = path, operation = StorageSyncOperation.TRASH)
        }
    }

    override fun dispose() {
        runBlocking {
            runCatching { journalBuffer.flushPending() }
        }
        dataEncryptor.dispose()
    }

    override suspend fun openReadSystemFile(name: String): InputStream = scope.run {
        val path = Path(systemHiddenDirName, name).pathString
        try {
            openRead(path)
        } catch (_: WallencException.Storage.FileNotFound) {
            ByteArrayInputStream(ByteArray(0))
        }
    }

    override suspend fun openWriteSystemFile(name: String): OutputStream = scope.run {
        val path = Path(systemHiddenDirName, name).pathString
        systemHiddenFilesIsActual = false
        return@run openWriteInternal(path, recordJournal = false).onClosing {
            systemHiddenFilesIsActual = false
        }
    }

    override suspend fun readSyncJournal(): StorageSyncJournal {
        journalBuffer.flushPending()
        return readSyncJournalUnchecked()
    }

    override suspend fun flushPendingSyncJournal() {
        journalBuffer.flushPending()
    }

    override suspend fun putSyncJournalEntries(entries: StorageSyncJournal) {
        journalBuffer.putEntries(entries)
    }

    private suspend fun readSyncJournalUnchecked(): StorageSyncJournal {
        val bytes = readSystemFileBytesOrEmpty { openReadSystemFile(SYNC_JOURNAL_FILENAME) }
        return StorageSyncJournalCodec.read(jackson, bytes)
    }

    private suspend fun writeSyncJournal(journal: StorageSyncJournal) {
        openWriteSystemFile(SYNC_JOURNAL_FILENAME).use { out ->
            jackson.writeValue(out, journal)
        }
    }

    override suspend fun readSyncLock(): StorageSyncLock? {
        val bytes = readSystemFileBytesOrEmpty { openReadSystemFile(SYNC_LOCK_FILENAME) }
        if (bytes.isEmpty()) {
            return null
        }
        return runCatching {
            jackson.readValue(bytes, StorageSyncLock::class.java)
        }.getOrNull()
    }

    override suspend fun tryAcquireSyncLock(holderId: String, leaseUntil: Instant): Boolean {
        return syncLockMutex.withLock {
            val current = readSyncLock()
            val now = Instant.now()
            val foreignLockActive = current != null &&
                current.holderId != holderId &&
                current.leaseUntil.isAfter(now) &&
                current.updatedAt.plusSeconds(SYNC_LOCK_STALE_TIMEOUT_SECONDS).isAfter(now)
            if (foreignLockActive) {
                return@withLock false
            }
            val next = StorageSyncLock(
                holderId = holderId,
                leaseUntil = leaseUntil,
                updatedAt = now,
            )
            openWriteSystemFile(SYNC_LOCK_FILENAME).use { out ->
                jackson.writeValue(out, next)
            }
            readSyncLock()?.holderId == holderId
        }
    }

    override suspend fun releaseSyncLock(holderId: String) {
        syncLockMutex.withLock {
            val current = readSyncLock() ?: return@withLock
            if (current.holderId != holderId) {
                return@withLock
            }
            openWriteSystemFile(SYNC_LOCK_FILENAME).use { out ->
                out.write(ByteArray(0))
            }
        }
    }

    override suspend fun forceClearSyncLock() {
        syncLockMutex.withLock {
            openWriteSystemFile(SYNC_LOCK_FILENAME).use { out ->
                out.write(ByteArray(0))
            }
        }
    }

    private suspend fun appendSyncEntry(path: String, operation: StorageSyncOperation) {
        val cleanedPath = StorageSyncPaths.normalize(path)
        if (!StorageSyncPaths.isSyncableUserPath(cleanedPath)) {
            return
        }
        val sequence = journalBuffer.nextSequence()
        val entry = journalBuffer.buildEntry(cleanedPath, operation, sequence)
        journalBuffer.appendEntry(cleanedPath, entry)
    }

    private suspend fun openWriteInternal(path: String, recordJournal: Boolean): OutputStream {
        val stream = source.openWrite(encryptPath(path))
        val encrypted = dataEncryptor.encryptStream(stream)
        if (!recordJournal) {
            return encrypted
        }
        return encrypted.onClosed {
            scope.launch {
                appendSyncEntry(path = path, operation = StorageSyncOperation.UPSERT)
            }
        }
    }

    private fun Iterable<IFile>.filterSystemHiddenFiles(): List<IFile> {
        return this.filter { file ->
            !file.metaInfo.path.contains(
                systemHiddenDirName
            )
        }
    }

    private fun Iterable<IDirectory>.filterSystemHiddenDirs(): List<IDirectory> {
        return this.filter { dir ->
            !dir.metaInfo.path.contains(
                systemHiddenDirName
            )
        }
    }

    private companion object {
        private const val SYNC_JOURNAL_FILENAME = "sync-journal.json"
        private const val SYNC_LOCK_FILENAME = "sync-lock.json"
        private const val SYNC_LOCK_STALE_TIMEOUT_SECONDS: Long = 60 * 60
        private val jackson = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper()
            .apply { findAndRegisterModules() }
    }
}