package com.pixelized.server.lwa.services.item

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.item.Item
import com.pixelized.shared.lwa.model.item.ItemJson
import com.pixelized.shared.lwa.model.item.factory.ItemJsonFactory
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 ItemStore(
    private val pathProvider: PathProvider,
    private val factory: ItemJsonFactory,
    private val jsonSerializer: Json,
    scope: CoroutineScope,
) {
    private val directory = File(pathProvider.itemsPath()).also { it.mkdirs() }
    private val itemFlow = MutableStateFlow<List<Item>>(emptyList())

    init {
        scope.launch {
            updateItemsFlow()
        }
    }

    fun item(itemId: String?): Item? {
        return itemFlow.value.firstOrNull { it.id == itemId }
    }

    fun itemsFlow(): StateFlow<List<Item>> = itemFlow

    suspend fun updateItemsFlow() {
        itemFlow.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<Item> {
        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 item have been save, ignore this file.
                    if (json.isBlank()) {
                        return@mapNotNull null
                    }
                    // decode the file
                    val data = try {
                        jsonSerializer.decodeFromString<ItemJson>(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.metadata.label })
                ?: emptyList()
        }
    }

    @Throws(
        BusinessException::class,
        FileWriteException::class,
        JsonCodingException::class,
        JsonConversionException::class,
    )
    suspend fun save(
        json: ItemJson,
        create: Boolean,
    ) {
        withContext(Dispatchers.IO) {
            val file = itemFile(id = json.id)
            // Guard case on update alteration
            if (create && file.exists()) {
                throw BusinessException(
                    message = "Item already exist, creation is impossible.",
                )
            }
            // Transform the json into the model.
            val item = try {
                factory.convertFromJson(json)
            } catch (exception: Exception) {
                throw JsonConversionException(root = exception)
            }
            if (item.id.isEmpty()) {
                throw BusinessException(
                    message = "Item 'id' is a mandatory field.",
                    code = APIResponse.ErrorCode.ItemId,
                )
            }
            if (item.metadata.label.isEmpty()) {
                throw BusinessException(
                    message = "Item 'name' is a mandatory field.",
                    code = APIResponse.ErrorCode.ItemName,
                )
            }
            // 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.
            itemFlow.update { items ->
                val index = items.indexOfFirst { it.id == json.id }
                items.toMutableList()
                    .also {
                        if (index >= 0) {
                            it[index] = item
                        } else {
                            it.add(item)
                        }
                    }
                    .sortedWith(compareBy(Collator.getInstance()) {
                        it.metadata.label
                    })
            }
        }
    }

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

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