package com.pixelized.server.lwa.services.alteration

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.alteration.Alteration
import com.pixelized.shared.lwa.model.alteration.AlterationJson
import com.pixelized.shared.lwa.model.alteration.AlterationJsonFactory
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 AlterationStore(
    private val pathProvider: PathProvider,
    private val factory: AlterationJsonFactory,
    private val jsonSerializer: Json,
    scope: CoroutineScope,
) {
    private val directory = File(pathProvider.alterationsPath()).also { it.mkdirs() }

    private val alterationFlow = MutableStateFlow<List<Alteration>>(emptyList())

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

    fun alterationsFlow(): StateFlow<List<Alteration>> = alterationFlow

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

    @Throws(
        FileReadException::class,
        JsonCodingException::class,
        JsonConversionException::class,
    )
    private suspend fun load(
        directory: File = this.directory,
    ): List<Alteration> {
        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 alteration have been save, ignore this file.
                    if (json.isBlank()) {
                        return@mapNotNull null
                    }
                    // decode the file
                    val data = try {
                        jsonSerializer.decodeFromString<AlterationJson>(json)
                    } catch (exception: Exception) {
                        throw JsonCodingException(root = exception)
                    }
                    // parse the json string.
                    val alterations = try {
                        factory.convertFromJson(data)
                    } catch (exception: Exception) {
                        throw JsonConversionException(root = exception)
                    }
                    return@mapNotNull alterations
                }
                ?.sortedWith(compareBy(Collator.getInstance()) { it.metadata.name })
                ?: emptyList()
        }
    }

    @Throws(
        BusinessException::class,
        FileWriteException::class,
        JsonCodingException::class,
        JsonConversionException::class,
    )
    suspend fun save(
        json: AlterationJson,
        create: Boolean,
    ) {
        withContext(Dispatchers.IO) {
            val file = alterationFile(id = json.id)
            // Guard case on update alteration
            if (create && file.exists()) {
                throw BusinessException(
                    message = "Alteration already exist, creation is impossible.",
                )
            }
            // Transform the json into the model.
            val alteration = try {
                factory.convertFromJson(json)
            } catch (exception: Exception) {
                throw JsonConversionException(root = exception)
            }
            if (alteration.id.isEmpty()) {
                throw BusinessException(
                    message = "Alteration 'id' is a mandatory field.",
                    code = APIResponse.ErrorCode.AlterationId,
                )
            }
            if (alteration.metadata.name.isEmpty()) {
                throw BusinessException(
                    message = "Alteration 'name' is a mandatory field.",
                    code = APIResponse.ErrorCode.AlterationName,
                )
            }
            // Encode the json into a string.
            val data = try {
                jsonSerializer.encodeToString(json)
            } catch (exception: Exception) {
                throw JsonCodingException(root = exception)
            }
            // Write the alteration into a file.
            try {
                file.writeText(
                    text = data,
                    charset = Charsets.UTF_8,
                )
            } catch (exception: Exception) {
                throw FileWriteException(root = exception)
            }
            // Update the dataflow.
            alterationFlow.update { alterations ->
                val index = alterations.indexOfFirst { it.id == json.id }
                alterations.toMutableList()
                    .also {
                        if (index >= 0) {
                            it[index] = alteration
                        } else {
                            it.add(alteration)
                        }
                    }
                    .sortedWith(compareBy(Collator.getInstance()) {
                        it.metadata.name
                    })
            }
        }
    }

    @Throws(BusinessException::class)
    fun delete(id: String) {
        val file = alterationFile(id = id)
        // Guard case on the file existence.
        if (file.exists().not()) {
            throw BusinessException(
                message = "Alteration doesn't not exist, deletion is impossible.",
            )
        }
        // Guard case on the file deletion
        if (file.delete().not()) {
            throw BusinessException(
                message = "Alteration file have not been deleted for unknown reason.",
            )
        }
        // Update the data model with the deleted alteration.
        alterationFlow.update { alterations ->
            alterations.toMutableList()
                .also { alteration ->
                    alteration.removeIf { it.id == id }
                }
                .sortedWith(compareBy(Collator.getInstance()) {
                    it.metadata.name
                })
        }
    }

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