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

import com.github.nullptroma.wallenc.domain.errors.WallencException
import com.github.nullptroma.wallenc.domain.vault.model.StorageKeyMap
import com.github.nullptroma.wallenc.domain.vault.ports.StorageKeyMapStore
import com.github.nullptroma.wallenc.domain.vault.storages.encrypt.EncryptedStorage
import com.github.nullptroma.wallenc.domain.datatypes.EncryptKey
import com.github.nullptroma.wallenc.domain.encrypt.Encryptor
import com.github.nullptroma.wallenc.domain.interfaces.IStorage
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
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.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.UUID

class UnlockManager(
    private val keymapRepository: StorageKeyMapStore,
    private val ioDispatcher: CoroutineDispatcher,
    vaultsManager: IVaultsManager
) : IUnlockManager {
    private val _openedStorages = MutableStateFlow<Map<UUID, EncryptedStorage>>(emptyMap())
    override val openedStorages: StateFlow<Map<UUID, IStorage>>
        get() = _openedStorages
    private val mutex = Mutex()

    override fun getOpenedStorageKey(uuid: UUID): EncryptKey? {
        val opened = _openedStorages.value
        val direct = opened[uuid]
        return direct?.getKey()
    }

    init {
        CoroutineScope(ioDispatcher).launch {
            vaultsManager.allStorages.collect {
                mutex.withLock {
                    val allKeys = keymapRepository.getAll()
                    val keysToRemove = mutableListOf<StorageKeyMap>()
                    val allStorages = it.toMutableList()
                    val map = _openedStorages.value.toMutableMap()
                    while(allStorages.isNotEmpty()) {
                        val storage = allStorages[allStorages.size-1]
                        val key = allKeys.find { key -> key.sourceUuid == storage.uuid }
                        if(key == null) {
                            allStorages.removeAt(allStorages.size - 1)
                            continue
                        }
                        try {
                            val encStorage = createEncryptedStorage(storage, key.key, getDestUuid(storage.uuid))
                            map[storage.uuid] = encStorage
                            allStorages.removeAt(allStorages.size - 1)
                            allStorages.add(encStorage)
                        }
                        catch (e: WallencException.Storage.IncorrectKey) {
                            keysToRemove.add(key)
                            allStorages.removeAt(allStorages.size - 1)
                        }
                        catch (_: WallencException.Storage.EncInfoMissing) {
                            keysToRemove.add(key)
                            allStorages.removeAt(allStorages.size - 1)
                        }
                        catch (_: Exception) {
                            allStorages.removeAt(allStorages.size - 1)
                        }
                    }
                    keymapRepository.delete(*keysToRemove.toTypedArray()) // удалить мёртвые ключи
                    _openedStorages.value = map.toMap()
                }
            }
        }
    }

    private suspend fun createEncryptedStorage(storage: IStorage, key: EncryptKey, uuid: UUID): EncryptedStorage {
        return EncryptedStorage.create(
            source = storage,
            key = key,
            ioDispatcher = ioDispatcher,
            uuid = uuid
        )
    }

    private fun getDestUuid(sourceUuid: UUID): UUID {
        return uuid5(
            namespace = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8"), // URL namespace
            name = "$sourceUuid:open"
        )
    }

    private fun uuid5(namespace: UUID, name: String): UUID {
        val digest = MessageDigest.getInstance("SHA-1")
        val nsBytes = ByteBuffer.allocate(16)
            .putLong(namespace.mostSignificantBits)
            .putLong(namespace.leastSignificantBits)
            .array()
        digest.update(nsBytes)
        digest.update(name.toByteArray(Charsets.UTF_8))
        val hash = digest.digest()

        hash[6] = (hash[6].toInt() and 0x0f or 0x50).toByte() // version 5
        hash[8] = (hash[8].toInt() and 0x3f or 0x80).toByte() // RFC 4122 variant

        val bb = ByteBuffer.wrap(hash, 0, 16)
        return UUID(bb.long, bb.long)
    }

    override suspend fun rememberKey(storage: IStorage, key: EncryptKey) = withContext(ioDispatcher) {
        mutex.withLock {
            val encInfo = storage.metaInfo.value.encInfo ?: throw WallencException.Storage.EncInfoMissing()
            if (!Encryptor.checkKey(key, encInfo)) {
                throw WallencException.Storage.IncorrectKey()
            }
            keymapRepository.add(
                StorageKeyMap(
                    sourceUuid = storage.uuid,
                    key = key,
                ),
            )
        }
    }

    override suspend fun open(
        storage: IStorage,
        key: EncryptKey,
        rememberPassword: Boolean
    ): EncryptedStorage = withContext(ioDispatcher) {
        return@withContext mutex.withLock {
            val encInfo = storage.metaInfo.value.encInfo ?: throw WallencException.Storage.EncInfoMissing()
            if (!Encryptor.checkKey(key, encInfo))
                throw WallencException.Storage.IncorrectKey()

            val opened = _openedStorages.value.toMutableMap()
            val cur = opened[storage.uuid]
            if (cur != null)
                return@withLock cur

            val keymap = StorageKeyMap(
                sourceUuid = storage.uuid,
                key = key
            )
            val encStorage = createEncryptedStorage(storage, keymap.key, getDestUuid(storage.uuid))
            opened[storage.uuid] = encStorage
            _openedStorages.value = opened
            if (rememberPassword) {
                keymapRepository.add(keymap)
            }
            encStorage
        }
    }

    /**
     * Закрыть шифрование хранилища, закрывает рекурсивно, удаляя все ключи
     * @param uuid uuid исходного хранилища
     */
    override suspend fun close(uuid: UUID): Unit = withContext(ioDispatcher) {
        mutex.withLock {
            val opened = _openedStorages.value.toMutableMap()
            closeBySourceUuid(opened, uuid)
            _openedStorages.value = opened
        }
    }

    // Закрытие только по source-экземпляру.
    override suspend fun close(storage: IStorage) {
        val opened = _openedStorages.value
        if (opened.containsKey(storage.uuid)) {
            close(storage.uuid)
        }
    }

    private fun closeBySourceUuid(opened: MutableMap<UUID, EncryptedStorage>, sourceUuid: UUID) {
        val enc = opened[sourceUuid] ?: return
        val nestedSourceUuid = enc.uuid
        if (nestedSourceUuid != sourceUuid && opened.containsKey(nestedSourceUuid)) {
            closeBySourceUuid(opened, nestedSourceUuid)
        }
        opened.remove(sourceUuid)
        enc.dispose()
    }
}