package com.github.nullptroma.wallenc.usecases

import com.github.nullptroma.wallenc.domain.datatypes.TwoFaTokenRecord
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.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
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 ManageTwoFaTokensUseCase @Inject constructor() {
    private val mutex = Mutex()

    fun observe(storageInfo: IStorageInfo): Flow<List<TwoFaTokenRecord>> {
        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): TwoFaTokenRecord? = mutex.withLock {
        val storage = storageInfo as? IStorage ?: return@withLock null
        readAll(storage).firstOrNull { it.id == id }
    }

    suspend fun create(
        storageInfo: IStorageInfo,
        issuer: String,
        account: String,
        secret: String,
        notes: String? = null,
        digits: Int = 6,
        periodSeconds: Int = 30,
        algorithm: String = "SHA1",
    ): TwoFaTokenRecord = mutex.withLock {
        val storage = storageInfo as? IStorage ?: error("Storage is not writable")
        val next = TwoFaTokenRecord(
            id = UUID.randomUUID().toString(),
            issuer = issuer.trim(),
            account = account.trim(),
            secret = secret.trim(),
            notes = notes?.trim().takeUnless { it.isNullOrBlank() },
            digits = digits.coerceIn(6, 8),
            periodSeconds = periodSeconds.coerceAtLeast(1),
            algorithm = normalizeAlgorithm(algorithm),
        )
        val updated = readAll(storage).toMutableList().apply { add(next) }
        writeAll(storage, updated)
        next
    }

    suspend fun update(storageInfo: IStorageInfo, token: TwoFaTokenRecord): Boolean = mutex.withLock {
        val storage = storageInfo as? IStorage ?: return@withLock false
        val updatedToken = token.copy(
            issuer = token.issuer.trim(),
            account = token.account.trim(),
            secret = token.secret.trim(),
            notes = token.notes?.trim().takeUnless { it.isNullOrBlank() },
        )
        val current = readAll(storage)
        val index = current.indexOfFirst { it.id == token.id }
        if (index < 0) return@withLock false
        val updated = current.toMutableList().apply { this[index] = updatedToken }
        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<TwoFaTokenRecord> {
        return StorageDomainJsonIo.readArray(storage, StorageDomainDataFiles.TWO_FA_TOKENS_FILE)
            .mapNotNull { parseToken(it) }
    }

    private suspend fun writeAll(storage: IStorage, records: List<TwoFaTokenRecord>) {
        StorageDomainJsonIo.writeArray(
            storage = storage,
            fileName = StorageDomainDataFiles.TWO_FA_TOKENS_FILE,
            data = records.map { record -> encodeToken(record) },
        )
    }

    private fun parseToken(element: JsonElement): TwoFaTokenRecord? {
        val obj = element as? JsonObject ?: return null
        val id = obj["id"]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } ?: return null
        val issuer = obj["issuer"]?.jsonPrimitive?.contentOrNull ?: return null
        val account = obj["account"]?.jsonPrimitive?.contentOrNull ?: return null
        val secret = obj["secret"]?.jsonPrimitive?.contentOrNull ?: return null
        val notes = obj["notes"]?.jsonPrimitive?.contentOrNull
        val digits = obj["digits"]?.jsonPrimitive?.contentOrNull?.toIntOrNull()?.coerceIn(6, 8) ?: 6
        val periodSeconds = obj["periodSeconds"]?.jsonPrimitive?.contentOrNull?.toIntOrNull()?.coerceAtLeast(1) ?: 30
        val algorithm = normalizeAlgorithm(obj["algorithm"]?.jsonPrimitive?.contentOrNull ?: "SHA1")
        return TwoFaTokenRecord(
            id = id,
            issuer = issuer,
            account = account,
            secret = secret,
            notes = notes,
            digits = digits,
            periodSeconds = periodSeconds,
            algorithm = algorithm,
        )
    }

    private fun encodeToken(record: TwoFaTokenRecord): JsonElement = buildJsonObject {
        put("id", JsonPrimitive(record.id))
        put("issuer", JsonPrimitive(record.issuer))
        put("account", JsonPrimitive(record.account))
        put("secret", JsonPrimitive(record.secret))
        record.notes?.let { put("notes", JsonPrimitive(it)) }
        put("digits", JsonPrimitive(record.digits))
        put("periodSeconds", JsonPrimitive(record.periodSeconds))
        put("algorithm", JsonPrimitive(normalizeAlgorithm(record.algorithm)))
    }

    private fun normalizeAlgorithm(algorithm: String): String {
        val normalized = algorithm
            .trim()
            .uppercase()
            .replace("HMAC", "")
            .replace("-", "")
            .replace("_", "")
        return when (normalized) {
            "SHA1", "SHA256", "SHA512" -> normalized
            else -> "SHA1"
        }
    }

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