package de.krisenvorrat.server.repository

import de.krisenvorrat.server.db.Categories
import de.krisenvorrat.server.db.Inventories
import de.krisenvorrat.server.db.Items
import de.krisenvorrat.server.db.Locations
import de.krisenvorrat.server.db.Settings
import de.krisenvorrat.server.db.Users
import de.krisenvorrat.server.model.InventoryStatsDto
import de.krisenvorrat.server.model.InventoryWithUsersDto
import de.krisenvorrat.server.model.UserDto
import de.krisenvorrat.shared.model.CategoryDto
import de.krisenvorrat.shared.model.InventoryDto
import de.krisenvorrat.shared.model.ItemDto
import de.krisenvorrat.shared.model.LocationDto
import de.krisenvorrat.shared.model.SettingDto
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.doubleOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.longOrNull
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.max
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import java.util.UUID

internal class InventoryRepository {

    /**
     * Resolves the effective inventoryId for a given userId.
     * If the user exists in the DB, returns their assigned inventoryId.
     * Falls back to the userId itself for JWT-only users (e.g. test tokens).
     */
    fun getEffectiveInventoryId(userId: String): String = transaction {
        Users.selectAll()
            .where { Users.id eq userId }
            .map { it[Users.inventoryId] }
            .singleOrNull() ?: userId
    }

    fun createInventory(): String {
        val inventoryId = UUID.randomUUID().toString()
        transaction {
            Inventories.insert {
                it[id] = inventoryId
                it[createdAt] = System.currentTimeMillis()
            }
        }
        return inventoryId
    }

    fun inventoryExists(inventoryId: String): Boolean = transaction {
        Inventories.selectAll().where { Inventories.id eq inventoryId }.count() > 0
    }

    /**
     * Assigns a user to a different inventory. Returns the old inventoryId.
     */
    fun assignUserToInventory(userId: String, newInventoryId: String): String? = transaction {
        val oldInventoryId = Users.selectAll()
            .where { Users.id eq userId }
            .map { it[Users.inventoryId] }
            .singleOrNull()
        Users.update({ Users.id eq userId }) {
            it[Users.inventoryId] = newInventoryId
        }
        oldInventoryId
    }

    /**
     * Deletes an inventory if no users are assigned to it and it has no items.
     */
    fun cleanupOrphanedInventory(inventoryId: String) = transaction {
        val userCount = Users.selectAll()
            .where { Users.inventoryId eq inventoryId }
            .count()
        if (userCount > 0) return@transaction
        val hasItems = Items.selectAll()
            .where { Items.inventoryId eq inventoryId }
            .count() > 0
        if (hasItems) return@transaction
        Inventories.deleteWhere { Inventories.id eq inventoryId }
    }

    fun listInventoriesWithUsers(): List<InventoryWithUsersDto> = transaction {
        Inventories.selectAll().map { invRow ->
            val invId = invRow[Inventories.id]
            val users = Users.selectAll()
                .where { Users.inventoryId eq invId }
                .map {
                    UserDto(
                        id = it[Users.id],
                        username = it[Users.username],
                        createdAt = it[Users.createdAt],
                        isAdmin = it[Users.isAdmin],
                        inventoryId = invId
                    )
                }
            InventoryWithUsersDto(inventoryId = invId, users = users)
        }
    }

    fun saveInventory(inventoryId: String, inventory: InventoryDto) {
        transaction {
            Items.deleteWhere { Items.inventoryId eq inventoryId }
            Settings.deleteWhere { Settings.inventoryId eq inventoryId }
            Categories.deleteWhere { Categories.inventoryId eq inventoryId }
            Locations.deleteWhere { Locations.inventoryId eq inventoryId }

            for (category in inventory.categories) {
                Categories.insert {
                    it[id] = category.id
                    it[name] = category.name
                    it[Categories.inventoryId] = inventoryId
                }
            }

            for (location in inventory.locations) {
                Locations.insert {
                    it[id] = location.id
                    it[name] = location.name
                    it[Locations.inventoryId] = inventoryId
                }
            }

            for (item in inventory.items) {
                Items.insert {
                    it[id] = item.id
                    it[name] = item.name
                    it[categoryId] = item.categoryId
                    it[quantity] = item.quantity
                    it[unit] = item.unit
                    it[unitPrice] = item.unitPrice
                    it[kcalPerKg] = item.kcalPerKg
                    it[expiryDate] = item.expiryDate
                    it[locationId] = item.locationId
                    it[notes] = item.notes
                    it[lastUpdated] = item.lastUpdated
                    it[Items.inventoryId] = inventoryId
                }
            }

            for (setting in inventory.settings) {
                Settings.insert {
                    it[key] = setting.key
                    it[value] = setting.value
                    it[Settings.inventoryId] = inventoryId
                }
            }
        }
    }

    fun loadInventory(inventoryId: String): InventoryDto {
        return transaction {
            val categories = Categories.selectAll()
                .where { Categories.inventoryId eq inventoryId }
                .map { CategoryDto(id = it[Categories.id], name = it[Categories.name]) }

            val locations = Locations.selectAll()
                .where { Locations.inventoryId eq inventoryId }
                .map { LocationDto(id = it[Locations.id], name = it[Locations.name]) }

            val items = Items.selectAll()
                .where { Items.inventoryId eq inventoryId }
                .map {
                    ItemDto(
                        id = it[Items.id],
                        name = it[Items.name],
                        categoryId = it[Items.categoryId],
                        quantity = it[Items.quantity],
                        unit = it[Items.unit],
                        unitPrice = it[Items.unitPrice],
                        kcalPerKg = it[Items.kcalPerKg],
                        expiryDate = it[Items.expiryDate],
                        locationId = it[Items.locationId],
                        notes = it[Items.notes],
                        lastUpdated = it[Items.lastUpdated]
                    )
                }

            val settings = Settings.selectAll()
                .where { Settings.inventoryId eq inventoryId }
                .map { SettingDto(key = it[Settings.key], value = it[Settings.value]) }

            InventoryDto(categories = categories, locations = locations, items = items, settings = settings)
        }
    }

    fun patchItem(inventoryId: String, itemId: String, item: ItemDto): Boolean {
        return transaction {
            val updated = Items.update({
                (Items.id eq itemId) and (Items.inventoryId eq inventoryId)
            }) {
                it[name] = item.name
                it[categoryId] = item.categoryId
                it[quantity] = item.quantity
                it[unit] = item.unit
                it[unitPrice] = item.unitPrice
                it[kcalPerKg] = item.kcalPerKg
                it[expiryDate] = item.expiryDate
                it[locationId] = item.locationId
                it[notes] = item.notes
                it[lastUpdated] = item.lastUpdated
            }
            updated > 0
        }
    }

    fun patchItemPartial(inventoryId: String, itemId: String, fields: JsonObject): Boolean {
        val updatableKeys = setOf("name", "categoryId", "quantity", "unit", "unitPrice", "kcalPerKg", "expiryDate", "locationId", "notes", "lastUpdated")
        return transaction {
            val exists = Items.selectAll()
                .where { (Items.id eq itemId) and (Items.inventoryId eq inventoryId) }
                .count() > 0
            if (!exists) return@transaction false

            val hasUpdatableFields = fields.keys.any { it in updatableKeys }
            if (!hasUpdatableFields) return@transaction true

            Items.update({
                (Items.id eq itemId) and (Items.inventoryId eq inventoryId)
            }) { stmt ->
                if ("name" in fields) stmt[name] = fields["name"]!!.jsonPrimitive.content
                if ("categoryId" in fields) stmt[categoryId] = fields["categoryId"]!!.jsonPrimitive.intOrNull ?: 0
                if ("quantity" in fields) stmt[quantity] = fields["quantity"]!!.jsonPrimitive.doubleOrNull ?: 0.0
                if ("unit" in fields) stmt[unit] = fields["unit"]!!.jsonPrimitive.content
                if ("unitPrice" in fields) stmt[unitPrice] = fields["unitPrice"]!!.jsonPrimitive.doubleOrNull ?: 0.0
                if ("kcalPerKg" in fields) stmt[kcalPerKg] = fields["kcalPerKg"]!!.jsonPrimitive.intOrNull
                if ("expiryDate" in fields) stmt[expiryDate] = fields["expiryDate"]!!.jsonPrimitive.contentOrNull
                if ("locationId" in fields) stmt[locationId] = fields["locationId"]!!.jsonPrimitive.intOrNull ?: 0
                if ("notes" in fields) stmt[notes] = fields["notes"]!!.jsonPrimitive.content
                if ("lastUpdated" in fields) stmt[lastUpdated] = fields["lastUpdated"]!!.jsonPrimitive.longOrNull ?: System.currentTimeMillis()
            }
            true
        }
    }

    fun loadItem(inventoryId: String, itemId: String): ItemDto? {
        return transaction {
            Items.selectAll()
                .where { (Items.id eq itemId) and (Items.inventoryId eq inventoryId) }
                .map {
                    ItemDto(
                        id = it[Items.id],
                        name = it[Items.name],
                        categoryId = it[Items.categoryId],
                        quantity = it[Items.quantity],
                        unit = it[Items.unit],
                        unitPrice = it[Items.unitPrice],
                        kcalPerKg = it[Items.kcalPerKg],
                        expiryDate = it[Items.expiryDate],
                        locationId = it[Items.locationId],
                        notes = it[Items.notes],
                        lastUpdated = it[Items.lastUpdated]
                    )
                }
                .singleOrNull()
        }
    }

    fun deleteItem(inventoryId: String, itemId: String): Boolean {
        return transaction {
            val deleted = Items.deleteWhere {
                (Items.id eq itemId) and (Items.inventoryId eq inventoryId)
            }
            deleted > 0
        }
    }

    fun getAggregatedStats(): InventoryStatsDto {
        return transaction {
            val totalItems = Items.selectAll().count()
            val totalLocations = Locations.selectAll().count()
            val totalCategories = Categories.selectAll().count()

            val lastUpdatedMax = Items.lastUpdated.max()
            val lastUpdated = Items
                .select(lastUpdatedMax)
                .map { it[lastUpdatedMax] }
                .singleOrNull()

            val thirtyDaysAgo = System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000
            val recentTransactions = Items.selectAll()
                .where { Items.lastUpdated greaterEq thirtyDaysAgo }
                .count()

            InventoryStatsDto(
                totalItems = totalItems,
                totalLocations = totalLocations,
                totalCategories = totalCategories,
                lastUpdated = lastUpdated,
                recentTransactions = recentTransactions
            )
        }
    }
}
