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

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.github.nullptroma.wallenc.domain.common.impl.CommonStorageMetaInfo
import com.github.nullptroma.wallenc.domain.datatypes.StorageEncryptionInfo
import com.github.nullptroma.wallenc.domain.datatypes.StorageMetaLoadState
import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageAccessor
import com.github.nullptroma.wallenc.domain.interfaces.IStorageMetaInfo
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
import com.github.nullptroma.wallenc.domain.tasks.TaskProgressLabel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.util.UUID

/**
 * Общий «скелет» для [IStorage]: единая логика meta-info, rename, setEncInfo,
 * clearAllContent и делегирование размеров и доступности в [accessor].
 *
 * Подклассы определяют только способ создания [accessor], значение
 * [isVirtualStorage] и (при необходимости) расширяют [init] своими шагами
 * (например, проверкой ключа или инициализацией внешней связи).
 */
abstract class BaseStorage(
    override val uuid: UUID,
    private val ioDispatcher: CoroutineDispatcher,
    metaInfoFilePostfix: String,
) : IStorage {

    protected val metaInfoFileName: String = "${metaInfoUuidPart()}$metaInfoFilePostfix"

    private val _metaInfo = MutableStateFlow<IStorageMetaInfo>(CommonStorageMetaInfo())
    final override val metaInfo: StateFlow<IStorageMetaInfo>
        get() = _metaInfo

    private val _metaLoadState = MutableStateFlow(StorageMetaLoadState.Loading)
    final override val metaLoadState: StateFlow<StorageMetaLoadState>
        get() = _metaLoadState

    final override val size: StateFlow<Long?>
        get() = accessor.size

    final override val numberOfFiles: StateFlow<Int?>
        get() = accessor.numberOfFiles

    final override val isAvailable: StateFlow<Boolean>
        get() = accessor.isAvailable

    final override val isEmpty: Flow<Boolean?>
        get() = accessor.numberOfFiles.map { n -> n?.let { it == 0 } }

    abstract override val accessor: IStorageAccessor

    /**
     * Базовая реализация [IStorageAccessor] передаёт UUID полностью; подклассы
     * могут переопределить, чтобы сохранить совместимость с уже существующими
     * именами файлов (например, [com.github.nullptroma.wallenc.domain.vault.storages.encrypt.EncryptedStorage]
     * раньше использовал первые 8 символов).
     */
    protected open fun metaInfoUuidPart(): String = uuid.toString()

    /**
     * Запускается единожды при первом использовании хранилища. Подклассы могут переопределить,
     * добавив свои шаги (инициализацию [accessor], проверку ключа и т.п.). Обязательно
     * должен в какой-то момент вызвать [readMetaInfo].
     */
    open suspend fun init() {
        readMetaInfo()
    }

    private suspend fun readMetaInfo() = withContext(ioDispatcher) {
        val (meta, state) = loadMetaFromDisk()
        _metaInfo.value = meta
        _metaLoadState.value = state
    }

    private suspend fun loadMetaFromDisk(): Pair<IStorageMetaInfo, StorageMetaLoadState> {
        return try {
            val bytes = accessor.openReadSystemFile(metaInfoFileName).use { it.readBytes() }
            when {
                bytes.isEmpty() -> {
                    val default = CommonStorageMetaInfo()
                    updateMetaInfo(default)
                    default to StorageMetaLoadState.Ready
                }
                else -> try {
                    jackson.readValue(bytes, CommonStorageMetaInfo::class.java) to StorageMetaLoadState.Ready
                } catch (_: Exception) {
                    CommonStorageMetaInfo() to StorageMetaLoadState.Unavailable
                }
            }
        } catch (_: WallencException.Storage.FileNotFound) {
            val default = CommonStorageMetaInfo()
            updateMetaInfo(default)
            default to StorageMetaLoadState.Ready
        } catch (_: Exception) {
            CommonStorageMetaInfo() to StorageMetaLoadState.Unavailable
        }
    }

    private suspend fun requireMetaReady() {
        if (_metaLoadState.value != StorageMetaLoadState.Ready) {
            throw WallencException.Storage.NotAvailable()
        }
    }

    private suspend fun updateMetaInfo(meta: IStorageMetaInfo) = withContext(ioDispatcher) {
        val writer = accessor.openWriteSystemFile(metaInfoFileName)
        writer.use { writer ->
            jackson.writeValue(writer, meta)
        }
        _metaInfo.value = meta
    }

    final override suspend fun rename(newName: String) = withContext(ioDispatcher) {
        requireMetaReady()
        val cur = metaInfo.value
        updateMetaInfo(
            CommonStorageMetaInfo(
                encInfo = cur.encInfo,
                name = newName,
            ),
        )
    }

    final override suspend fun setEncInfo(encInfo: StorageEncryptionInfo?) = withContext(ioDispatcher) {
        requireMetaReady()
        val cur = metaInfo.value
        updateMetaInfo(
            CommonStorageMetaInfo(
                encInfo = encInfo,
                name = cur.name,
            ),
        )
    }

    final override suspend fun clearAllContent(onProgress: suspend (TaskProgress) -> Unit) = withContext(ioDispatcher) {
        val files = accessor.getAllFiles()
        val dirs = accessor.getAllDirs()
        val paths = buildList {
            addAll(files.map { it.metaInfo.path })
            addAll(dirs.map { it.metaInfo.path })
        }
            .filter { it != "/" && it.isNotBlank() }
            .sortedByDescending { it.length }
        val total = paths.size
        if (total == 0) {
            return@withContext
        }
        paths.forEachIndexed { index, path ->
            accessor.delete(path)
            if (index % PROGRESS_REPORT_INTERVAL == 0 || index == paths.lastIndex) {
                val done = index + 1
                onProgress(
                    TaskProgress(
                        fraction = done.toFloat() / total,
                        label = TaskProgressLabel.ClearContentProgress(done, total),
                    ),
                )
                coroutineContext.ensureActive()
            }
        }
    }

    companion object {
        private const val PROGRESS_REPORT_INTERVAL = 16
        private val jackson = jacksonObjectMapper().apply { findAndRegisterModules() }
    }
}
