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

import com.pixelized.server.lwa.services.item.ItemStore
import com.pixelized.server.lwa.server.exception.BusinessException
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.model.item.Item
import com.pixelized.shared.lwa.protocol.rest.ApiPurseJson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import java.util.UUID

class InventoryService(
    private val inventoryStore: InventoryStore,
    private val itemStore: ItemStore,
    private val factory: InventoryJsonFactory,
) {
    private val scope = CoroutineScope(Dispatchers.IO + Job())

    private val inventoryJson = inventoryStore.inventoryFlow()
        .map { it.mapValues { entry -> factory.convertToJson(inventory = entry.value) } }
        .stateIn(
            scope = scope,
            started = SharingStarted.Eagerly,
            initialValue = emptyMap()
        )

    fun inventory(characterSheetId: String): Inventory {
        return inventoryStore.inventoryFlow().value[characterSheetId]
            ?: Inventory.empty(characterSheetId = characterSheetId)
    }

    fun inventoryJson(characterSheetId: String): InventoryJson {
        return inventoryJson.value[characterSheetId]
            ?: factory.convertToJson(Inventory.empty(characterSheetId = characterSheetId))
    }

    @Throws
    suspend fun updatePurse(
        purse: ApiPurseJson,
    ) {
        val inventory = inventory(
            characterSheetId = purse.characterSheetId
        )
        inventoryStore.save(
            inventory = inventory.copy(
                purse = inventory.purse.copy(
                    gold = inventory.purse.gold + purse.gold,
                    silver = inventory.purse.silver + purse.silver,
                    copper = inventory.purse.copper + purse.copper,
                ),
            ),
            create = false,
        )
    }

    @Throws
    suspend fun save(
        inventoryJson: InventoryJson,
        create: Boolean,
    ) {
        inventoryStore.save(
            inventory = factory.convertFromJson(json = inventoryJson),
            create = create,
        )
    }

    @Throws
    suspend fun delete(characterSheetId: String) {
        inventoryStore.delete(characterSheetId = characterSheetId)
    }

    @Throws
    suspend fun createInventoryItem(
        characterSheetId: String,
        itemId: String,
        count: Float,
    ): String {
        // get the inventory of the character, if none create one.
        val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
            ?: Inventory.empty(characterSheetId = characterSheetId)
        // create an inventoryId.
        val inventoryId = inventory.items.createInventoryId()
        // create an inventory Item.
        val item = Inventory.Item(
            inventoryId = inventoryId,
            itemId = itemId,
            count = count,
            equipped = false,
        )
        // update the inventory with the updated item.
        val updatedInventory = inventory.copy(
            items = inventory.items.toMutableList().also {
                it.add(item)
            }
        )
        // save the inventory
        inventoryStore.save(
            inventory = updatedInventory,
            create = false,
        )
        return inventoryId
    }

    @Throws
    suspend fun getInventoryItem(
        characterSheetId: String,
        inventoryId: String,
    ): Inventory.Item {
        // get the inventory of the character, if none create one.
        val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
            ?: Inventory.empty(characterSheetId = characterSheetId)
        // Guard case.
        return inventory.items
            .firstOrNull { it.inventoryId == inventoryId }
            ?: throw BusinessException(
                message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
            )
    }

    @Throws
    suspend fun getItem(
        itemId: String,
    ): Item {
        return itemStore.item(itemId = itemId)
            ?: throw BusinessException(message = "Item (id:$itemId) not found.")
    }

    @Throws
    suspend fun changeInventoryItemCount(
        characterSheetId: String,
        inventoryId: String,
        count: Float,
    ) {
        if (count < 0) {
            throw BusinessException(
                message = "InventoryItem (id:$inventoryId) quantity cannot go below 0.",
            )
        }
        // get the inventory of the character, if none create one.
        val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
            ?: Inventory.empty(characterSheetId = characterSheetId)
        // Guard case.
        val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
        if (inventoryItemIndex < 0) {
            throw BusinessException(
                message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
            )
        }
        // update the inventory with the updated item.
        val updatedInventory = inventory.copy(
            items = inventory.items.toMutableList().also { items ->
                if (count == 0f) {
                    items.removeAt(inventoryItemIndex)
                } else {
                    items[inventoryItemIndex] = items[inventoryItemIndex].copy(count = count)
                }
            }
        )
        // save the inventory
        inventoryStore.save(
            inventory = updatedInventory,
            create = false,
        )
    }

    @Throws
    suspend fun consumeInventoryItem(
        characterSheetId: String,
        inventoryId: String,
    ) {
        // get the inventory of the character, if none create one.
        val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
            ?: Inventory.empty(characterSheetId = characterSheetId)
        // Guard case.
        val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
        if (inventoryItemIndex < 0) {
            throw BusinessException(
                message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
            )
        }
        // other Guard case.
        val itemId = inventory.items[inventoryItemIndex].itemId
        val item = itemStore.item(itemId = itemId)
            ?: throw BusinessException(message = "Item (id:$itemId) not found.")
        if (item.options.consumable.not()) {
            throw BusinessException(message = "Item (id:$itemId) is not consumable.")
        }
        // update the inventory with the updated item.
        val updatedInventory = inventory.copy(
            items = inventory.items.toMutableList().also { items ->
                val inventoryItem = inventory.items[inventoryItemIndex]
                if (inventoryItem.count - 1f <= 0f) {
                    items.removeAt(inventoryItemIndex)
                } else {
                    items[inventoryItemIndex] = inventoryItem.copy(count = inventoryItem.count - 1f)
                }
            }
        )
        // save the inventory
        inventoryStore.save(
            inventory = updatedInventory,
            create = false,
        )
    }

    @Throws
    suspend fun equipInventoryItem(
        characterSheetId: String,
        inventoryId: String,
        equip: Boolean,
    ) {
        // get the inventory of the character, if none create one.
        val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
            ?: Inventory.empty(characterSheetId = characterSheetId)
        // Guard case.
        val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
        if (inventoryItemIndex < 0) {
            throw BusinessException(
                message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId).",
            )
        }
        // other Guard case.
        val itemId = inventory.items[inventoryItemIndex].itemId
        val item = itemStore.item(itemId = itemId)
            ?: throw BusinessException(message = "Item (id:$itemId) not found.")
        if (item.options.equipable.not()) {
            throw BusinessException(message = "Item (id:$itemId) is not equipable.")
        }
        // update the inventory with the updated item.
        val updatedInventory = inventory.copy(
            items = inventory.items.toMutableList().also { items ->
                items[inventoryItemIndex] = inventory.items[inventoryItemIndex].copy(
                    equipped = equip
                )
            }
        )
        // save the inventory
        inventoryStore.save(
            inventory = updatedInventory,
            create = false,
        )
    }

    @Throws
    suspend fun deleteInventoryItem(
        characterSheetId: String,
        inventoryId: String,
    ) {
        // get the inventory of the character, if none create one.
        val inventory = inventoryStore.inventoryFlow().value[characterSheetId]
            ?: Inventory.empty(characterSheetId = characterSheetId)
        // Guard case.
        val inventoryItemIndex = inventory.items.indexOfFirst { it.inventoryId == inventoryId }
        if (inventoryItemIndex < 0) {
            throw BusinessException(
                message = "InventoryItem (id:$inventoryId) not found in Inventory(characterSheetId:$characterSheetId)",
            )
        }
        // update the inventory with the updated item.
        val updatedInventory = inventory.copy(
            items = inventory.items.toMutableList().also { items ->
                items.removeAt(inventoryItemIndex)
            }
        )
        // save the inventory
        inventoryStore.save(
            inventory = updatedInventory,
            create = false,
        )
    }

    private fun List<Inventory.Item>.createInventoryId(): String {
        var inventoryId: String
        do {
            inventoryId = UUID.randomUUID().toString()
        } while (any { inventoryId == it.inventoryId })
        return inventoryId
    }
}
