package com.pixelized.server.lwa.services.map

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.map.MapJson
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 MapStore(
    private val pathProvider: PathProvider,
    private val jsonSerializer: Json,
    scope: CoroutineScope,
) {
    private val directory = File(pathProvider.mapPath()).also { it.mkdirs() }
    private val mapsFlow = MutableStateFlow<Map<String, MapJson>>(emptyMap())

    init {
        // make the file path.
        File(pathProvider.mapPath()).mkdirs()
        // load the initial data
        scope.launch {
            updateMapFlow()
        }
    }

    fun maps(): StateFlow<Map<String, MapJson>> = mapsFlow

    suspend fun updateMapFlow() {
        mapsFlow.value = try {
            load().associateBy { it.id }
        } catch (exception: Exception) {
            println(exception.message) // TODO proper exception handling
            emptyMap()
        }
    }

    private suspend fun load(
        directory: File = this.directory,
    ): List<MapJson> {
        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
                    try {
                        jsonSerializer.decodeFromString<MapJson>(json)
                    } catch (exception: Exception) {
                        throw JsonCodingException(root = exception)
                    }
                }
                ?: emptyList()
        }
    }

    @Throws(JsonConversionException::class, FileWriteException::class)
    suspend fun save(
        map: MapJson,
        create: Boolean,
    ) {
        val file = mapFile(id = map.id)
        // Guard case on update alteration
        if (create && file.exists()) {
            throw BusinessException(
                message = "Map already exist, creation is impossible.",
            )
        }
        // Encode the json into a string.
        val data = try {
            this.jsonSerializer.encodeToString(map)
        } catch (exception: Exception) {
            throw JsonConversionException(root = exception)
        }
        // write the map into a file
        try {
            file.writeText(
                text = data,
                charset = Charsets.UTF_8,
            )
        } catch (exception: Exception) {
            throw FileWriteException(root = exception)
        }
        // Update the dataflow.
        mapsFlow.update { maps ->
            maps.toMutableMap().also {
                it[map.id] = map
            }
        }
    }

    @Throws(BusinessException::class)
    fun delete(id: String) {
        val file = mapFile(id = id)
        // Guard case on the file existence.
        if (file.exists().not()) {
            throw BusinessException(
                message = "Map doesn't not exist, deletion is impossible.",
            )
        }
        // Guard case on the file deletion
        if (file.delete().not()) {
            throw BusinessException(
                message = "Map file have not been deleted for unknown reason.",
            )
        }
        // Update the data model with the deleted alteration.
        mapsFlow.update { maps ->
            maps.toMutableMap().also {
                it.remove(key = id)
            }
        }
    }

    private fun mapFile(id: String) = File("${pathProvider.mapPath()}$id.json")
}
