import https from "https";
import { DependencyContainer } from "tsyringe";
import { IPostDBLoadMod } from "@spt-aki/models/external/IPostDBLoadMod";
import { RagfairServer } from "@spt-aki/servers/RagfairServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { LogTextColor } from "@spt-aki/models/spt/logging/LogTextColor";
import { LogBackgroundColor } from "@spt-aki/models/spt/logging/LogBackgroundColor";
import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService";
import { RagfairOfferService } from "@spt-aki/services/RagfairOfferService";
import { IBarterScheme, ITrader } from "@spt-aki/models/eft/common/tables/ITrader"
import { Traders } from "@spt-aki/models/enums/Traders"
import { Item } from "@spt-aki/models/eft/common/tables/IItem"
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem"
import { HandbookItem, IHandbookBase } from "@spt-aki/models/eft/common/tables/IHandbookBase"
import { IDatabaseTables } from "@spt-aki/models/spt/server/IDatabaseTables"

import pkg from "../package.json";
import modConfig from "../config/config.json";

const query = "{\"query\":\"{\\n\\t\\titems(type: any){\\n\\t\\t\\tid\\n\\t\\t\\tbasePrice\\n\\t\\t\\tfleaMarketFee\\n\\t\\t\\tavg24hPrice\\n\\t\\t\\tlastOfferCount\\n\\t\\t\\tsellFor {\\n\\t\\t\\tprice\\n\\t\\t\\t}\\n\\t\\t\\tbuyFor{\\n\\t\\t\\t\\tprice\\n\\t\\t\\t}\\n\\t\\t\\t\\thistoricalPrices{\\n\\t\\t\\t\\tprice\\n\\t\\t\\t\\ttimestamp\\n\\t\\t\\t}\\n\\t\\t}\\n\\t}\\n\"}"
const headers = {
    hostname: "api.tarkov.dev",
    path: "/graphql",
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "Accept": "application/json",
    }
};

class Mod implements IPostDBLoadMod
{
    private static modName: string = `${pkg.author}-${pkg.name}`;
    private static updateInterval: number = (!modConfig?.UpdateIntervalSecond || typeof(modConfig.UpdateIntervalSecond) !== "number" || modConfig.UpdateIntervalSecond < 600 ? 600 : modConfig.UpdateIntervalSecond)*1000;
    private static container: DependencyContainer;
    protected static updateTimer: number; 
    
	private static database: DatabaseServer
	private static tables: IDatabaseTables
	private static handbook: IHandbookBase
	private static items: Record<string, ITemplateItem>
	private static traders: Record<string, ITrader>
	private static traderList: ITrader[]
	private static euroRatio: number
	private static dollarRatio: number
	private static fleaPrices: Record<string, number>

    public postDBLoad(container: DependencyContainer): void 
    {
		// S P T 3 . 0 . 0
		Mod.container = container;

        const logger = container.resolve<ILogger>("WinstonLogger");

        logger.info(`Loading: ${Mod.modName} ${pkg.version}${modConfig.Enabled ? ` - Update Interval: ${(Mod.updateInterval/60000).toFixed(0)} mins` : " [Disabled]"}`);
		if (!modConfig.Enabled)
		{
			return;
		}
        
		Mod.database = container.resolve<DatabaseServer>("DatabaseServer")
		Mod.tables = Mod.database.getTables()
		Mod.items = Mod.tables.templates.items
        Mod.fleaPrices = Object.assign({}, Mod.database.getTables().templates.prices)
		Mod.handbook = Mod.tables.templates.handbook
		Mod.euroRatio = Mod.handbook.Items.find((x) => x.Id == "569668774bdc2da2298b4568").Price
		Mod.dollarRatio = Mod.handbook.Items.find((x) => x.Id == "5696686a4bdc2da3298b456a").Price
		Mod.traders = Mod.tables.traders

		// Hardcode list for best buy_price_coef
		Mod.traderList = [
			Mod.traders[Traders.THERAPIST],
			Mod.traders[Traders.RAGMAN],
			Mod.traders[Traders.JAEGER],
			Mod.traders[Traders.MECHANIC],
			Mod.traders[Traders.PRAPOR],
			Mod.traders[Traders.SKIER],
			Mod.traders[Traders.PEACEKEEPER],
		]

        Mod.updatePrices(true);
        Mod.updateTimer = setInterval(Mod.updatePrices, Mod.updateInterval, false);
    }

    static updatePrices(init: Boolean = false): void
    {
        const logger = Mod.container.resolve<ILogger>("WinstonLogger");
        const databaseServer = Mod.container.resolve<DatabaseServer>("DatabaseServer");
        const pricesTable = databaseServer.getTables().templates.prices;
        const itemsTable = databaseServer.getTables().templates.items;

        logger.log(`${Mod.modName} - Requesting price data from the api server...`, LogTextColor.BLACK, LogBackgroundColor.YELLOW);
        const req = https.request(headers, res => 
        {
            // Hooray, we have the data and alive api server
            if (res.statusCode == 200)
            {
                // Thinking
                let response = "";
                res.on("data", function(d) 
                {
                    response += d;
                });

                // And more thinking
                res.on("end", function(d) 
                {
                    // Harder
                    try
                    {
                        // Parse the data from the api server
                        const respond = JSON.parse(response);

                        // Configs
                        const updateFilters = modConfig?.UpdateFilters;
                        const updateBannedItems = modConfig?.UpdatePricesFromTradersForBannedItems;

                        let priceFilter = updateFilters.Prices.toString() || "avg";
                        if (priceFilter != "avg" && priceFilter != "lowest" && priceFilter != "highest" && priceFilter != "med")
                        {
                            priceFilter = "avg";
                            logger.error(`${Mod.modName}: Config "UpdateFilters" - "Prices" has bad value "${updateFilters?.Prices}", using default value: avg`);
                        }

                        let priceTimeLimit = Number(updateFilters.LimitPriceDataWithinTime_Hour) || 24;

                        // UwU
                        let updateCount = 0;

                        // What's dis?
                        for (const i in respond.data.items)
                        {
                            const apiData = respond.data.items[i];
                            const itemId = apiData.id;
                            const avg24hPrice = apiData.avg24hPrice;
                            let price = 0;

                            // Blacklist check
                            let skip = false;
                            for (const j in modConfig.Blacklist)
                            {
                                if (modConfig.Blacklist[j] === itemId)
                                {
                                    skip = true;
                                    break;
                                }
                            }
                            if (skip === true) continue;

	
                            // Bitcoin price from Therapist
                            /* No longer used since item has banned from live flea market and no data to use
                            if (id === "59faff1d86f7746c51718c9c")
                            {
                                for (const trader in datas.data.items[i].traderPrices)
                                {
                                    if (datas.data.items[i].traderPrices[trader].trader.name.toLowerCase() === "therapist")
                                    {
                                        price = datas.data.items[i].traderPrices[trader].price;
                                        break;
                                    }
                                }
                            } */

                            // Skip undefiend items
                            /* but we don't need to skip it, we might needs when we playing BE or Up-Versions
                            if (!pricesTable[id])
                            {
                                continue;
                            } */

                            // Skip or update prices from traders for banned items
                            if (avg24hPrice < 1 && !apiData.historicalPrices?.length)
                            {
                                // No thx by user's choice or no data to use at all
                                if (!updateBannedItems?.Enabled || (!apiData.sellFor?.length && !apiData.buyFor?.length))
                                {
                                    continue;
                                }

                                let pickedPrice = -1;
                                if (apiData.buyFor?.length > 0)
                                {
                                    for (const p in apiData.buyFor)
                                    {
                                        if (pickedPrice == -1) pickedPrice = apiData.buyFor[p].price
                                        pickedPrice = Math.min(apiData.buyFor[p].price, pickedPrice);
                                    }
                                }
                                else if (apiData.sellFor?.length > 0)
                                {
                                    for (const p in apiData.sellFor)
                                    {
                                        if (pickedPrice == -1) pickedPrice = apiData.sellFor[p].price
                                        pickedPrice = Math.max(apiData.sellFor[p].price, pickedPrice);
                                    }
                                }

                                if (updateBannedItems?.LowPricedItemsOnly)
                                {
                                    if (pricesTable[itemId] < pickedPrice)
                                    {
                                        price = pickedPrice;
                                    }
                                }
                                else if (pickedPrice > 0)
                                {
                                    // Update the item price anyway, by user's choice
                                    price = pickedPrice;
                                }
                                else
                                {
                                    // Nothing to update
                                    continue;
                                }
                            }
                            else // We have actual price data from the api server
                            {
                                // Ez price
                                if (priceFilter == "avg" && priceTimeLimit === 24 && avg24hPrice > 0)
                                {
                                    price = avg24hPrice;
                                }
                                // Time to do some math
                                else if (apiData.historicalPrices?.length)
                                {
                                    let pickedPrice = [];
                                    for (const p in apiData.historicalPrices)
                                    {
                                        const data = apiData.historicalPrices[p];
                                        switch(priceFilter)
                                        {
                                            case "lowest":
                                            {
                                                pickedPrice[0] = Math.min(data.price, pickedPrice[0]);
                                                break;
                                            }

                                            case "avg":
                                            {
                                                pickedPrice.push(data.price);
                                                break;
                                            }

                                            case "highest":
                                            {
                                                pickedPrice[0] = Math.max(data.price, pickedPrice[0]);
                                                break;
                                            }

                                            case "med":
                                            {
                                                pickedPrice.push(data.price);
                                                break;
                                            }
                                        }
                                    }

                                    if (pickedPrice.length)
                                    {
                                        if (priceFilter == "med")
                                        {
                                            pickedPrice = pickedPrice.sort()
                                            price = pickedPrice[Math.floor(pickedPrice.length / 2)]
                                        }
                                        else if (priceFilter != "avg")
                                        {
                                            price = pickedPrice[0];
                                        }
                                        else // avg
                                        {
                                            price = pickedPrice.reduce((a, b) => a + b, 0) / pickedPrice.length;
                                        }
                                    }
                                    else // No data at all, wtf.
                                    {
                                        continue;
                                    }
                                }
                                else
                                {
                                    // No data at all, wtf.
                                    if (avg24hPrice < 1)
                                    {
                                        continue;
                                    }
                                    else // I guess this is what we've got now
                                    {
                                        price = avg24hPrice;
                                    }
                                }
                            }

                            // Finall we got a new price?
                            if (pricesTable[itemId] !== price && price > 0)
                            {
                                updateCount++;
                                
                                const itemBarters = Mod.bartersResolver(Mod.traderList, itemId)
                                let traderBarter : ResolvedBarter
                                let traderBarterResource
                                let traderPrice
                                
                                for (const barter of itemBarters) {
                                    if (traderBarter && barter.barterLoyaltyLevel > traderBarter.barterLoyaltyLevel) continue

                                    for (const resource of barter.barterResources) {
                                        if (resource._tpl == "5449016a4bdc2d6f028b456f") {
                                            traderBarter = barter
                                            traderBarterResource = resource
                                            break
                                        } else if (resource._tpl == "569668774bdc2da2298b4568") {
                                            traderBarter = barter
                                            traderBarterResource = resource
                                            break
                                        } else if (resource._tpl == "5696686a4bdc2da3298b456a") {
                                            traderBarter = barter
                                            traderBarterResource = resource
                                            break
                                        }
                                    }

                                    if (traderBarterResource)
                                    {
                                        switch (traderBarterResource._tpl)
                                        {
                                            case "5449016a4bdc2d6f028b456f":
                                                traderPrice = traderBarterResource.count
                                                break
                                            case "569668774bdc2da2298b4568":
                                                traderPrice = traderBarterResource.count * Mod.euroRatio
                                                break
                                            case "5696686a4bdc2da3298b456a":
                                                traderPrice = traderBarterResource.count * Mod.dollarRatio
                                                break
                                        }
                                    }
                                }

                                let isEquipment = false
                                let noFlea = apiData.fleaMarketFee == null || apiData.fleaMarketFee == undefined
                                if (Mod.items[itemId] && Mod.checkParent(Mod.items[itemId], [ "5448fe124bdc2da5018b4567", "55802f3e4bdc2de7118b4584", "55802f4a4bdc2ddb688b4569", "550aa4154bdc2dd8348b456b", "5448e54d4bdc2dcc718b4568", "5422acb9af1c889c16000029" ]))
                                {
                                    isEquipment = true
                                }
                                let origPrice = Mod.getDefaultPrice(itemId)

                                if (traderBarter && traderBarter.barterLoyaltyLevel == 1 && traderPrice)
                                {
                                    if (price < traderPrice)
                                        pricesTable[itemId] = price
                                    else pricesTable[itemId] = traderPrice
                                }
                                else if (price < origPrice)
                                    pricesTable[itemId] = price
                                else if (isEquipment && price / origPrice > 100)
                                    pricesTable[itemId] = (origPrice + price / 50) / 2;
                                else if (isEquipment && price / origPrice > 50)
                                    pricesTable[itemId] = (origPrice + price / 23) / 2;
                                else if (price / origPrice > 200)
                                    pricesTable[itemId] = (origPrice + price / 50) / 2;
                                else if (price / origPrice > 100)
                                    pricesTable[itemId] = (origPrice + price / 20) / 2;
                                else if (price / origPrice > 50)
                                    pricesTable[itemId] = (origPrice + price / 8) / 2;
                                else if (isEquipment && price / origPrice > 12) 
                                    pricesTable[itemId] = (origPrice + price / 3) / 2;
                                else if (isEquipment && price / origPrice > 6)
                                    pricesTable[itemId] = (origPrice + price / 2) / 2;
                                else
                                    pricesTable[itemId] = price;
                                
                                if (!noFlea && !(traderPrice && traderBarter?.barterLoyaltyLevel == 1)) {
                                    
                                    let overpricedFactor = apiData.fleaMarketFee / apiData.basePrice
                                    if (overpricedFactor > 10 && apiData.basePrice < 2500) pricesTable[itemId] = (origPrice * 3 + pricesTable[itemId]) / 4
                                    else if (overpricedFactor > 8 && apiData.basePrice < 4500) pricesTable[itemId] = (origPrice * 4 + pricesTable[itemId]) / 5
                                    else if (apiData.lastOfferCount <= 1 && overpricedFactor > 3) pricesTable[itemId] = (origPrice * 2 + pricesTable[itemId]) / 3
                                }

                                if (traderBarter && traderBarter.barterLoyaltyLevel == 2)
                                {
                                    if (traderPrice < pricesTable[itemId]) pricesTable[itemId] = (traderPrice * 2  + pricesTable[itemId]) / 3
                                }

                                if (traderBarter && traderBarter.barterLoyaltyLevel == 3)
                                {
                                    
                                    if (traderPrice < pricesTable[itemId]) pricesTable[itemId] = (traderPrice + pricesTable[itemId]) / 2
                                }

                                logger.info(`${Mod.modName} - "${Mod.items[itemId]?._name}" ${origPrice} -> ${price} -> ${pricesTable[itemId]} (E: ${isEquipment}, trader: ${traderBarter?.traderID} ll${traderBarter?.barterLoyaltyLevel} - ${traderBarterResource?._tpl} * ${traderBarterResource?.count} => ${traderPrice})`, LogTextColor.BLACK, LogBackgroundColor.YELLOW);
                            }
                        }

                        // Did we do it?
                        if (updateCount)
                        {
                            logger.log(`${Mod.modName}: Updated market data, Total ${updateCount} items`, LogTextColor.BLACK, LogBackgroundColor.CYAN);
                        }
                        else
                        {
                            logger.log(`${Mod.modName}: Already up to date!`, LogTextColor.WHITE, LogBackgroundColor.MAGENTA);
                        }

                        // Generate flea market offers with new prices, Only once upon load
                        if (init)
                        {
                            const traders = databaseServer.getTables().traders;
                            for (const traderId in traders)
                            {
                                traders[traderId].base.refreshTraderRagfairOffers = true;
                            }
                            Mod.container.resolve<RagfairOfferService>("RagfairOfferService").offers = [];
                            Mod.container.resolve<RagfairPriceService>("RagfairPriceService").generateDynamicPrices();
                            Mod.container.resolve<RagfairServer>("RagfairServer").load();
                            logger.log(`${Mod.modName}: Generated initial flea market offers`, LogTextColor.WHITE, LogBackgroundColor.MAGENTA);
                        }
                    }
                    catch (error) 
                    {
                        logger.error(`${Mod.modName}: ${error}`);
                    }
                });
            }
            else
            {
                let reason = "Failed to update market data";

                if (res.statusCode >= 400 && res.statusCode <= 403)
                {
                    reason = "Tarkov.dev might banned your IP for an hour or having trouble on the server";
                }
                else if (res.statusCode == 503)
                {
                    reason = "Tarkov.dev is offline";
                }

                logger.error(`${Mod.modName}: (${res.statusCode}) ${reason}. Retry in ${(Mod.updateInterval/60000).toFixed(0)} mins`);
            }
        }).on("error", error =>
        {
            logger.error(`${Mod.modName}: ${error} - Mod disabled`);
            clearInterval(Mod.updateTimer);
        });

        req.write(query);
        req.end();
    }

    
	static bartersResolver(traderList : ITrader[],itemID: string): ResolvedBarter[] {
        const logger = Mod.container.resolve<ILogger>("WinstonLogger");
		const itemBarters: ResolvedBarter[] = []

		try {
			traderList.forEach((trader) => {
				const allTraderBarters = trader.assort.items
				const traderBarters = allTraderBarters.filter((x) => x._tpl == itemID)

				const barters = traderBarters
					.map((barter) => recursion(barter)) // find and get list of "parent items" for a passed component
					.map((barter) => ({
						// reset parentItem for actual parent items because of recursion function.
						// can be done in a more elegant way, but i'm too tired after a night of debugging. who cares anyway, it works.
						parentItem: barter.originalItemID ? (barter.originalItemID == itemID ? null : barter.originalItemID) : null,
						barterResources: trader.assort.barter_scheme[barter._id][0],
						barterLoyaltyLevel: trader.assort.loyal_level_items[barter._id],
						traderID: trader.base._id,
					}))

				itemBarters.push(...barters)

				function recursion(barter: PlaceholderItem): PlaceholderItem {
					if (barter.parentId == "hideout") {
						return barter
					} else {
						let parentBarter
						try {
							// spent literary 12 hours debugging this feature... KMP.
							// all because of one item, SWORD International Mk-18 not having proper .parentId is assort table. who would have thought. thx Nikita
							parentBarter = allTraderBarters.find((x) => x._id == barter.parentId)
							parentBarter.originalItemID = parentBarter._tpl
						} catch (error) {
							return barter // FML
						}
						return recursion(parentBarter)
					}
				}
			})
		} catch (error) {
			logger.warning(`\n[ItemInfo] bartersResolver failed because of another mod. Send bug report. Continue safely.`)
		}

		return itemBarters
	}

    static checkParent(item: ITemplateItem, ids: string[]) {
        if (item._parent) {
            if (ids.includes(item._parent)) return true
            return Mod.checkParent(Mod.items[item._parent], ids)
        }
        else
            return false
    }

	static getDefaultPrice(itemID: string): number {
		if (typeof Mod.fleaPrices[itemID] != "undefined") {
			// Forgot quotes, typeof returns string..
			return Mod.fleaPrices[itemID]
		} else {
			return Mod.getItemInHandbook(itemID)?.Price
		}
	}

	static getItemInHandbook(itemID: string): HandbookItem {
		try {
			return Mod.handbook.Items.find((i) => i.Id === itemID) // Outs: @Id, @ParentId, @Price
		} catch (error) {
            const logger = Mod.container.resolve<ILogger>("WinstonLogger");
			logger.error(error)
		}
	}
}

module.exports = { mod: new Mod() }


// A silly solution to some weird recursion logic that adds values to an object that shouldn't have them
interface PlaceholderItem extends Item {
	originalItemID?: string
}

interface ResolvedBarter {
	parentItem: string
	barterResources: IBarterScheme[]
	barterLoyaltyLevel: number
	traderID: string
}