package com.pixelized.server.lwa.services.character

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.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetJson
import com.pixelized.shared.lwa.model.characterSheet.factory.CharacterSheetJsonFactory
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
import java.text.Collator

class CharacterSheetStore(
    private val pathProvider: PathProvider,
    private val factory: CharacterSheetJsonFactory,
    private val jsonSerializer: Json,
    scope: CoroutineScope,
) {
    private val directory = File(pathProvider.characterStorePath()).also { it.mkdirs() }
    private val characterSheetsFlow = MutableStateFlow<List<CharacterSheet>>(value = emptyList())

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

    fun characterSheetsFlow(): StateFlow<List<CharacterSheet>> = characterSheetsFlow

    suspend fun updateCharacterFlow() {
        characterSheetsFlow.value = try {
            load()
        } catch (exception: Exception) {
            println(exception.message) // TODO proper exception handling
            emptyList()
        }
    }

    @Throws(
        FileReadException::class,
        JsonCodingException::class,
        JsonConversionException::class,
    )
    suspend fun load(
        directory: File = this.directory,
    ): List<CharacterSheet> {
        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<CharacterSheetJson>(json)
                    } catch (exception: Exception) {
                        throw JsonCodingException(root = exception)
                    }
                    // parse the json string.
                    try {
                        factory.convertFromJson(data)
                    } catch (exception: Exception) {
                        throw JsonConversionException(root = exception)
                    }
                }
                ?.sortedWith(compareBy(Collator.getInstance()) { it.name })
                ?: emptyList()
        }
    }

    @Throws(
        BusinessException::class,
        FileWriteException::class,
        JsonCodingException::class,
        JsonConversionException::class,
    )
    suspend fun save(
        sheet: CharacterSheet,
        create: Boolean,
    ) {
        withContext(Dispatchers.IO) {
            val file = characterSheetFile(id = sheet.id)
            // Guard case on update alteration
            if (create && file.exists()) {
                throw BusinessException(message = "Character already exist, creation is impossible.")
            }
            // Transform the json into the model.
            val json = try {
                factory.convertToJson(sheet = sheet)
            } catch (exception: Exception) {
                throw JsonConversionException(root = exception)
            }
            // Encode the json into a string.
            val data = try {
                jsonSerializer.encodeToString(json)
            } catch (exception: Exception) {
                throw JsonCodingException(root = exception)
            }
            // write the character file.
            try {
                file.writeText(
                    text = data,
                    charset = Charsets.UTF_8,
                )
            } catch (exception: Exception) {
                throw FileWriteException(root = exception)
            }
            // Update the dataflow.
            characterSheetsFlow.update { characters ->
                characters.toMutableList()
                    .also { data ->
                        val index = data.indexOfFirst { it.id == sheet.id }
                        if (index >= 0) {
                            data[index] = sheet
                        } else {
                            data.add(sheet)
                        }
                    }
                    .sortedWith(compareBy(Collator.getInstance()) {
                        it.name
                    })
            }
        }
    }

    @Throws(BusinessException::class)
    suspend fun delete(characterSheetId: String) {
        withContext(Dispatchers.IO) {
            val file = characterSheetFile(id = characterSheetId)
            // Guard case on the file existence.
            if (file.exists().not()) {
                throw BusinessException(
                    message = "Character file with id:$characterSheetId doesn't not exist.",
                    code = APIResponse.ErrorCode.CharacterSheetId
                )
            }
            // Guard case on the file deletion
            if (file.delete().not()) {
                throw BusinessException(
                    message = "Character file have not been deleted for unknown reason.",
                )
            }
            // Update the data model with the deleted character.
            characterSheetsFlow.update { characters ->
                characters.toMutableList()
                    .also { data -> data.removeIf { it.id == characterSheetId } }
                    .sortedWith(compareBy(Collator.getInstance()) { it.name })
            }
        }
    }

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