using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
using SPTarkov.Server.Core.Models.Enums;
using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Server.Core.Utils;
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;

namespace SPTarkov.Server.Core.Services;

/// <summary>
///     Stores flea prices for items as well as methods to interact with them.
/// </summary>
[Injectable(InjectionType.Singleton)]
public class RagfairPriceService(
    ISptLogger<RagfairPriceService> logger,
    RandomUtil randomUtil,
    HandbookHelper handbookHelper,
    TraderHelper traderHelper,
    PresetHelper presetHelper,
    ItemHelper itemHelper,
    DatabaseService databaseService,
    ServerLocalisationService serverLocalisationService,
    ConfigServer configServer
)
{
    private readonly RagfairConfig _ragfairConfig = configServer.GetConfig<RagfairConfig>();
    protected Dictionary<MongoId, double>? _staticPrices;

    /// <summary>
    ///     Generate static (handbook) and dynamic (prices.json) flea prices, store inside class as dictionaries
    /// </summary>
    public void Load()
    {
        RefreshStaticPrices();
        RefreshDynamicPrices();
    }

    public string GetRoute()
    {
        return "RagfairPriceService";
    }

    /// <summary>
    ///     Iterate over all items of type "Item" in db and get template price, store in cache
    /// </summary>
    public void RefreshStaticPrices()
    {
        _staticPrices = new Dictionary<MongoId, double>();
        foreach (
            var item in databaseService
                .GetItems()
                .Values.Where(item => string.Equals(item.Type, "Item", StringComparison.OrdinalIgnoreCase))
        )
        {
            _staticPrices[item.Id] = handbookHelper.GetTemplatePrice(item.Id);
        }
    }

    /// <summary>
    ///     Copy the prices.json data into our dynamic price dictionary
    /// </summary>
    public void RefreshDynamicPrices()
    {
        // TODO: remove as redundant?
    }

    /// <summary>
    ///     Get the dynamic price for an item. If value doesn't exist, use static (handbook) value.
    ///     if no static value, return 1
    /// </summary>
    /// <param name="tplId">Item tpl id to get price for</param>
    /// <returns>price in roubles</returns>
    public double GetFleaPriceForItem(MongoId tplId)
    {
        // Get dynamic price (templates/prices), if that doesn't exist get price from static array (templates/handbook)
        var itemPrice = itemHelper.GetDynamicItemPrice(tplId) ?? GetStaticPriceForItem(tplId);
        if (itemPrice is null)
        {
            var itemFromDb = itemHelper.GetItem(tplId);
            logger.Warning(
                serverLocalisationService.GetText(
                    "ragfair-unable_to_find_item_price_for_item_in_flea_handbook",
                    new { tpl = tplId, name = itemFromDb.Value.Name ?? "" }
                )
            );
        }

        // If no price in dynamic/static, set to 1
        if (itemPrice == 0)
        {
            itemPrice = 1;
        }

        return itemPrice.Value;
    }

    /// <summary>
    ///     Get the dynamic (flea) price for an item
    /// </summary>
    /// <param name="itemTpl"> Item template id to look up </param>
    /// <returns> Price in roubles </returns>
    public double? GetDynamicPriceForItem(MongoId itemTpl)
    {
        databaseService.GetPrices().TryGetValue(itemTpl, out var value);

        return value;
    }

    /// <summary>
    ///     Grab the static (handbook) for an item by its tplId
    /// </summary>
    /// <param name="itemTpl">item template id to look up</param>
    /// <returns>price in roubles</returns>
    public double? GetStaticPriceForItem(MongoId itemTpl)
    {
        return handbookHelper.GetTemplatePrice(itemTpl);
    }

    /// <summary>
    ///     Get prices for all items on flea, prioritize handbook prices first, use prices from prices.json if missing
    ///     This will refresh the caches prior to building the output
    /// </summary>
    /// <returns>Dictionary of item tpls and rouble cost</returns>
    public Dictionary<MongoId, double> GetAllFleaPrices()
    {
        var dynamicPrices = databaseService.GetPrices();
        // Use dynamic prices first, fill in any gaps with data from static prices (handbook)
        return dynamicPrices.Concat(_staticPrices).GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.First().Value);
    }

    public Dictionary<MongoId, double> GetAllStaticPrices()
    {
        // Refresh the cache so we include any newly added custom items
        if (_staticPrices is null)
        {
            RefreshStaticPrices();
        }

        return _staticPrices;
    }

    /// <summary>
    ///     Get the percentage difference between two values
    /// </summary>
    /// <param name="a">numerical value a</param>
    /// <param name="b">numerical value b</param>
    /// <returns>different in percent</returns>
    protected double GetPriceDifference(double a, double b)
    {
        return 100 * a / (a + b);
    }

    /// <summary>
    ///     Generate a currency cost for an item and its mods
    /// </summary>
    /// <param name="offerItems">Item with mods to get price for</param>
    /// <param name="desiredCurrency">Currency price desired in</param>
    /// <param name="isPackOffer">Price is for a pack type offer</param>
    /// <returns>cost of item in desired currency</returns>
    public double GetDynamicOfferPriceForOffer(IEnumerable<Item> offerItems, MongoId desiredCurrency, bool isPackOffer)
    {
        // Price to return.
        var price = 0d;

        // Iterate over each item in the offer.
        foreach (var item in offerItems)
        {
            // Skip over armor inserts as those are not factored into item prices.
            if (itemHelper.IsOfBaseclass(item.Template, BaseClasses.BUILT_IN_INSERTS))
            {
                continue;
            }

            price += GetDynamicItemPrice(item.Template, desiredCurrency, item, offerItems, isPackOffer).Value;

            // Check if the item is a weapon preset.
            if (item?.Upd?.SptPresetId is not null && presetHelper.IsPresetBaseClass(item.Upd.SptPresetId.Value, BaseClasses.WEAPON))
            // This is a weapon preset, which has its own price calculation that takes into account the mods in the
            // preset. Since we've already calculated the price for the preset entire preset in
            // `getDynamicItemPrice`, we can skip the rest of the items in the offer.
            {
                break;
            }
        }

        return Math.Round(price);
    }

    /// <summary>
    /// </summary>
    /// <param name="itemTemplateId">items tpl value</param>
    /// <param name="desiredCurrency">Currency to return result in</param>
    /// <param name="item">Item object (used for weapon presets)</param>
    /// <param name="offerItems"></param>
    /// <param name="isPackOffer"></param>
    /// <returns></returns>
    public double? GetDynamicItemPrice(
        MongoId itemTemplateId,
        MongoId desiredCurrency,
        Item? item = null,
        IEnumerable<Item>? offerItems = null,
        bool? isPackOffer = null
    )
    {
        var isPreset = false;
        var price = GetFleaPriceForItem(itemTemplateId);

        // Adjust price if below handbook price, based on config.
        if (_ragfairConfig.Dynamic.OfferAdjustment.AdjustPriceWhenBelowHandbookPrice)
        {
            price = AdjustPriceIfBelowHandbook(price, itemTemplateId);
        }

        // Use trader price if higher, based on config.
        if (_ragfairConfig.Dynamic.UseTraderPriceForOffersIfHigher)
        {
            var traderPrice = traderHelper.GetHighestSellToTraderPrice(itemTemplateId);
            if (traderPrice > price)
            {
                price = traderPrice;
            }
        }

        // Prices for weapon presets are handled differently.
        if (
            item?.Upd?.SptPresetId is not null
            && offerItems is not null
            && presetHelper.IsPresetBaseClass(item.Upd.SptPresetId.Value, BaseClasses.WEAPON)
        )
        {
            price = GetWeaponPresetPrice(item, offerItems, price);
            isPreset = true;
        }

        // Check for existence of manual price adjustment multiplier
        if (_ragfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(itemTemplateId, out var multiplier))
        {
            price *= multiplier;
        }

        // The quality of the item affects the price + not on the ignore list
        if (item is not null && !_ragfairConfig.Dynamic.IgnoreQualityPriceVarianceBlacklist.Contains(itemTemplateId))
        {
            var qualityModifier = itemHelper.GetItemQualityModifier(item);
            price *= qualityModifier;
        }

        // Make adjustments for unreasonably priced items.
        foreach (var (key, value) in _ragfairConfig.Dynamic.UnreasonableModPrices)
        {
            if (!value.Enabled || !itemHelper.IsOfBaseclass(itemTemplateId, key))
            {
                continue;
            }

            price = AdjustUnreasonablePrice(value, itemTemplateId, price);
        }

        // Vary the price based on the type of offer.
        var range = GetOfferTypeRangeValues(isPreset, isPackOffer ?? false);
        price = RandomiseOfferPrice(price, range);

        // Convert to different currency if required.
        if (desiredCurrency != Money.ROUBLES)
        {
            price = handbookHelper.FromRoubles(price, desiredCurrency);
        }

        if (price <= 0)
        {
            return 0.1d;
        }

        return price;
    }

    /// <summary>
    ///     using data from config, adjust an items price to be relative to its handbook price
    /// </summary>
    /// <param name="unreasonableItemChange">Change object from config</param>
    /// <param name="itemTpl">Item being adjusted</param>
    /// <param name="price">Current price of item</param>
    /// <returns>Adjusted price of item</returns>
    protected double AdjustUnreasonablePrice(UnreasonableModPrices unreasonableItemChange, MongoId itemTpl, double price)
    {
        var itemHandbookPrice = handbookHelper.GetTemplatePrice(itemTpl);
        if (itemHandbookPrice > 0)
        {
            return price;
        }

        // Flea price is over handbook price
        if (price > itemHandbookPrice * unreasonableItemChange.HandbookPriceOverMultiplier)
        {
            // Skip extreme values
            if (price <= 1)
            {
                return price;
            }

            // Price is over limit, adjust
            return itemHandbookPrice * unreasonableItemChange.NewPriceHandbookMultiplier;
        }

        return price;
    }

    /// <summary>
    ///     Get different min/max price multipliers for different offer types (preset/pack/default)
    /// </summary>
    /// <param name="isPreset">Offer is a preset</param>
    /// <param name="isPack">Offer is a pack</param>
    /// <returns>MinMax values</returns>
    protected MinMax<double> GetOfferTypeRangeValues(bool isPreset, bool isPack)
    {
        // Use different min/max values if the item is a preset or pack
        var priceRanges = _ragfairConfig.Dynamic.PriceRanges;
        if (isPreset)
        {
            return priceRanges.Preset;
        }

        if (isPack)
        {
            return priceRanges.Pack;
        }

        return priceRanges.Default;
    }

    /// <summary>
    ///     Check to see if an items price is below its handbook price and adjust according to values set to config/ragfair.json
    /// </summary>
    /// <param name="itemPrice">price of item</param>
    /// <param name="itemTpl">item template Id being checked</param>
    /// <returns>adjusted price value in roubles</returns>
    protected double AdjustPriceIfBelowHandbook(double itemPrice, MongoId itemTpl)
    {
        var itemHandbookPrice = GetStaticPriceForItem(itemTpl);
        var priceDifferencePercent = GetPriceDifference(itemHandbookPrice.Value, itemPrice);
        var offerAdjustmentSettings = _ragfairConfig.Dynamic.OfferAdjustment;

        // Only adjust price if difference is > a percent AND item price passes threshold set in config
        if (
            priceDifferencePercent > offerAdjustmentSettings.MaxPriceDifferenceBelowHandbookPercent
            && itemPrice >= offerAdjustmentSettings.PriceThresholdRub
        )
        // var itemDetails = this.itemHelper.getItem(itemTpl);
        // this.logger.debug(`item below handbook price {itemDetails[1]._name} handbook: {itemHandbookPrice} flea: ${itemPrice} {priceDifferencePercent}%`);
        {
            return Math.Round(itemHandbookPrice.Value * offerAdjustmentSettings.HandbookPriceMultiplier);
        }

        return itemPrice;
    }

    /// <summary>
    ///     Multiply the price by a randomised curve where n = 2, shift = 2
    /// </summary>
    /// <param name="existingPrice">price to alter</param>
    /// <param name="rangeValues">min and max to adjust price by</param>
    /// <returns>multiplied price</returns>
    protected double RandomiseOfferPrice(double existingPrice, MinMax<double> rangeValues)
    {
        // Multiply by 100 to get 2 decimal places of precision
        var multiplier = randomUtil.GetBiasedRandomNumber(rangeValues.Min * 100, rangeValues.Max * 100, 2, 2);

        // return multiplier back to its original decimal place location
        return existingPrice * (multiplier / 100);
    }

    /// <summary>
    ///     Calculate the cost of a weapon preset by adding together the price of its mods + base price of default weapon preset
    /// </summary>
    /// <param name="weaponRootItem">base weapon</param>
    /// <param name="weaponWithChildren">weapon plus mods</param>
    /// <param name="existingPrice">price of existing base weapon</param>
    /// <returns>price of weapon in roubles</returns>
    protected double GetWeaponPresetPrice(Item weaponRootItem, IEnumerable<Item> weaponWithChildren, double existingPrice)
    {
        // Get the default preset for this weapon
        var presetResult = GetWeaponPreset(weaponRootItem);
        if (presetResult.IsDefault)
        {
            return GetFleaPriceForItem(weaponRootItem.Template);
        }

        // Get mods on current gun not in default preset
        var newOrReplacedModsInPresetVsDefault = weaponWithChildren.Where(x =>
            !presetResult.Preset.Items.Any(y => y.Template == x.Template)
        );

        // Add up extra mods price
        var extraModsPrice = 0d;
        foreach (var mod in newOrReplacedModsInPresetVsDefault)
        // Use handbook or trader price, whatever is higher (dont use dynamic flea price as purchased item cannot be relisted)
        {
            extraModsPrice += GetHighestHandbookOrTraderPriceAsRouble(mod.Template).Value;
        }

        // Only deduct cost of replaced mods if there's replaced/new mods
        if (newOrReplacedModsInPresetVsDefault.Any())
        {
            // Add up cost of mods replaced
            var modsReplacedByNewMods = newOrReplacedModsInPresetVsDefault.Where(x =>
                presetResult.Preset.Items.Any(y => y.SlotId == x.SlotId)
            );

            // Add up replaced mods price
            var replacedModsPrice = 0d;
            foreach (var replacedMod in modsReplacedByNewMods)
            {
                replacedModsPrice += GetHighestHandbookOrTraderPriceAsRouble(replacedMod.Template).Value;
            }

            // Subtract replaced mods total from extra mods total
            extraModsPrice -= replacedModsPrice;
        }

        // return extra mods price + base gun price
        return existingPrice + extraModsPrice;
    }

    /// <summary>
    ///     Get the highest price for an item that is stored in handbook or trader assorts
    /// </summary>
    /// <param name="itemTpl">Item to get highest price of</param>
    /// <returns>rouble cost</returns>
    protected double? GetHighestHandbookOrTraderPriceAsRouble(MongoId itemTpl)
    {
        var price = GetStaticPriceForItem(itemTpl);
        var traderPrice = traderHelper.GetHighestSellToTraderPrice(itemTpl);
        if (traderPrice > price)
        {
            price = traderPrice;
        }

        return price;
    }

    /// <summary>
    ///     Attempt to get the default preset for a weapon, failing that get the first preset in the array
    ///     (assumes default = has encyclopedia entry)
    /// </summary>
    /// <param name="weapon">weapon item to get preset of</param>
    /// <returns>Default preset object</returns>
    protected WeaponPreset GetWeaponPreset(Item weapon)
    {
        var defaultPreset = presetHelper.GetDefaultPreset(weapon.Template);
        if (defaultPreset is not null)
        {
            return new WeaponPreset { IsDefault = true, Preset = defaultPreset };
        }

        var nonDefaultPresets = presetHelper.GetPresets(weapon.Template);

        if (logger.IsLogEnabled(LogLevel.Debug))
        {
            logger.Debug(
                nonDefaultPresets.Count == 1
                    ? $"Item Id: {weapon.Template} has no default encyclopedia entry but only one preset: ({nonDefaultPresets[0].Name}), choosing preset: ({nonDefaultPresets[0].Name})"
                    : $"Item Id: {weapon.Template} has no default encyclopedia entry, choosing first preset({nonDefaultPresets[0].Name}) of {nonDefaultPresets.Count}"
            );
        }

        return new WeaponPreset { IsDefault = false, Preset = nonDefaultPresets[0] };
    }

    public record WeaponPreset
    {
        public bool IsDefault { get; set; }

        public Preset Preset { get; set; }
    }
}
