package com.pixelized.server.lwa.services.inventory

import com.pixelized.server.lwa.server.exception.BusinessException
import com.pixelized.server.lwa.server.exception.FileReadException
import com.pixelized.server.lwa.server.exception.FileWriteException
import com.pixelized.server.lwa.server.exception.JsonCodingException
import com.pixelized.server.lwa.server.exception.JsonConversionException
import com.pixelized.shared.lwa.model.inventory.Inventory
import com.pixelized.shared.lwa.model.inventory.InventoryJson
import com.pixelized.shared.lwa.model.inventory.factory.InventoryJsonFactory
import com.pixelized.shared.lwa.protocol.rest.APIResponse
import com.pixelized.shared.lwa.utils.PathProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.io.File

class InventoryStore(
    private val pathProvider: PathProvider,
    private val factory: InventoryJsonFactory,
    private val jsonSerializer: Json,
    scope: CoroutineScope,
) {
    private val directory = File(pathProvider.inventoryPath()).also { it.mkdirs() }
    private val inventoryFlow = MutableStateFlow<Map<String, Inventory>>(value = emptyMap())

    init {
        // load the initial data
        scope.launch {
            updateInventoryFlow()
        }
    }

    fun inventoryFlow(): StateFlow<Map<String, Inventory>> = inventoryFlow

    suspend fun updateInventoryFlow() {
        inventoryFlow.value = try {
            load()
        } catch (exception: Exception) {
            println(exception.message) // TODO proper exception handling
            emptyMap()
        }
    }

    @Throws(
        FileReadException::class,
        JsonCodingException::class,
        JsonConversionException::class,
    )
    private suspend fun load(
        directory: File = this.directory,
    ): Map<String, Inventory> {
        return withContext(Dispatchers.IO) {
            directory
                .listFiles()
                ?.mapNotNull { file ->
                    val json = try {
                        file.readText(charset = Charsets.UTF_8)
                    } catch (exception: Exception) {
                        throw FileReadException(root = exception)
                    }
                    // Guard, if the json is blank no character have been save, ignore this file.
                    if (json.isBlank()) {
                        return@mapNotNull null
                    }
                    // decode the file
                    val data = try {
                        jsonSerializer.decodeFromString<InventoryJson>(json)
                    } catch (exception: Exception) {
                        throw JsonCodingException(root = exception)
                    }
                    // parse the json string.
                    val inventory = try {
                        factory.convertFromJson(data)
                    } catch (exception: Exception) {
                        throw JsonConversionException(root = exception)
                    }
                    inventory.characterSheetId to inventory
                }
                ?.toMap()
                ?: emptyMap()
        }
    }

    @Throws(
        BusinessException::class,
        FileWriteException::class,
        JsonCodingException::class,
        JsonConversionException::class,
    )
    suspend fun save(
        inventory: Inventory,
        create: Boolean,
    ) {
        withContext(Dispatchers.IO) {
            val file = inventoryFile(
                id = inventory.characterSheetId
            )
            if (create && file.exists()) {
                throw BusinessException(message = "Inventory already exist, creation is impossible.")
            }
            val json = try {
                factory.convertToJson(inventory = inventory)
            } catch (exception: Exception) {
                throw JsonConversionException(root = exception)
            }
            val data = try {
                jsonSerializer.encodeToString(json)
            } catch (exception: Exception) {
                throw JsonCodingException(root = exception)
            }
            try {
                file.writeText(
                    text = data,
                    charset = Charsets.UTF_8,
                )
            } catch (exception: Exception) {
                throw FileWriteException(root = exception)
            }
            inventoryFlow.update { flow ->
                flow.toMutableMap().also { data ->
                    data[inventory.characterSheetId] = inventory
                }
            }
        }
    }

    @Throws(BusinessException::class)
    suspend fun delete(characterSheetId: String) {
        withContext(Dispatchers.IO) {
            val file = inventoryFile(
                id = characterSheetId,
            )
            if (file.exists().not()) {
                throw BusinessException(
                    message = "Inventory file with id:$characterSheetId doesn't not exist.",
                    code = APIResponse.ErrorCode.CharacterSheetId
                )
            }
            if (file.delete().not()) {
                throw BusinessException(
                    message = "Inventory file have not been deleted for unknown reason.",
                )
            }
            inventoryFlow.update { characters ->
                characters.toMutableMap().also { data -> data.remove(characterSheetId) }
            }
        }
    }

    private fun inventoryFile(id: String): File {
        return File("${pathProvider.inventoryPath()}${id}.json")
    }
}
