package theorycrafter

import androidx.compose.runtime.toMutableStateMap
import eve.data.EveItemType
import eve.data.ModuleSlotType
import eve.data.asIsk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import theorycrafter.esi.MarketApi
import theorycrafter.esi.getAllItemPrices
import theorycrafter.fitting.EveItem
import theorycrafter.fitting.Fit
import theorycrafter.fitting.maxLoadedChargeAmount
import theorycrafter.fitting.utils.mapEach
import theorycrafter.utils.data
import theorycrafter.utils.sendEsiRequestWithRetry
import theorycrafter.utils.timeAction
import java.io.File
import java.io.IOException
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds


/**
 * Encapsulates the prices of eve items.
 */
class EveItemPrices(
    private val priceByItemId: Map<Int, Double>,
    overrideByItemId: Map<Int, Double> = mutableMapOf(),
    private val onOverridesChanged: (Map<Int, Double>) -> Unit = {}
) {


    /**
     * The overrides, as a mutable state map.
     */
    private val overrideByItemId = overrideByItemId.toList().toMutableStateMap()


    /**
     * Returns the price of the given item; `null` if unknown.
     */
    operator fun get(itemType: EveItemType): Double? = 
        overrideByItemId[itemType.itemId] ?: priceByItemId[itemType.itemId]


    /**
     * Sets an override of the price of the given item.
     */
    fun setOverride(itemType: EveItemType, price: Double) {
        overrideByItemId[itemType.itemId] = price
        onOverridesChanged(overrideByItemId)
    }


    /**
     * Removes the override of the price of the given item.
     */
    fun removeOverride(itemType: EveItemType) {
        overrideByItemId.remove(itemType.itemId)
        onOverridesChanged(overrideByItemId)
    }


    /**
     * Returns whether the price for the given item is an override.
     */
    fun isOverriden(itemType: EveItemType): Boolean =
        itemType.itemId in overrideByItemId


    /**
     * Returns all the price overrides, mapping item ids to their prices.
     */
    val overrides: Map<Int, Double>
        get() = overrideByItemId


    /**
     * The price of an item; `null` if unknown.
     */
    val EveItemType.price: Double?
        get() = get(this)


    /**
     * Returns the price info of an item type.
     */
    fun EveItemType.priceInfo(): ItemsPriceInfo {
        val price = get(this)
        return ItemsPriceInfo(
            price = price ?: 0.0,
            priceIsUnknown = price == null,
            priceIsOverride = isOverriden(this)
        )
    }


    /**
     * Returns the price info of an item.
     */
    @Suppress("unused")
    fun EveItem<*>.priceInfo(): ItemsPriceInfo = type.priceInfo()


    /**
     * Returns the price of an item type.
     */
    fun priceInfoOf(itemType: EveItemType): ItemsPriceInfo = itemType.priceInfo()


    /**
     * Returns the price of an item or the given value if unknown.
     */
    fun priceInfoOf(item: EveItem<*>): ItemsPriceInfo = item.type.priceInfo()


    /**
     * Returns the price information for the total of all the given items.
     */
    fun priceInfoOf(items: Iterable<EveItemType?>): ItemsPriceInfo {
        var sum = 0.0
        var anyPriceIsUnknown = false
        var anyPriceIsOverride = false
        for (item in items) {
            if (item == null)
                continue
            val price = get(item)
            sum += price ?: 0.0
            anyPriceIsUnknown = anyPriceIsUnknown || (price == null)
            anyPriceIsOverride = anyPriceIsOverride || isOverriden(item)
        }
        return ItemsPriceInfo(
            price = sum,
            priceIsUnknown = anyPriceIsUnknown,
            priceIsOverride = anyPriceIsOverride
        )
    }


    /**
     * Returns the price information for the total of all the given items.
     */
    fun Iterable<EveItem<*>?>.priceInfo(): ItemsPriceInfo {
        return priceInfoOf(items = mapEach { it?.type })
    }


    /**
     * Returns the price information for the total of all the fitted modules and rigs.
     * Does not include charges.
     */
    fun Fit.Modules.priceInfo(): ItemsPriceInfo {
        var sum = 0.0
        var anyPriceIsUnknown = false
        var anyPriceIsOverride = false

        for (slotType in ModuleSlotType.entries) {
            val priceInfo = slotsInRack(slotType).priceInfo()
            sum += priceInfo.price
            anyPriceIsUnknown = anyPriceIsUnknown || priceInfo.priceIsUnknown
            anyPriceIsOverride = anyPriceIsOverride || priceInfo.priceIsOverride
        }

        return ItemsPriceInfo(
            price = sum,
            priceIsUnknown = anyPriceIsUnknown,
            priceIsOverride = anyPriceIsOverride
        )
    }


    /**
     * Returns the price information for the total of all the loaded charges.
     */
    fun Fit.Modules.chargesPrice(): ItemsPriceInfo {
        var sum = 0.0
        var anyPriceIsUnknown = false
        var anyPriceIsOverride = false

        for (slotType in ModuleSlotType.entries) {
            for (module in slotsInRack(slotType)) {
                val charge = module?.loadedCharge ?: continue
                val amount = maxLoadedChargeAmount(module.type, charge.type) ?: continue
                val priceInfo = charge.type.priceInfo()
                sum += priceInfo.price * amount
                anyPriceIsUnknown = anyPriceIsUnknown || priceInfo.priceIsUnknown
                anyPriceIsOverride = anyPriceIsOverride || priceInfo.priceIsOverride
            }
        }

        return ItemsPriceInfo(
            price = sum,
            priceIsUnknown = anyPriceIsUnknown,
            priceIsOverride = anyPriceIsOverride
        )
    }


    /**
     * Returns the price information for the total of the fitted subsystems; `null` if the fit's ship does not use
     * subsystems.
     */
    fun Fit.subsystemsPrice(): ItemsPriceInfo? {
        return subsystemByKind?.values?.priceInfo()
    }


    /**
     * Returns the price information for the total of the fitted drones.
     */
    fun Fit.Drones.priceInfo(): ItemsPriceInfo {
        return all.priceInfo()
    }


    /**
     * Returns the price information for the total of the fitted implants.
     */
    fun Fit.Implants.priceInfo(): ItemsPriceInfo {
        return slots.priceInfo()
    }


    /**
     * Returns the price information for the total of the fitted boosters.
     */
    fun Fit.Boosters.priceInfo(): ItemsPriceInfo {
        return slots.priceInfo()
    }


    /**
     * Returns the price information for the total of the items in the cargo hold.
     */
    fun Fit.Cargohold.priceInfo(): ItemsPriceInfo {
        return contents.priceInfo()
    }


    /**
     * Returns the price information for the total of the fit.
     */
    fun Fit.priceInfo(includeCharges: Boolean): ItemsPriceInfo {
        return ship.type.priceInfo() +
                subsystemsPrice() +
                modules.priceInfo() +
                (if (includeCharges) modules.chargesPrice() else null) +
                drones.priceInfo() +
                implants.priceInfo() +
                boosters.priceInfo() +
                cargohold.priceInfo()
    }


    /**
     * Returns the price of the fit.
     */
    fun Fit.price(includeCharges: Boolean): Double =
        priceInfo(includeCharges).price


}


/**
 * Information about a the price of an item or a collection of items.
 */
data class ItemsPriceInfo(

    /**
     * The price.
     */
    val price: Double,

    /**
     * Whether the real price is unknown. For collections, this is `true` if any price is unknown.
     */
    val priceIsUnknown: Boolean,

    /**
     * Whether the returned [price] is an override. For collections, this is `true` if any price is an override.
     */
    val priceIsOverride: Boolean,

) {

    /**
     * Returns the sum of the price and the given value.
     */
    operator fun plus(other: ItemsPriceInfo?): ItemsPriceInfo =
        if (other == null)
            this
        else
            ItemsPriceInfo(
                price = price + other.price,
                priceIsUnknown = priceIsUnknown || other.priceIsUnknown,
                priceIsOverride = priceIsOverride || other.priceIsOverride
            )


    /**
     * Returns the multiplication of the price by the given value.
     */
    operator fun times(multiplier: Int): ItemsPriceInfo =
        ItemsPriceInfo(
            price = price * multiplier,
            priceIsUnknown = priceIsUnknown,
            priceIsOverride = priceIsOverride
        )


    /**
     * Returns the suffix to append to a price in order to indicate whether price is unknown and/or an override.
     */
    val uncertaintySuffix: String =
        (if (priceIsUnknown) "*" else "") +
            (if (priceIsOverride) "†" else "")


    /**
     * Returns a string that can be displayed to the user to represent this price information.
     */
    fun toDisplayString(withIsk: Boolean = false): String = price.asIsk(withUnits = withIsk) + uncertaintySuffix


    /**
     * Returns the string representation of the price.
     */
    override fun toString(): String = toDisplayString()


}


/**
 * The symbold for unknown price.
 */
const val PriceUnknownSymbol = "*"


/**
 * The symbol for marking price overrides.
 */
const val PriceOverrideSymbol = "†"


/**
 * The time between price updates.
 */
private val PriceUpdatePeriod = 6.hours


/**
 * Fetches [EveItemPrices] every [PriceUpdatePeriod], keeping them up-to-date in [TheorycrafterContext.eveItemPrices].
 */
suspend fun keepEveItemPricesUpdated(scope: CoroutineScope) {
    var priceOverrides = readPriceOverrides()
    fun onOverridesChanged(overrides: Map<Int, Double>) {
        priceOverrides = overrides
        scope.launch {
            writePriceOverrides(overrides)
        }
    }

    val (cachedPrices, lastUpdateTime) = readCachedPricesAndLastUpdateTime()

    val now = System.currentTimeMillis()
    val updatePeriodMillis = PriceUpdatePeriod.inWholeMilliseconds
    if (cachedPrices != null) {
        println("Using cached item prices")
        TheorycrafterContext.eveItemPrices = EveItemPrices(
            priceByItemId = cachedPrices,
            overrideByItemId = priceOverrides,
            onOverridesChanged = ::onOverridesChanged
        )
    }

    val millisUntilNextUpdate = (updatePeriodMillis - (now - lastUpdateTime)).coerceIn(0..updatePeriodMillis)
    println("Time until next prices update: ${millisUntilNextUpdate.milliseconds}")
    if (cachedPrices != null)
        delay(millisUntilNextUpdate)
    while (true) {
        println("Updating item prices")
        val newPrices = fetchPrices()
        TheorycrafterContext.eveItemPrices = EveItemPrices(
            priceByItemId = newPrices,
            overrideByItemId = priceOverrides,
            onOverridesChanged = ::onOverridesChanged
        )
        writePricesCache(newPrices)
        delay(PriceUpdatePeriod)
    }
}


/**
 * Reads the price overrides from the file.
 */
private fun readPriceOverrides(): Map<Int, Double> {
    val file = File(USER_FILES_DIR, "price_overrides.dat")
    if (!file.exists())
        return emptyMap()

    return timeAction("Loading price overrides") {
        val eveData = TheorycrafterContext.eveData
        buildMap {
            file.inputStream().buffered().data().use { input ->
                val itemCount = input.readInt()
                repeat(itemCount) {
                    val itemId = input.readInt()
                    val price = input.readDouble()
                    if (eveData.pricedItemTypeOrNull(itemId) != null)
                        put(itemId, price)
                }
            }
        }
    }
}


/**
 * Writes the given price overrides to file.
 */
private fun writePriceOverrides(overrides: Map<Int, Double>) {
    val file = File(USER_FILES_DIR, "price_overrides.dat")
    file.outputStream().buffered().data().use { output ->
        output.writeInt(overrides.size)
        overrides.forEach { (itemId, price) ->
            output.writeInt(itemId)
            output.writeDouble(price)
        }
    }
}


/**
 * Reads the cached prices and returns them, and timestamp when they were fetched.
 */
private fun readCachedPricesAndLastUpdateTime(): Pair<Map<Int, Double>?, Long> {
    val file = File(USER_FILES_DIR, "prices.dat")
    if (!file.exists())
        return Pair(null, 0)

    val lastModified = file.lastModified()
    if (lastModified == 0L)
        return Pair(null, 0)

    val prices = timeAction("Loading cached prices") {
        try {
            buildMap<Int, Double> {
                file.inputStream().buffered().data().use { input ->
                    val itemCount = input.readInt()
                    repeat(itemCount) {
                        val itemId = input.readInt()
                        val price = input.readDouble()
                        put(itemId, price)
                    }
                }
            }
        } catch (e: IOException) {
            System.err.println("Error reading cached prices")
            e.printStackTrace()
            return Pair(null, 0)
        }
    }

    return Pair(prices, lastModified)
}


/**
 * Writes the given prices to cache.
 */
private fun writePricesCache(prices: Map<Int, Double>) {
    val file = File(USER_FILES_DIR, "prices.dat")
    file.outputStream().buffered().data().use { output ->
        output.writeInt(prices.size)
        for ((itemId, price) in prices) {
            output.writeInt(itemId)
            output.writeDouble(price)
        }
    }
}


/**
 * Fetches and returns prices.
 */
private suspend fun fetchPrices(): Map<Int, Double> {
    val prices = sendEsiRequestWithRetry(
        onFirstException = {
            System.err.println("Failed to retrieve prices.\n${it.javaClass.simpleName} ${it.message}")
        },
    ) {
        MarketApi.getAllItemPrices()
    }
    return buildMap {
        prices.forEach {
            val itemId = it.typeId
            val price = it.averagePrice ?: return@forEach
            put(itemId, price)
        }
    }
}
