package com.github.nullptroma.wallenc.domain.vault.vaults.local

import com.github.nullptroma.wallenc.domain.errors.WallencException

import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.vault.storages.local.LocalStorage
import com.github.nullptroma.wallenc.vault.contract.DescribedVault
import com.github.nullptroma.wallenc.vault.contract.VaultDescriptor
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
import kotlin.io.path.Path
import kotlin.io.path.createDirectory
import kotlin.io.path.pathString

class LocalVault(
    private val ioDispatcher: CoroutineDispatcher,
    vaultRoot: File?,
) : DescribedVault {

    override val uuid: UUID = vaultRoot?.let { root ->
        root.mkdirs()
        readOrCreateVaultUuid(File(root, UUID_FILE_NAME))
    } ?: UUID.randomUUID()

    override val descriptor: VaultDescriptor = VaultDescriptor.LocalDevice(uuid)

    private val _storages = MutableStateFlow<List<IStorage>>(emptyList())
    override val storages: StateFlow<List<IStorage>> = _storages

    private val _storagesScanInProgress = MutableStateFlow(false)
    override val storagesScanInProgress: StateFlow<Boolean> = _storagesScanInProgress

    private val _isAvailable = MutableStateFlow(false)
    override val isAvailable: StateFlow<Boolean> = _isAvailable

    private val _totalSpace = MutableStateFlow<Long?>(null)
    override val totalSpace: StateFlow<Long?> = _totalSpace

    private val _availableSpace = MutableStateFlow<Long?>(null)
    override val availableSpace: StateFlow<Long?> = _availableSpace

    private val path = MutableStateFlow(vaultRoot)

    init {
        CoroutineScope(ioDispatcher).launch {
            _storagesScanInProgress.value = true
            try {
                _isAvailable.value = path.value != null
                if (path.value != null) {
                    readStorages()
                }
            } finally {
                _storagesScanInProgress.value = false
            }
        }
    }

    private suspend fun readStorages() {
        val path = path.value
        if (path == null || !_isAvailable.value) {
            throw WallencException.Storage.NotAvailable()
        }

        val dirs = path.listFiles()?.filter { it.isDirectory }
        if (dirs != null) {
            _storages.value = dirs.map {
                val storageUuid = UUID.fromString(it.name)
                LocalStorage(storageUuid, it.path, ioDispatcher).apply { init() }
            }
        }
    }

    override suspend fun createStorage(): LocalStorage = withContext(ioDispatcher) {
        val path = path.value
        if (path == null || !_isAvailable.value) {
            throw WallencException.Storage.NotAvailable()
        }

        val storageUuid = UUID.randomUUID()
        val next = Path(path.path, storageUuid.toString())
        next.createDirectory()
        val newStorage = LocalStorage(storageUuid, next.pathString, ioDispatcher)
        newStorage.init()
        _storages.value = _storages.value.toMutableList().apply {
            add(newStorage)
        }
        return@withContext newStorage
    }

    override suspend fun createStorage(
        enc: StorageEncryptionInfo,
    ): LocalStorage = withContext(ioDispatcher) {
        val storage = createStorage()
        storage.setEncInfo(enc)
        return@withContext storage
    }

    override suspend fun rescanStorages() = withContext(ioDispatcher) {
        _storagesScanInProgress.value = true
        try {
            if (_isAvailable.value) {
                readStorages()
            }
        } finally {
            _storagesScanInProgress.value = false
        }
    }

    override suspend fun remove(storage: IStorage) = withContext(ioDispatcher) {
        val path = path.value
        if (path == null || !_isAvailable.value) {
            throw WallencException.Storage.NotAvailable()
        }

        val curStorages = _storages.value.toMutableList()
        val index = curStorages.indexOfFirst { it.uuid == storage.uuid }
        if (index != -1) {
            val localStorage = curStorages[index] as LocalStorage
            curStorages.removeAt(index)
            _storages.value = curStorages
            File(localStorage.absolutePath).deleteRecursively()
        }
    }

    private companion object {
        const val UUID_FILE_NAME = ".uuid"

        private val uuidLock = Any()

        private fun readOrCreateVaultUuid(idFile: File): UUID = synchronized(uuidLock) {
            if (idFile.exists()) {
                idFile.bufferedReader().use { reader ->
                    val line = reader.readLine()?.trim()
                    if (!line.isNullOrEmpty()) {
                        runCatching { UUID.fromString(line) }.getOrNull()?.let { return@synchronized it }
                    }
                }
            }
            val generated = UUID.randomUUID()
            val parent = idFile.parentFile ?: throw IllegalStateException("No parent for $idFile")
            parent.mkdirs()
            val tmp = File.createTempFile("vault-uuid-", ".tmp", parent)
            try {
                FileOutputStream(tmp).use { fos ->
                    fos.write(generated.toString().toByteArray(Charsets.UTF_8))
                    fos.fd.sync()
                }
                if (!tmp.renameTo(idFile)) {
                    tmp.copyTo(idFile, overwrite = true)
                }
            } finally {
                if (tmp.exists()) {
                    tmp.delete()
                }
            }
            return@synchronized generated
        }
    }
}
