using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Ragfair;
using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using Core.Helpers;
using Core.Models.Eft.Profile;
using Core.Models.Enums;
using Core.Routers;
using Core.Utils;
using Core.Models.Spt.Config;
using Core.Models.Common;
using Core.Models.Eft.Trade;
using Core.Generators;
using System.Xml.Linq;
using System;
using Core.Models.Spt.Services;
using LogLevel = Core.Models.Spt.Logging.LogLevel;

namespace Core.Controllers;

[Injectable]
public class RagfairController
{
    private readonly ISptLogger<RagfairController> _logger;
    private readonly TimeUtil _timeUtil;
    private readonly JsonUtil _jsonUtil;
    private readonly HttpResponseUtil _httpResponseUtil;
    private readonly EventOutputHolder _eventOutputHolder;
    private readonly RagfairServer _ragfairServer;
    private readonly ItemHelper _itemHelper;
    private readonly InventoryHelper _inventoryHelper;
    private readonly RagfairSellHelper _ragfairSellHelper;
    private readonly HandbookHelper _handbookHelper;
    private readonly ProfileHelper _profileHelper;
    private readonly PaymentHelper _paymentHelper;
    private readonly RagfairHelper _ragfairHelper;
    private readonly RagfairSortHelper _ragfairSortHelper;
    private readonly RagfairOfferHelper _ragfairOfferHelper;
    private readonly TraderHelper _traderHelper;
    private readonly DatabaseService _databaseService;
    private readonly LocalisationService _localisationService;
    private readonly RagfairTaxService _ragfairTaxService;
    private readonly RagfairOfferService _ragfairOfferService;
    private readonly PaymentService _paymentService;
    private readonly RagfairPriceService _ragfairPriceService;
    private readonly RagfairOfferGenerator _ragfairOfferGenerator;
    private readonly ConfigServer _configServer;

    private readonly RagfairConfig _ragfairConfig;

    public RagfairController(
        ISptLogger<RagfairController> logger,
        TimeUtil timeUtil,
        JsonUtil jsonUtil,
        HttpResponseUtil httpResponseUtil,
        EventOutputHolder eventOutputHolder,
        RagfairServer ragfairServer,
        ItemHelper itemHelper,
        InventoryHelper inventoryHelper,
        RagfairSellHelper ragfairSellHelper,
        HandbookHelper handbookHelper,
        ProfileHelper profileHelper,
        PaymentHelper paymentHelper,
        RagfairHelper ragfairHelper,
        RagfairSortHelper ragfairSortHelper,
        RagfairOfferHelper ragfairOfferHelper,
        TraderHelper traderHelper,
        DatabaseService databaseService,
        LocalisationService localisationService,
        RagfairTaxService ragfairTaxService,
        RagfairOfferService ragfairOfferService,
        PaymentService paymentService,
        RagfairPriceService ragfairPriceService,
        RagfairOfferGenerator ragfairOfferGenerator,
        ConfigServer configServer
    )
    {
        _logger = logger;
        _timeUtil = timeUtil;
        _jsonUtil = jsonUtil;
        _httpResponseUtil = httpResponseUtil;
        _eventOutputHolder = eventOutputHolder;
        _ragfairServer = ragfairServer;
        _itemHelper = itemHelper;
        _inventoryHelper = inventoryHelper;
        _ragfairSellHelper = ragfairSellHelper;
        _handbookHelper = handbookHelper;
        _profileHelper = profileHelper;
        _paymentHelper = paymentHelper;
        _ragfairHelper = ragfairHelper;
        _ragfairSortHelper = ragfairSortHelper;
        _ragfairOfferHelper = ragfairOfferHelper;
        _traderHelper = traderHelper;
        _databaseService = databaseService;
        _localisationService = localisationService;
        _ragfairTaxService = ragfairTaxService;
        _ragfairOfferService = ragfairOfferService;
        _paymentService = paymentService;
        _ragfairPriceService = ragfairPriceService;
        _ragfairOfferGenerator = ragfairOfferGenerator;
        _configServer = configServer;

        _ragfairConfig = _configServer.GetConfig<RagfairConfig>();
    }

    /**
     * Check all profiles and sell player offers / send player money for listing if it sold
     */
    public void Update()
    {
        foreach (var (sessionId, profile) in _profileHelper.GetProfiles())
        {
            // Check profile is capable of creating offers
            var pmcProfile = profile.CharacterData.PmcData;
            if (
                pmcProfile.RagfairInfo is not null && pmcProfile.Info.Level >= _databaseService.GetGlobals().Configuration.RagFair.MinUserLevel
            )
            {
                _ragfairOfferHelper.ProcessOffersOnProfile(sessionId);
            }
        }
    }

    /**
     * Handles client/ragfair/find
     * Returns flea offers that match required search parameters
     * @param sessionID Player id
     * @param searchRequest Search request data
     * @returns IGetOffersResult
     */
    public GetOffersResult GetOffers(string sessionID, SearchRequestData searchRequest)
    {
        var profile = _profileHelper.GetFullProfile(sessionID);

        var itemsToAdd = _ragfairHelper.FilterCategories(sessionID, searchRequest);
        var traderAssorts = _ragfairHelper.GetDisplayableAssorts(sessionID);
        var result = new GetOffersResult
        {
            Offers = new List<RagfairOffer>(),
            OffersCount = searchRequest.Limit,
            SelectedCategory = searchRequest.HandbookId,
        };

        result.Offers = GetOffersForSearchType(searchRequest, itemsToAdd, traderAssorts, profile.CharacterData.PmcData);

        // Client requested a category refresh
        if (searchRequest.UpdateOfferCount is not null)
        {
            result.Categories = GetSpecificCategories(profile.CharacterData.PmcData, searchRequest, result.Offers);
        }

        AddIndexValueToOffers(result.Offers);

        // Sort offers
        result.Offers = _ragfairSortHelper.SortOffers(
            result.Offers,
            searchRequest.SortType.Value,
            searchRequest.SortDirection.Value
        );

        // Match offers with quests and lock unfinished quests - get offers from traders
        foreach (var traderOffer in result.Offers.Where(offer => _ragfairOfferHelper.OfferIsFromTrader(offer)))
        {
            // For the items, check the barter schemes. The method getDisplayableAssorts sets a flag sptQuestLocked
            // to true if the quest is not completed yet
            if (_ragfairOfferHelper.TraderOfferItemQuestLocked(traderOffer, traderAssorts))
            {
                traderOffer.Locked = true;
            }

            // Update offers BuyRestrictionCurrent/BuyRestrictionMax values
            SetTraderOfferPurchaseLimits(traderOffer, profile);
            SetTraderOfferStackSize(traderOffer);
        }

        result.OffersCount = result.Offers.Count;

        // Handle paging before returning results only if searching for general items, not preset items
        if (searchRequest.BuildCount == 0)
        {
            var start = searchRequest.Page * searchRequest.Limit;
            var end = (int)Math.Min((double)((searchRequest.Page + 1) * searchRequest.Limit), result.Offers.Count);
            result.Offers = result.Offers.Slice(start.Value, end);
        }

        return result;
    }

    /**
     * Adjust ragfair offer stack count to match same value as traders assort stack count
     * @param offer Flea offer to adjust stack size of
     */
    private void SetTraderOfferStackSize(RagfairOffer offer)
    {
        var firstItem = offer.Items[0];
        var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(offer.User.Id).Items;

        var assortPurchased = traderAssorts.FirstOrDefault(x => x.Id == offer.Items.First().Id);
        if (assortPurchased is null)
        {
            _logger.Warning(
                _localisationService.GetText(
                    "ragfair-unable_to_adjust_stack_count_assort_not_found",
                    new
                    {
                        offerId = offer.Items.First().Id,
                        traderId = offer.User.Id,
                    }
                )
            );

            return;
        }

        firstItem.Upd.StackObjectsCount = assortPurchased.Upd.StackObjectsCount;
    }

    /**
     * Update a trader flea offer with buy restrictions stored in the traders assort
     * @param offer Flea offer to update
     * @param fullProfile Players full profile
     */
    private void SetTraderOfferPurchaseLimits(RagfairOffer offerToUpdate, SptProfile fullProfile)
    {
        var assortId = offerToUpdate.Items.First().Id;

        // No trader found in profile, create a blank record for them
        var existsInProfile = !fullProfile.TraderPurchases.TryAdd(offerToUpdate.User.Id, new Dictionary<string, TraderPurchaseData>());
        if (!existsInProfile)
        {
            // Not purchased by player before, use value from assort data

            // Find patching assort by its id
            var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(offerToUpdate.User.Id).Items;
            var assortData = traderAssorts.FirstOrDefault((item) => item.Id == assortId);

            // Set restriction based on data found above
            offerToUpdate.BuyRestrictionMax = assortData.Upd.BuyRestrictionMax;

            return;
        }

        // Get purchases player made with trader since last reset
        var traderPurchases = fullProfile.TraderPurchases[offerToUpdate.User.Id];

        // Get specific assort purchase data and set current purchase buy value
        traderPurchases.TryGetValue(assortId, out var assortTraderPurchaseData);

        offerToUpdate.BuyRestrictionCurrent = (int?)assortTraderPurchaseData?.PurchaseCount ?? 0;
    }

    /**
     * Add index to all offers passed in (0-indexed)
     * @param offers Offers to add index value to
     */
    private void AddIndexValueToOffers(List<RagfairOffer> offers)
    {
        var counter = 0;

        foreach (var offer in offers)
        {
            offer.InternalId = ++counter;
        }
    }

    /**
     * Get categories for the type of search being performed, linked/required/all
     * @param searchRequest Client search request data
     * @param offers Ragfair offers to get categories for
     * @returns record with templates + counts
     */
    private Dictionary<string, int>? GetSpecificCategories(PmcData pmcProfile, SearchRequestData searchRequest, List<RagfairOffer> offers)
    {
        // Linked/required search categories
        var playerHasFleaUnlocked =
            pmcProfile.Info.Level >= _databaseService.GetGlobals().Configuration.RagFair.MinUserLevel;
        List<RagfairOffer> offerPool = [];
        if (IsLinkedSearch(searchRequest) || IsRequiredSearch(searchRequest))
        {
            offerPool = offers;
        }
        else if (!(IsLinkedSearch(searchRequest) || IsRequiredSearch(searchRequest)))
        {
            // Get all categories
            offerPool = _ragfairOfferService.GetOffers();
        }
        else
        {
            _logger.Error(_localisationService.GetText("ragfair-unable_to_get_categories"));
            if (_logger.IsLogEnabled(LogLevel.Debug))
            {
                _logger.Debug(_jsonUtil.Serialize(searchRequest));
            }
            return new Dictionary<string, int>();
        }

        return _ragfairServer.GetAllActiveCategories(playerHasFleaUnlocked, searchRequest, offerPool);
    }

    /**
     * Is the flea search being performed a 'linked' search type
     * @param info Search request
     * @returns True if it is a 'linked' search type
     */
    private bool IsLinkedSearch(SearchRequestData searchRequest)
    {
        return !string.IsNullOrEmpty(searchRequest.LinkedSearchId);
    }

    /**
     * Is the flea search being performed a 'required' search type
     * @param info Search request
     * @returns True if it is a 'required' search type
     */
    private bool IsRequiredSearch(SearchRequestData searchRequest)
    {
        return !string.IsNullOrEmpty(searchRequest.NeededSearchId);
    }

    /**
     * Get offers for the client based on type of search being performed
     * @param searchRequest Client search request data
     * @param itemsToAdd Comes from ragfairHelper.filterCategories()
     * @param traderAssorts Trader assorts
     * @param pmcProfile Player profile
     * @returns array of offers
     */
    private List<RagfairOffer> GetOffersForSearchType(SearchRequestData searchRequest, List<string> itemsToAdd, Dictionary<string, TraderAssort> traderAssorts,
        PmcData pmcProfile)
    {
        // Searching for items in preset menu
        if (searchRequest.BuildCount > 0)
        {
            return _ragfairOfferHelper.GetOffersForBuild(searchRequest, itemsToAdd, traderAssorts, pmcProfile);
        }

        if (searchRequest.NeededSearchId?.Length > 0)
        {
            return _ragfairOfferHelper.GetOffersThatRequireItem(searchRequest, pmcProfile);
        }

        // Searching for general items
        return _ragfairOfferHelper.GetValidOffers(searchRequest, itemsToAdd, traderAssorts, pmcProfile);
    }

    /**
     * Called when creating an offer on flea, fills values in top right corner
     * @param getPriceRequest Client request object
     * @param ignoreTraderOffers Should trader offers be ignored in the calculation
     * @returns min/avg/max values for an item based on flea offers available
     */
    public GetItemPriceResult GetItemMinAvgMaxFleaPriceValues(GetMarketPriceRequestData getPriceRequest, bool ignoreTraderOffers = true)
    {
        // Get all items of tpl
        var offers = _ragfairOfferService.GetOffersOfType(getPriceRequest.TemplateId);

        // Offers exist for item, get averages of what's listed
        if (offers.Count > 0)
        {
            // These get calculated while iterating through the list below
            var minMax = new MinMax(int.MaxValue, 0);

            // Get the average offer price, excluding barter offers
            var average = GetAveragePriceFromOffers(offers, minMax, ignoreTraderOffers);

            return new GetItemPriceResult { Avg = Math.Round(average), Min = minMax.Min, Max = minMax.Max };
        }

        // No offers listed, get price from live ragfair price list prices.json
        // No flea price, get handbook price
        var fleaPrices = _databaseService.GetPrices();
        if (!fleaPrices.TryGetValue(getPriceRequest.TemplateId, out var tplPrice))
        {
            tplPrice = _handbookHelper.GetTemplatePrice(getPriceRequest.TemplateId) ?? 0;
        }

        return new GetItemPriceResult { Avg = tplPrice, Min = tplPrice, Max = tplPrice };
    }

    private double GetAveragePriceFromOffers(List<RagfairOffer> offers, MinMax minMax, bool ignoreTraderOffers)
    {
        var sum = 0d;
        var totalOfferCount = 0;

        foreach (var offer in offers)
        {
            // Exclude barter items, they tend to have outrageous equivalent prices
            if (offer.Requirements.Any(req => !_paymentHelper.IsMoneyTpl(req.Template)))
            {
                continue;
            }

            if (ignoreTraderOffers && _ragfairOfferHelper.OfferIsFromTrader(offer))
            {
                continue;
            }

            // Figure out how many items the requirementsCost is applying to, and what the per-item price is
            var offerItemCount = offer.SellInOnePiece.GetValueOrDefault(false)
                ? (offer.Items.First().Upd?.StackObjectsCount ?? 1)
                : 1;
            var perItemPrice = offer.RequirementsCost / offerItemCount;

            // Handle min/max calculations based on the per-item price
            if (perItemPrice < minMax.Min)
            {
                minMax.Min = perItemPrice;
            }
            else if (perItemPrice > minMax.Max)
            {
                minMax.Max = perItemPrice;
            }

            sum += perItemPrice.Value;
            totalOfferCount++;
        }

        if (totalOfferCount == 0)
        {
            return -1d;
        }

        return sum / totalOfferCount;
    }

    /**
     * List item(s) on flea for sale
     * @param pmcData Player profile
     * @param offerRequest Flea list creation offer
     * @param sessionID Session id
     * @returns IItemEventRouterResponse
     */
    public ItemEventRouterResponse AddPlayerOffer(PmcData pmcData, AddOfferRequestData offerRequest, string sessionID)
    {
        var output = _eventOutputHolder.GetOutput(sessionID);
        var fullProfile = _profileHelper.GetFullProfile(sessionID);

        var validationMessage = "";
        if (!IsValidPlayerOfferRequest(offerRequest, validationMessage))
        {
            return _httpResponseUtil.AppendErrorToOutput(output, validationMessage);
        }

        var typeOfOffer = GetOfferType(offerRequest);
        if (typeOfOffer == FleaOfferType.UNKNOWN)
        {
            return _httpResponseUtil.AppendErrorToOutput(output, "Unknown offer type, cannot list item on flea");
        }

        switch (typeOfOffer)
        {
            case FleaOfferType.SINGLE:
                return CreateSingleOffer(sessionID, offerRequest, fullProfile, output);
            case FleaOfferType.MULTI:
                return CreateMultiOffer(sessionID, offerRequest, fullProfile, output);
            case FleaOfferType.PACK:
                return CreatePackOffer(sessionID, offerRequest, fullProfile, output);
            case FleaOfferType.UNKNOWN:
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

    /**
     * Is the item to be listed on the flea valid
     * @param offerRequest Client offer request
     * @param errorMessage message to show to player when offer is invalid
     * @returns Is offer valid
     */
    private bool IsValidPlayerOfferRequest(AddOfferRequestData offerRequest, string validationMessage)
    {
        if (offerRequest?.Items is null || offerRequest.Items.Count == 0)
        {
            _logger.Error(_localisationService.GetText("ragfair-invalid_player_offer_request"));

            return false;
        }

        if (offerRequest.Requirements is null)
        {
            _logger.Error(_localisationService.GetText("ragfair-unable_to_place_offer_with_no_requirements"));

            return false;
        }

        return true;
    }

    /**
     * Given a client request, determine what type of offer is being created
     * single/multi/pack
     * @param offerRequest Client request
     * @returns FleaOfferType
     */
    private FleaOfferType GetOfferType(AddOfferRequestData offerRequest)
    {
        var sellInOncePiece = offerRequest.SellInOnePiece.GetValueOrDefault(false);

        if (!sellInOncePiece)
        {
            if (offerRequest.Items.Count == 1)
            {
                return FleaOfferType.SINGLE;
            }

            if (offerRequest.Items.Count > 1)
            {
                return FleaOfferType.MULTI;
            }
        }

        if (sellInOncePiece)
        {
            return FleaOfferType.PACK;
        }

        return FleaOfferType.UNKNOWN;
    }

    /**
     * Create a flea offer for multiples of the same item, can be single items or items with multiple in the stack
     * e.g. 2 ammo stacks of 30 cartridges each
     * Each item can be purchased individually
     * @param sessionID Session id
     * @param offerRequest Offer request from client
     * @param fullProfile Full profile of player
     * @param output Response to send to client
     * @returns IItemEventRouterResponse
     */
    private ItemEventRouterResponse CreateMultiOffer(string sessionID, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output)
    {
        var pmcData = fullProfile.CharacterData.PmcData;
        // var itemsToListCount = offerRequest.Items.Count; // Wasnt used to commented out for now // Does not count stack size, only items

        // multi-offers are all the same item,
        // Get first item and its children and use as template
        var firstListingAndChidren = _itemHelper.FindAndReturnChildrenAsItems(
            pmcData.Inventory.Items,
            offerRequest.Items[0]);

        // Find items to be listed on flea (+ children) from player inventory
        var result = GetItemsToListOnFleaFromInventory(pmcData, offerRequest.Items);
        if (result.Items is null || !string.IsNullOrEmpty(result.ErrorMessage))
        {
            _httpResponseUtil.AppendErrorToOutput(output, result.ErrorMessage);
        }

        // Total count of items summed using their stack counts
        var stackCountTotal = _ragfairOfferHelper.GetTotalStackCountSize(result.Items);

        // When listing identical items on flea, condense separate items into one stack with a merged stack count
        // e.g. 2 ammo items, stackObjectCount = 3 for each, will result in 1 stack of 6

        firstListingAndChidren[0].Upd ??= new Upd{ };

        firstListingAndChidren[0].Upd.StackObjectsCount = stackCountTotal;

        // Create flea object
        var offer = CreatePlayerOffer(sessionID, offerRequest.Requirements, firstListingAndChidren, false);

        // This is the item that will be listed on flea, has merged stackObjectCount
        var newRootOfferItem = offer.Items[0];

        // Average offer price for single item (or whole weapon)
        var averages = GetItemMinAvgMaxFleaPriceValues(new GetMarketPriceRequestData{ TemplateId = offer.Items[0].Template });
        var averageOfferPrice = averages.Avg;

        // Check for and apply item price modifer if it exists in config
        if (_ragfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(newRootOfferItem.Template, out var itemPriceModifer))
        {
            averageOfferPrice *= itemPriceModifer;
        }

        // Get average of item+children quality
        var qualityMultiplier = _itemHelper.GetItemQualityModifierForItems(offer.Items, true);

        // Multiply single item price by quality
        averageOfferPrice *= qualityMultiplier;

        // Get price player listed items for in roubles
        var playerListedPriceInRub = CalculateRequirementsPriceInRub(offerRequest.Requirements);

        // Roll sale chance
        var sellChancePercent = _ragfairSellHelper.CalculateSellChance(
            averageOfferPrice.Value,
            playerListedPriceInRub,
            qualityMultiplier);

        // Create array of sell times for items listed
        offer.SellResults = _ragfairSellHelper.RollForSale(sellChancePercent, (int)stackCountTotal);

        // Subtract flea market fee from stash
        if (_ragfairConfig.Sell.Fees)
        {
            var taxFeeChargeFailed = ChargePlayerTaxFee(
                sessionID,
                newRootOfferItem,
                pmcData,
                playerListedPriceInRub,
                (int)stackCountTotal,
                offerRequest,
                output);
            if (taxFeeChargeFailed)
            {
                return output;
            }
        }

        // Add offer to players profile + add to client response
        fullProfile.CharacterData.PmcData.RagfairInfo.Offers.Add(offer);
        output.ProfileChanges[sessionID].RagFairOffers.Add(offer);

        // Remove items from inventory after creating offer
        foreach (var itemToRemove in offerRequest.Items) {
            _inventoryHelper.RemoveItem(pmcData, itemToRemove, sessionID, output);
        }

        return output;
    }

    /**
     * Create a flea offer for multiple items, can be single items or items with multiple in the stack
     * e.g. 2 ammo stacks of 30 cartridges each
     * The entire package must be purchased in one go
     * @param sessionID Session id
     * @param offerRequest Offer request from client
     * @param fullProfile Full profile of player
     * @param output Response to send to client
     * @returns IItemEventRouterResponse
     */
    private ItemEventRouterResponse CreatePackOffer(string sessionID, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output)
    {
        var pmcData = fullProfile.CharacterData.PmcData;
        // var itemsToListCount = offerRequest.Items.Count; // Wasnt used so commented out for now // Does not count stack size, only items

        // multi-offers are all the same item,
        // Get first item and its children and use as template
        var firstListingAndChidren = _itemHelper.FindAndReturnChildrenAsItems(
            pmcData.Inventory.Items,
            offerRequest.Items[0]);

        // Find items to be listed on flea (+ children) from player inventory
        var result = GetItemsToListOnFleaFromInventory(pmcData, offerRequest.Items);
        if (result.Items is null || result.ErrorMessage is not null)
        {
            _httpResponseUtil.AppendErrorToOutput(output, result.ErrorMessage);
        }

        // Total count of items summed using their stack counts
        var stackCountTotal = _ragfairOfferHelper.GetTotalStackCountSize(result.Items);

        // When listing identical items on flea, condense separate items into one stack with a merged stack count
        // e.g. 2 ammo items, stackObjectCount = 3 for each, will result in 1 stack of 6
        firstListingAndChidren[0].Upd ??= new Upd { };

        firstListingAndChidren[0].Upd.StackObjectsCount = stackCountTotal;

        // Create flea object
        var offer = CreatePlayerOffer(sessionID, offerRequest.Requirements, firstListingAndChidren, true);

        // This is the item that will be listed on flea, has merged stackObjectCount
        var newRootOfferItem = offer.Items[0];

        // Single price for an item
        var averages = GetItemMinAvgMaxFleaPriceValues( new GetMarketPriceRequestData{ TemplateId = firstListingAndChidren[0].Template });
        var singleItemPrice = averages.Avg;

        // Check for and apply item price modifer if it exists in config
        if (_ragfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(newRootOfferItem.Template, out double itemPriceModifer))
        {
            singleItemPrice *= itemPriceModifer;
        }

        // Get average of item+children quality
        var qualityMultiplier = _itemHelper.GetItemQualityModifierForItems(offer.Items, true);

        // Multiply single item price by quality
        singleItemPrice *= qualityMultiplier;

        // Get price player listed items for in roubles
        var playerListedPriceInRub = CalculateRequirementsPriceInRub(offerRequest.Requirements);

        // Roll sale chance
        var sellChancePercent = _ragfairSellHelper.CalculateSellChance(
            singleItemPrice.Value * stackCountTotal,
            playerListedPriceInRub,
            qualityMultiplier);

        // Create array of sell times for items listed + sell all at once as its a pack
        offer.SellResults = _ragfairSellHelper.RollForSale(sellChancePercent, (int)stackCountTotal, true);

        // Subtract flea market fee from stash
        if (_ragfairConfig.Sell.Fees)
        {
            var taxFeeChargeFailed = ChargePlayerTaxFee(
                sessionID,
                newRootOfferItem,
                pmcData,
                playerListedPriceInRub,
                (int)stackCountTotal,
                offerRequest,
                output);
            if (taxFeeChargeFailed)
            {
                return output;
            }
        }

        // Add offer to players profile + add to client response
        fullProfile.CharacterData.PmcData.RagfairInfo.Offers.Add(offer);
        output.ProfileChanges[sessionID].RagFairOffers.Add(offer);

        // Remove items from inventory after creating offer
        foreach (var itemToRemove in offerRequest.Items) {
            _inventoryHelper.RemoveItem(pmcData, itemToRemove, sessionID, output);
        }

        return output;
    }

    /**
     * Create a flea offer for a single item - includes an item with > 1 sized stack
     * e.g. 1 ammo stack of 30 cartridges
     * @param sessionID Session id
     * @param offerRequest Offer request from client
     * @param fullProfile Full profile of player
     * @param output Response to send to client
     * @returns IItemEventRouterResponse
     */
    private ItemEventRouterResponse CreateSingleOffer(string sessionID, AddOfferRequestData offerRequest, SptProfile fullProfile,
        ItemEventRouterResponse output)
    {
        var pmcData = fullProfile.CharacterData.PmcData;
        // var itemsToListCount = offerRequest.Items.Count; // Wasnt used so commented out for now // Does not count stack size, only items

        // Find items to be listed on flea from player inventory
        var result = GetItemsToListOnFleaFromInventory(pmcData, offerRequest.Items);
        if (result.Items is null || result.ErrorMessage is not null)
        {
            _httpResponseUtil.AppendErrorToOutput(output, result.ErrorMessage);
        }

        // Total count of items summed using their stack counts
        var stackCountTotal = _ragfairOfferHelper.GetTotalStackCountSize(result.Items);

        // Checks are done, create the offer
        var playerListedPriceInRub = CalculateRequirementsPriceInRub(offerRequest.Requirements);
        var offer = CreatePlayerOffer(
            sessionID,
            offerRequest.Requirements,
            result.Items.First(),
            false
        );
        var rootItem = offer.Items.First();

        // Get average of items quality+children
        var qualityMultiplier = _itemHelper.GetItemQualityModifierForItems(offer.Items, true);

        // Average offer price for single item (or whole weapon)
        var averages = GetItemMinAvgMaxFleaPriceValues(new GetMarketPriceRequestData { TemplateId = rootItem.Template });
        var averageOfferPriceSingleItem = averages.Avg;

        // Check for and apply item price modifer if it exists in config
        if (_ragfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(rootItem.Template, out double itemPriceModifer))
        {
            averageOfferPriceSingleItem *= itemPriceModifer;
        }

        // Multiply single item price by quality
        averageOfferPriceSingleItem *= qualityMultiplier;

        // Packs are reduced to the average price of a single item in the pack vs the averaged single price of an item
        var sellChancePercent = _ragfairSellHelper.CalculateSellChance(
            averageOfferPriceSingleItem.Value,
            playerListedPriceInRub,
            qualityMultiplier
        );
        offer.SellResults = _ragfairSellHelper.RollForSale(sellChancePercent, (int)stackCountTotal);

        // Subtract flea market fee from stash
        if (_ragfairConfig.Sell.Fees)
        {
            var taxFeeChargeFailed = ChargePlayerTaxFee(
                sessionID,
                rootItem,
                pmcData,
                playerListedPriceInRub,
                (int)stackCountTotal,
                offerRequest,
                output
            );
            if (taxFeeChargeFailed)
            {
                return output;
            }
        }

        // Add offer to players profile + add to client response
        fullProfile.CharacterData.PmcData.RagfairInfo.Offers.Add(offer);
        output.ProfileChanges[sessionID].RagFairOffers.Add(offer);

        // Remove items from inventory after creating offer
        foreach (var itemToRemove in offerRequest.Items)
        {
            _inventoryHelper.RemoveItem(pmcData, itemToRemove, sessionID, output);
        }

        return output;
    }

    /**
     * Charge player a listing fee for using flea, pulls charge from data previously sent by client
     * @param sessionID Player id
     * @param rootItem Base item being listed (used when client tax cost not found and must be done on server)
     * @param pmcData Player profile
     * @param requirementsPriceInRub Rouble cost player chose for listing (used when client tax cost not found and must be done on server)
     * @param itemStackCount How many items were listed by player (used when client tax cost not found and must be done on server)
     * @param offerRequest Add offer request object from client
     * @param output IItemEventRouterResponse
     * @returns True if charging tax to player failed
     */
    private bool ChargePlayerTaxFee(
        string sessionId,
        Item rootItem,
        PmcData pmcData,
        double requirementsPriceInRub,
        int itemStackCount,
        AddOfferRequestData offerRequest,
        ItemEventRouterResponse output)
    {
        // Get tax from cache hydrated earlier by client, if that's missing fall back to server calculation (inaccurate)
        var storedClientTaxValue = _ragfairTaxService.GetStoredClientOfferTaxValueById(offerRequest.Items[0]);
        var tax = storedClientTaxValue is not null
            ? storedClientTaxValue.Fee
            : _ragfairTaxService.CalculateTax(
                rootItem,
                pmcData,
                requirementsPriceInRub,
                itemStackCount,
                offerRequest.SellInOnePiece.GetValueOrDefault(false)
            );

        if (_logger.IsLogEnabled(LogLevel.Debug))
        {
            _logger.Debug($"Offer tax to charge: {tax}, pulled from client: {storedClientTaxValue.Count is not null}");
        }

        // cleanup of cache now we've used the tax value from it
        _ragfairTaxService.ClearStoredOfferTaxById(offerRequest.Items.First());

        var buyTradeRequest = CreateBuyTradeRequestObject("RUB", tax.Value);
        _paymentService.PayMoney(pmcData, buyTradeRequest, sessionId, output);
        if (output.Warnings.Count > 0)
        {
            _httpResponseUtil.AppendErrorToOutput(
                output,
                _localisationService.GetText("ragfair-unable_to_pay_commission_fee", tax)
            );
            return true;
        }

        return false;
    }

    private RagfairOffer CreatePlayerOffer(string sessionId, List<Requirement> requirements, List<Item> items, bool sellInOnePiece)
    {
        var loyalLevel = 1;
        var formattedItems = items.Select(
            item =>
            {
                var isChild = items.Any((subItem) => subItem.Id == item.ParentId);

                return new Item
                {
                    Id = item.Id,
                    Template = item.Template,
                    ParentId = isChild ? item.ParentId : "hideout",
                    SlotId = isChild ? item.SlotId : "hideout",
                    Upd = item.Upd
                };
            }
        );

        var formattedRequirements = requirements.Select(
            item => new BarterScheme
            {
                Template = item.Template,
                Count = item.Count,
                OnlyFunctional = item.OnlyFunctional,
            }
        );

        return _ragfairOfferGenerator.CreateAndAddFleaOffer(
            sessionId,
            _timeUtil.GetTimeStamp(),
            formattedItems.ToList(),
            formattedRequirements.ToList(),
            loyalLevel,
            sellInOnePiece
        );
    }

    /**
     * Get the handbook price in roubles for the items being listed
     * @param requirements
     * @returns Rouble price
     */
    private double CalculateRequirementsPriceInRub(List<Requirement> requirements)
    {
        var requirementsPriceInRub = 0d;
        foreach (var item in requirements)
        {
            var requestedItemTpl = item.Template;

            if (_paymentHelper.IsMoneyTpl(requestedItemTpl))
            {
                requirementsPriceInRub += _handbookHelper.InRUB(item.Count.Value, requestedItemTpl);
            }
            else
            {
                requirementsPriceInRub += _itemHelper.GetDynamicItemPrice(requestedItemTpl).Value * item.Count.Value;
            }
        }

        return requirementsPriceInRub;
    }

    private GetItemsToListOnFleaFromInventoryResult GetItemsToListOnFleaFromInventory(PmcData pmcData, List<string> itemIdsFromFleaOfferRequest)
    {
        List<List<Item>> itemsToReturn = [];
        var errorMessage = string.Empty;

        // Count how many items are being sold and multiply the requested amount accordingly
        foreach (var itemId in itemIdsFromFleaOfferRequest)
        {
            var item = pmcData.Inventory.Items.FirstOrDefault((i) => i.Id == itemId);
            if (item is null)
            {
                errorMessage = _localisationService.GetText("ragfair-unable_to_find_item_in_inventory", new { id = itemId });
                _logger.Error(errorMessage);

                return new GetItemsToListOnFleaFromInventoryResult { Items = itemsToReturn, ErrorMessage = errorMessage };
            }

            item = _itemHelper.FixItemStackCount(item);
            itemsToReturn.Add(_itemHelper.FindAndReturnChildrenAsItems(pmcData.Inventory.Items, itemId));
        }

        if (itemsToReturn?.Count == 0)
        {
            errorMessage = _localisationService.GetText("ragfair-unable_to_find_requested_items_in_inventory");
            _logger.Error(errorMessage);

            return new GetItemsToListOnFleaFromInventoryResult { ErrorMessage = errorMessage };
        }

        return new GetItemsToListOnFleaFromInventoryResult { Items = itemsToReturn, ErrorMessage = errorMessage };
    }

    public record GetItemsToListOnFleaFromInventoryResult
    {
        public List<List<Item>>? Items { get; set; }
        public string? ErrorMessage { get; set; }
    }

    public ItemEventRouterResponse RemoveOffer(RemoveOfferRequestData removeRequest, string sessionId)
    {
        var output = _eventOutputHolder.GetOutput(sessionId);

        var pmcData = _profileHelper.GetPmcProfile(sessionId);
        var playerProfileOffers = pmcData.RagfairInfo.Offers;
        if (playerProfileOffers is null)
        {
            _logger.Warning(
                _localisationService.GetText(
                    "ragfair-unable_to_remove_offer_not_found_in_profile",
                    new
                    {
                        profileId = sessionId,
                        offerId = removeRequest.OfferId
                    }
                )
            );

            pmcData.RagfairInfo.Offers = new List<RagfairOffer>();
        }

        var playerOfferIndex = playerProfileOffers.FindIndex(offer => offer.Id == removeRequest.OfferId);
        if (playerOfferIndex == -1)
        {
            _logger.Error(
                _localisationService.GetText(
                    "ragfair-offer_not_found_in_profile",
                    new
                    {
                        offerId = removeRequest.OfferId
                    }
                )
            );
            return _httpResponseUtil.AppendErrorToOutput(
                output,
                _localisationService.GetText("ragfair-offer_not_found_in_profile_short")
            );
        }

        var differenceInSeconds = playerProfileOffers[playerOfferIndex].EndTime - _timeUtil.GetTimeStamp();
        if (differenceInSeconds > _ragfairConfig.Sell.ExpireSeconds)
        {
            // `expireSeconds` Default is 71 seconds
            var newEndTime = _ragfairConfig.Sell.ExpireSeconds + _timeUtil.GetTimeStamp();
            playerProfileOffers[playerOfferIndex].EndTime = (long?)Math.Round((double)newEndTime);
        }

        return output;
    }

    public ItemEventRouterResponse ExtendOffer(ExtendOfferRequestData extendRequest, string sessionId)
    {
        var output = _eventOutputHolder.GetOutput(sessionId);

        var pmcData = _profileHelper.GetPmcProfile(sessionId);
        var playerOffers = pmcData.RagfairInfo.Offers;
        var playerOfferIndex = playerOffers.FindIndex((offer) => offer.Id == extendRequest.OfferId);
        var secondsToAdd = extendRequest.RenewalTime * TimeUtil.OneHourAsSeconds;

        if (playerOfferIndex == -1)
        {
            _logger.Warning(
                _localisationService.GetText(
                    "ragfair-offer_not_found_in_profile",
                    new
                    {
                        offerId = extendRequest.OfferId
                    }
                )
            );
            return _httpResponseUtil.AppendErrorToOutput(output, _localisationService.GetText("ragfair-offer_not_found_in_profile_short"));
        }

        var playerOffer = playerOffers[playerOfferIndex];

        // MOD: Pay flea market fee
        if (_ragfairConfig.Sell.Fees)
        {
            var count = 1;
            var sellInOncePiece = playerOffer.SellInOnePiece.GetValueOrDefault(false);
            if (!sellInOncePiece)
            {
                count = (int)playerOffer.Items.Sum(offerItem => offerItem.Upd?.StackObjectsCount ?? 0);
            }

            var tax = _ragfairTaxService.CalculateTax(
                playerOffer.Items.First(),
                pmcData,
                playerOffer.RequirementsCost.Value,
                count,
                sellInOncePiece
            );

            var request = CreateBuyTradeRequestObject("RUB", tax);
            _paymentService.PayMoney(pmcData, request, sessionId, output);
            if (output.Warnings.Count > 0)
            {
                return _httpResponseUtil.AppendErrorToOutput(
                    output,
                    _localisationService.GetText("ragfair-unable_to_pay_commission_fee")
                );
            }
        }

        // Add extra time to offer
        playerOffers[playerOfferIndex].EndTime += (long?)Math.Round((decimal)secondsToAdd);

        return output;
    }

    /**
     * Create a basic trader request object with price and currency type
     * @param currency What currency: RUB, EURO, USD
     * @param value Amount of currency
     * @returns IProcessBuyTradeRequestData
     */
    private ProcessBuyTradeRequestData CreateBuyTradeRequestObject(string currency, double value)
    {
        return new ProcessBuyTradeRequestData
        {
            TransactionId = "ragfair",
            Action = "TradingConfirm",
            SchemeItems = [new IdWithCount { Id = _paymentHelper.GetCurrency(currency), Count = Math.Round(value) }],
            Type = "",
            ItemId = "",
            Count = 0,
            SchemeId = 0,
        };
    }

    public Dictionary<string, double> GetAllFleaPrices()
    {
        return _ragfairPriceService.GetAllFleaPrices();
    }

    public Dictionary<string, double> GetStaticPrices()
    {
        return _ragfairPriceService.GetAllStaticPrices();
    }

    public RagfairOffer? GetOfferById(string sessionId, GetRagfairOfferByIdRequest request)
    {
        var offers = _ragfairOfferService.GetOffers();
        var offerToReturn = offers.FirstOrDefault((offer) => offer.InternalId == request.Id);

        return offerToReturn;
    }
}
