package com.github.nullptroma.wallenc.usecases

import com.github.nullptroma.wallenc.domain.datatypes.TextSecretEntryRecord
import com.github.nullptroma.wallenc.domain.datatypes.TextSecretRecord
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IStorageInfo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonPrimitive
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class ManageTextSecretsUseCase @Inject constructor() {
    private val mutex = Mutex()

    fun observe(storageInfo: IStorageInfo): Flow<List<TextSecretRecord>> {
        val storage = storageInfo as? IStorage ?: return flowOf(emptyList())
        return merge(
            flowOf(Unit),
            storage.accessor.filesUpdates
                .filter { page ->
                    page.data.any { file ->
                        domainFilePathEquals(file.metaInfo.path)
                    }
                }
                .map {},
        ).map {
            mutex.withLock { readAll(storage) }
        }.distinctUntilChanged()
    }

    suspend fun get(storageInfo: IStorageInfo, id: String): TextSecretRecord? = mutex.withLock {
        val storage = storageInfo as? IStorage ?: return@withLock null
        readAll(storage).firstOrNull { it.id == id }
    }

    suspend fun create(
        storageInfo: IStorageInfo,
        title: String,
        items: List<TextSecretEntryRecord>,
        id: String = UUID.randomUUID().toString(),
    ): TextSecretRecord =
        mutex.withLock {
            val storage = storageInfo as? IStorage ?: error("Storage is not writable")
            val next = TextSecretRecord(
                id = id,
                title = title.trim(),
                items = items.normalizeItems(),
            )
            val updated = readAll(storage).toMutableList().apply { add(next) }
            writeAll(storage, updated)
            next
        }

    suspend fun update(storageInfo: IStorageInfo, secret: TextSecretRecord): Boolean = mutex.withLock {
        val storage = storageInfo as? IStorage ?: return@withLock false
        val current = readAll(storage)
        val index = current.indexOfFirst { it.id == secret.id }
        if (index < 0) return@withLock false
        val updatedSecret = secret.copy(
            title = secret.title.trim(),
            items = secret.items.normalizeItems(),
        )
        val updated = current.toMutableList().apply { this[index] = updatedSecret }
        writeAll(storage, updated)
        true
    }

    suspend fun delete(storageInfo: IStorageInfo, id: String): Boolean = mutex.withLock {
        val storage = storageInfo as? IStorage ?: return@withLock false
        val current = readAll(storage)
        val updated = current.filterNot { it.id == id }
        if (updated.size == current.size) return@withLock false
        writeAll(storage, updated)
        true
    }

    private suspend fun readAll(storage: IStorage): List<TextSecretRecord> {
        return StorageDomainJsonIo.readArray(storage, StorageDomainDataFiles.TEXT_SECRETS_FILE)
            .mapNotNull { parseSecret(it) }
    }

    private suspend fun writeAll(storage: IStorage, records: List<TextSecretRecord>) {
        StorageDomainJsonIo.writeArray(
            storage = storage,
            fileName = StorageDomainDataFiles.TEXT_SECRETS_FILE,
            data = records.map { encodeSecret(it) },
        )
    }

    private fun parseSecret(element: JsonElement): TextSecretRecord? {
        val obj = element as? JsonObject ?: return null
        val id = obj["id"]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } ?: return null
        val title = obj["title"]?.jsonPrimitive?.contentOrNull ?: return null
        val itemsElement = obj["items"] ?: JsonArray(emptyList())
        val items = (itemsElement as? JsonArray)?.mapNotNull { parseItem(it) } ?: emptyList()
        return TextSecretRecord(
            id = id,
            title = title,
            items = items,
        )
    }

    private fun parseItem(element: JsonElement): TextSecretEntryRecord? {
        val obj = element as? JsonObject ?: return null
        val value = obj["value"]?.jsonPrimitive?.contentOrNull ?: return null
        val label = obj["label"]?.jsonPrimitive?.contentOrNull
        return TextSecretEntryRecord(
            label = label,
            value = value,
        )
    }

    private fun encodeSecret(record: TextSecretRecord): JsonElement = buildJsonObject {
        put("id", JsonPrimitive(record.id))
        put("title", JsonPrimitive(record.title))
        put(
            "items",
            buildJsonArray {
                record.items.forEach { item ->
                    add(
                        buildJsonObject {
                            item.label?.let { put("label", JsonPrimitive(it)) }
                            put("value", JsonPrimitive(item.value))
                        },
                    )
                }
            },
        )
    }

    private fun List<TextSecretEntryRecord>.normalizeItems(): List<TextSecretEntryRecord> =
        this.mapNotNull { item ->
            val value = item.value.trim()
            if (value.isBlank()) {
                null
            } else {
                TextSecretEntryRecord(
                    label = item.label?.trim().takeUnless { it.isNullOrBlank() },
                    value = value,
                )
            }
        }

    private fun domainFilePathEquals(path: String): Boolean =
        path.trimStart('/') == StorageDomainDataFiles.TEXT_SECRETS_FILE.trimStart('/')
}
