using System.Text.RegularExpressions;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Hideout;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Player;
using Core.Models.Eft.Profile;
using Core.Models.Eft.Ragfair;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Routers;
using Core.Servers;
using Core.Services;
using Core.Utils;
using SptCommon.Annotations;
using SptCommon.Extensions;

namespace Core.Helpers;

[Injectable]
public class RagfairOfferHelper(
    ISptLogger<RagfairOfferHelper> _logger,
    TimeUtil _timeUtil,
    HashUtil _hashUtil,
    BotHelper _botHelper,
    RagfairSortHelper _ragfairSortHelper,
    PresetHelper _presetHelper,
    RagfairHelper _ragfairHelper,
    PaymentHelper _paymentHelper,
    TraderHelper _traderHelper,
    QuestHelper _questHelper,
    RagfairServerHelper _ragfairServerHelper,
    ItemHelper _itemHelper,
    DatabaseService _databaseService,
    RagfairOfferService _ragfairOfferService,
    LocaleService _localeService,
    LocalisationService _localisationService,
    MailSendService _mailSendService,
    RagfairRequiredItemsService _ragfairRequiredItemsService,
    ProfileHelper _profileHelper,
    EventOutputHolder _eventOutputHolder,
    ConfigServer _configServer)
{
    protected RagfairConfig _ragfairConfig = _configServer.GetConfig<RagfairConfig>();
    protected BotConfig _botConfig = _configServer.GetConfig<BotConfig>();
    protected static string _goodSoldTemplate = "5bdabfb886f7743e152e867e 0"; // Your {soldItem} {itemCount} items were bought by {buyerNickname}.

    /// <summary>
    ///     Passthrough to ragfairOfferService.getOffers(), get flea offers a player should see
    /// </summary>
    /// <param name="searchRequest">Data from client</param>
    /// <param name="itemsToAdd">ragfairHelper.filterCategories()</param>
    /// <param name="traderAssorts">Trader assorts</param>
    /// <param name="pmcData">Player profile</param>
    /// <returns>Offers the player should see</returns>
    public List<RagfairOffer> GetValidOffers(
        SearchRequestData searchRequest,
        List<string> itemsToAdd,
        Dictionary<string, TraderAssort> traderAssorts,
        PmcData pmcData)
    {
        var playerIsFleaBanned = _profileHelper.PlayerIsFleaBanned(pmcData);
        var tieredFlea = _ragfairConfig.TieredFlea;
        var tieredFleaLimitTypes = tieredFlea.UnlocksType;
        return _ragfairOfferService.GetOffers()
            .Where(
                offer =>
                {
                    if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData))
                    {
                        return false;
                    }

                    var isDisplayable = IsDisplayableOffer(
                        searchRequest,
                        itemsToAdd,
                        traderAssorts,
                        offer,
                        pmcData,
                        playerIsFleaBanned
                    );

                    if (!isDisplayable)
                    {
                        return false;
                    }

                    // Not trader offer + tiered flea enabled
                    if (tieredFlea.Enabled && !OfferIsFromTrader(offer))
                    {
                        CheckAndLockOfferFromPlayerTieredFlea(
                            tieredFlea,
                            offer,
                            tieredFleaLimitTypes.Keys.ToList(),
                            pmcData.Info.Level.Value
                        );
                    }

                    return true;
                }
            )
            .ToList();
    }

    /// <summary>
    ///     Disable offer if item is flagged by tiered flea config
    /// </summary>
    /// <param name="tieredFlea">Tiered flea settings from ragfair config</param>
    /// <param name="offer">Ragfair offer to check</param>
    /// <param name="tieredFleaLimitTypes">Dict of item types with player level to be viewable</param>
    /// <param name="playerLevel">Level of player viewing offer</param>
    protected void CheckAndLockOfferFromPlayerTieredFlea(
        TieredFlea tieredFlea,
        RagfairOffer offer,
        List<string> tieredFleaLimitTypes,
        int playerLevel)
    {
        var offerItemTpl = offer.Items.FirstOrDefault().Template;
        if (tieredFlea.AmmoTplUnlocks is not null && _itemHelper.IsOfBaseclass(offerItemTpl, BaseClasses.AMMO))
        {
            if (tieredFlea.AmmoTplUnlocks.TryGetValue(offerItemTpl, out var unlockLevel) && playerLevel < unlockLevel)
            {
                offer.Locked = true;

                return;
            }
        }

        // Check for a direct level requirement for the offer item
        if (tieredFlea.UnlocksTpl.TryGetValue(offerItemTpl, out var itemLevelRequirement))
        {
            if (playerLevel < itemLevelRequirement)
            {
                offer.Locked = true;

                return;
            }
        }

        // Optimisation - Ensure the item has at least one of the limited base types
        if (_itemHelper.IsOfBaseclasses(offerItemTpl, tieredFleaLimitTypes))
        {
            // Loop over flea types
            foreach (var tieredItemType in tieredFleaLimitTypes
                         .Where(tieredItemType => _itemHelper.IsOfBaseclass(offerItemTpl, tieredItemType)))
            {
                if (playerLevel < tieredFlea.UnlocksType[tieredItemType])
                {
                    offer.Locked = true;
                }

                break;
            }
        }
    }

    /// <summary>
    ///     Get matching offers that require the desired item and filter out offers from non traders if player is below ragfair
    ///     unlock level
    /// </summary>
    /// <param name="searchRequest">Search request from client</param>
    /// <param name="pmcData">Player profile</param>
    /// <returns>Matching RagfairOffer objects</returns>
    public List<RagfairOffer> GetOffersThatRequireItem(SearchRequestData searchRequest, PmcData pmcData)
    {
        // Get all offers that require the desired item and filter out offers from non traders if player below ragifar unlock
        var requiredOffers = _ragfairRequiredItemsService.GetRequiredItemsById(searchRequest.NeededSearchId);
        var tieredFlea = _ragfairConfig.TieredFlea;
        var tieredFleaLimitTypes = tieredFlea.UnlocksType;
        return requiredOffers.Where(
                offer =>
                {
                    if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData))
                    {
                        return false;
                    }

                    if (tieredFlea.Enabled && !OfferIsFromTrader(offer))
                    {
                        CheckAndLockOfferFromPlayerTieredFlea(
                            tieredFlea,
                            offer,
                            tieredFleaLimitTypes.Keys.ToList(),
                            pmcData.Info.Level.Value
                        );
                    }

                    return true;
                }
            )
            .ToList();
    }

    /// <summary>
    ///     Get offers from flea/traders specifically when building weapon preset
    /// </summary>
    /// <param name="searchRequest">Search request data</param>
    /// <param name="itemsToAdd">string array of item tpls to search for</param>
    /// <param name="traderAssorts">All trader assorts player can access/buy</param>
    /// <param name="pmcData">Player profile</param>
    /// <returns>RagfairOffer array</returns>
    public List<RagfairOffer> GetOffersForBuild(
        SearchRequestData searchRequest,
        List<string> itemsToAdd,
        Dictionary<string, TraderAssort> traderAssorts,
        PmcData pmcData)
    {
        var offersMap = new Dictionary<string, List<RagfairOffer>>();
        var offersToReturn = new List<RagfairOffer>();
        var playerIsFleaBanned = _profileHelper.PlayerIsFleaBanned(pmcData);
        var tieredFlea = _ragfairConfig.TieredFlea;
        var tieredFleaLimitTypes = tieredFlea.UnlocksType;

        foreach (var desiredItemTpl in searchRequest.BuildItems)
        {
            var matchingOffers = _ragfairOfferService.GetOffersOfType(desiredItemTpl.Key);
            if (matchingOffers is null)
            {
                // No offers found for this item, skip
                continue;
            }

            foreach (var offer in matchingOffers)
            {
                // Don't show pack offers
                if (offer.SellInOnePiece.GetValueOrDefault(false))
                {
                    continue;
                }

                if (!PassesSearchFilterCriteria(searchRequest, offer, pmcData))
                {
                    continue;
                }

                if (
                    !IsDisplayableOffer(
                        searchRequest,
                        itemsToAdd,
                        traderAssorts,
                        offer,
                        pmcData,
                        playerIsFleaBanned
                    )
                )
                {
                    continue;
                }

                if (OfferIsFromTrader(offer))
                {
                    if (TraderBuyRestrictionReached(offer))
                    {
                        continue;
                    }

                    if (TraderOutOfStock(offer))
                    {
                        continue;
                    }

                    if (TraderOfferItemQuestLocked(offer, traderAssorts))
                    {
                        continue;
                    }

                    if (TraderOfferLockedBehindLoyaltyLevel(offer, pmcData))
                    {
                        continue;
                    }
                }

                // Tiered flea and not trader offer
                if (tieredFlea.Enabled && !OfferIsFromTrader(offer))
                {
                    CheckAndLockOfferFromPlayerTieredFlea(
                        tieredFlea,
                        offer,
                        tieredFleaLimitTypes.Keys.ToList(),
                        pmcData.Info.Level.Value
                    );

                    // Do not add offer to build if user does not have access to it
                    if (offer.Locked.GetValueOrDefault(false))
                    {
                        continue;
                    }
                }

                var key = offer.Items[0].Template;
                if (!offersMap.ContainsKey(key))
                {
                    offersMap.Add(key, []);
                }

                offersMap[key].Add(offer);
            }
        }

        // Get best offer for each item to show on screen
        var offersToSort = new List<RagfairOffer>();
        foreach (var possibleOffers in offersMap.Values)
        {
            // prepare temp list for offers
            offersToSort.Clear();

            // Remove offers with locked = true (quest locked) when > 1 possible offers
            // single trader item = shows greyed out
            // multiple offers for item = is greyed out
            if (possibleOffers.Count > 1)
            {
                var lockedOffers = GetLoyaltyLockedOffers(possibleOffers, pmcData);

                // Exclude locked offers + above loyalty locked offers if at least 1 was found
                offersToSort = possibleOffers.Where(
                        offer => !(offer.Locked.GetValueOrDefault(false) || lockedOffers.Contains(offer.Id))
                    )
                    .ToList();

                // Exclude trader offers over their buy restriction limit
                offersToSort = GetOffersInsideBuyRestrictionLimits(possibleOffers);
            }

            // Sort offers by price and pick the best
            var offer = _ragfairSortHelper.SortOffers(offersToSort, RagfairSort.PRICE)[0];
            offersToReturn.Add(offer);
        }

        return offersToReturn;
    }

    /**
     * Should a ragfair offer be visible to the player
     * @param searchRequest Search request
     * @param itemsToAdd ?
     * @param traderAssorts Trader assort items - used for filtering out locked trader items
     * @param offer The flea offer
     * @param pmcProfile Player profile
     * @returns True = should be shown to player
     */
    private bool IsDisplayableOffer(SearchRequestData searchRequest, List<string> itemsToAdd,
        Dictionary<string, TraderAssort> traderAssorts, RagfairOffer offer, PmcData pmcProfile,
        bool playerIsFleaBanned = false)
    {
        var offerRootItem = offer.Items[0];
        /** Currency offer is sold for */
        var moneyTypeTpl = offer.Requirements[0].Template;
        var isTraderOffer = _databaseService.GetTraders().ContainsKey(offer.User.Id);

        if (!isTraderOffer && playerIsFleaBanned)
        {
            return false;
        }

        // Offer root items tpl not in searched for array
        if (!itemsToAdd.Contains(offerRootItem.Template))
        {
            // skip items we shouldn't include
            return false;
        }

        // Performing a required search and offer doesn't have requirement for item
        if (
            !string.IsNullOrEmpty(searchRequest.NeededSearchId) &&
            !offer.Requirements.Any(requirement => requirement.Template == searchRequest.NeededSearchId)
        )
        {
            return false;
        }

        // Weapon/equipment search + offer is preset
        if (
            searchRequest.BuildItems.Count == 0 && // Prevent equipment loadout searches filtering out presets
            searchRequest.BuildCount is not null &&
            _presetHelper.HasPreset(offerRootItem.Template))
        {
            return false;
        }

        // commented out as required search "which is for checking offers that are barters"
        // has info.removeBartering as true, this if statement removed barter items.
        if (searchRequest.RemoveBartering.GetValueOrDefault(false) && !_paymentHelper.IsMoneyTpl(moneyTypeTpl))
        {
            // Don't include barter offers
            return false;
        }

        if (offer.RequirementsCost is null)
        {
            // Don't include offers with undefined or NaN in it
            return false;
        }

        // Handle trader items to remove items that are not available to the user right now
        // e.g. required search for "lamp" shows 4 items, 3 of which are not available to a new player
        // filter those out
        if (isTraderOffer)
        {
            if (!traderAssorts.ContainsKey(offer.User.Id))
            {
                // trader not visible on flea market
                return false;
            }

            if (
                !traderAssorts[offer.User.Id].Items.Any(item => { return item.Id == offer.Root; })
            )
            {
                // skip (quest) locked items
                return false;
            }
        }

        return true;
    }

    /// <summary>
    ///     Get offers that have not exceeded buy limits
    /// </summary>
    /// <param name="possibleOffers">offers to process</param>
    /// <returns>Offers</returns>
    protected List<RagfairOffer> GetOffersInsideBuyRestrictionLimits(List<RagfairOffer> possibleOffers)
    {
        // Check offer has buy limit + is from trader + current buy count is at or over max
        return possibleOffers.Where(
                offer =>
                {
                    if (
                        offer.BuyRestrictionMax is null &&
                        OfferIsFromTrader(offer) &&
                        offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax
                    )
                    {
                        if (offer.BuyRestrictionCurrent >= offer.BuyRestrictionMax)
                        {
                            return false;
                        }
                    }

                    // Doesnt have buy limits, retrun offer
                    return true;
                }
            )
            .ToList();
    }

    /// <summary>
    ///     Check if offer is from trader standing the player does not have
    /// </summary>
    /// <param name="offer">Offer to check</param>
    /// <param name="pmcProfile">Player profile</param>
    /// <returns>True if item is locked, false if item is purchaseable</returns>
    protected bool TraderOfferLockedBehindLoyaltyLevel(RagfairOffer offer, PmcData pmcProfile)
    {
        if (!pmcProfile.TradersInfo.TryGetValue(offer.User.Id, out var userTraderSettings))
        {
            _logger.Warning(
                $"Trader: {offer.User.Id} not found in profile, assuming offer is not locked being loyalty level"
            );
            return false;
        }

        return userTraderSettings.LoyaltyLevel < offer.LoyaltyLevel;
    }

    /// <summary>
    ///     Check if offer item is quest locked for current player by looking at sptQuestLocked property in traders
    ///     barter_scheme
    /// </summary>
    /// <param name="offer">Offer to check is quest locked</param>
    /// <param name="traderAssorts">all trader assorts for player</param>
    /// <returns>true if quest locked</returns>
    public bool TraderOfferItemQuestLocked(RagfairOffer offer, Dictionary<string, TraderAssort> traderAssorts)
    {
        var itemIds = offer.Items.Select(x => x.Id).ToList();
        //foreach (var item in offer.Items)
        //{
        //    traderAssorts.TryGetValue(offer.User.Id, out var assorts);
        //    foreach (var barterKvP in assorts.BarterScheme.Where(x => itemIds.Contains(x.Key)))
        //    {
        //        foreach (var subBarter in barterKvP.Value)
        //        {
        //            if (subBarter.Any(subBarter => subBarter.SptQuestLocked.GetValueOrDefault(false)))
        //            {
        //                return true;
        //            }
        //        }
        //    }
        //}

        foreach (var item in offer.Items)
        {
            traderAssorts.TryGetValue(offer.User.Id, out var assorts);
            if (assorts.BarterScheme
                .Where(x => itemIds.Contains(x.Key))
                .Any(
                    barterKvP => barterKvP.Value
                        .Any(
                            subBarter => subBarter
                                .Any(subBarter => subBarter.SptQuestLocked.GetValueOrDefault(false))
                        )
                ))
            {
                return true;
            }
        }

        // Fallback, nothing found
        return false;
    }

    /// <summary>
    ///     Has trader offer ran out of stock to sell to player
    /// </summary>
    /// <param name="offer">Offer to check stock of</param>
    /// <returns>true if out of stock</returns>
    protected bool TraderOutOfStock(RagfairOffer offer)
    {
        if (offer?.Items?.Count == 0)
        {
            return true;
        }

        return offer.Items[0]?.Upd?.StackObjectsCount == 0;
    }

    /// <summary>
    ///     Check if trader offers' BuyRestrictionMax value has been reached
    /// </summary>
    /// <param name="offer">Offer to check restriction properties of</param>
    /// <returns>true if restriction reached, false if no restrictions/not reached</returns>
    protected bool TraderBuyRestrictionReached(RagfairOffer offer)
    {
        var traderAssorts = _traderHelper.GetTraderAssortsByTraderId(offer.User.Id).Items;

        // Find item being purchased from traders assorts
        var assortData = traderAssorts.FirstOrDefault(item => item.Id == offer.Items[0].Id);
        if (assortData is null)
        {
            // No trader assort data
            _logger.Warning(
                $"Unable to find trader: " +
                $"${offer.User.Nickname}assort for item: ${_itemHelper.GetItemName(offer.Items[0].Template)} " +
                $"{offer.Items[0].Template}, cannot check if buy restriction reached"
            );

            return false;
        }

        if (assortData.Upd is null)
        {
            // No Upd = no chance of limits
            return false;
        }

        // No restriction values
        // Can't use !assortData.upd.BuyRestrictionX as value could be 0
        if (assortData.Upd.BuyRestrictionMax is null || assortData.Upd.BuyRestrictionCurrent is null)
        {
            return false;
        }

        // Current equals max, limit reached
        if (assortData.Upd.BuyRestrictionCurrent >= assortData.Upd.BuyRestrictionMax)
        {
            return true;
        }

        return false;
    }

    protected List<string> GetLoyaltyLockedOffers(List<RagfairOffer> offers, PmcData pmcProfile)
    {
        var loyaltyLockedOffers = new List<string>();
        foreach (var offer in offers.Where(offer => OfferIsFromTrader(offer)))
            if (pmcProfile.TradersInfo.TryGetValue(offer.User.Id, out var traderDetails) &&
                traderDetails.LoyaltyLevel < offer.LoyaltyLevel)
            {
                loyaltyLockedOffers.Add(offer.Id);
            }

        return loyaltyLockedOffers;
    }

    /**
     * Process all player-listed flea offers for a desired profile
     * @param sessionId Session id to process offers for
     * @returns true = complete
     */
    public bool ProcessOffersOnProfile(string sessionId)
    {
        var timestamp = _timeUtil.GetTimeStamp();
        var profileOffers = GetProfileOffers(sessionId);

        // No offers, don't do anything
        if (profileOffers?.Count == 0)
        {
            return true;
        }

        foreach (var offer in profileOffers)
            if (offer.SellResults?.Count > 0 && timestamp >= offer.SellResults[0].SellTime)
            {
                // Checks first item, first is spliced out of array after being processed
                // Item sold
                var totalItemsCount = 1d;
                var boughtAmount = 1;

                if (!offer.SellInOnePiece.GetValueOrDefault(false))
                {
                    // offer.items.reduce((sum, item) => sum + item.upd?.StackObjectsCount ?? 0, 0);
                    totalItemsCount = GetTotalStackCountSize([offer.Items]);
                    boughtAmount = offer.SellResults[0].Amount.Value;
                }

                var ratingToAdd = offer.SummaryCost / totalItemsCount * boughtAmount;
                IncreaseProfileRagfairRating(_profileHelper.GetFullProfile(sessionId), ratingToAdd.Value);

                CompleteOffer(sessionId, offer, boughtAmount);
                offer.SellResults.Splice(0, 1); // Remove the sell result object now its been processed
            }

        return true;
    }

    /**
     * Count up all rootitem StackObjectsCount properties of an array of items
     * @param itemsInInventoryToList items to sum up
     * @returns Total stack count
     */
    public double GetTotalStackCountSize(List<List<Item>> itemsInInventoryToList)
    {
        var total = 0d;
        foreach (var itemAndChildren in itemsInInventoryToList)
            // Only count the root items stack count in total
            total += itemAndChildren[0]?.Upd?.StackObjectsCount.GetValueOrDefault(1) ?? 1;

        return total;
    }

    /**
     * Add amount to players ragfair rating
     * @param sessionId Profile to update
     * @param amountToIncrementBy Raw amount to add to players ragfair rating (excluding the reputation gain multiplier)
     */
    public void IncreaseProfileRagfairRating(SptProfile profile, double? amountToIncrementBy)
    {
        var ragfairGlobalsConfig = _databaseService.GetGlobals().Configuration.RagFair;

        profile.CharacterData.PmcData.RagfairInfo.IsRatingGrowing = true;
        if (amountToIncrementBy is null)
        {
            _logger.Warning($"Unable to increment ragfair rating, value was not a number: {amountToIncrementBy}");

            return;
        }

        profile.CharacterData.PmcData.RagfairInfo.Rating +=
            ragfairGlobalsConfig.RatingIncreaseCount /
            ragfairGlobalsConfig.RatingSumForIncrease *
            amountToIncrementBy;
    }

    /**
     * Return all offers a player has listed on a desired profile
     * @param sessionId Session id
     * @returns List of ragfair offers
     */
    protected List<RagfairOffer> GetProfileOffers(string sessionId)
    {
        var profile = _profileHelper.GetPmcProfile(sessionId);

        if (profile.RagfairInfo?.Offers is null)
        {
            return [];
        }

        return profile.RagfairInfo.Offers;
    }

    /**
     * Delete an offer from a desired profile and from ragfair offers
     * @param sessionId Session id of profile to delete offer from
     * @param offerId Id of offer to delete
     */
    protected void DeleteOfferById(string sessionId, string offerId)
    {
        var profileRagfairInfo = _profileHelper.GetPmcProfile(sessionId).RagfairInfo;
        var index = profileRagfairInfo.Offers.FindIndex(o => o.Id == offerId);
        profileRagfairInfo.Offers.Splice(index, 1);

        // Also delete from ragfair
        _ragfairOfferService.RemoveOfferById(offerId);
    }

    /**
     * Complete the selling of players' offer
     * @param sessionID Session id
     * @param offer Sold offer details
     * @param boughtAmount Amount item was purchased for
     * @returns ItemEventRouterResponse
     */
    public ItemEventRouterResponse CompleteOffer(string offerOwnerSessionId, RagfairOffer offer, int boughtAmount)
    {
        var itemTpl = offer.Items[0].Template;
        var paymentItemsToSendToPlayer = new List<Item>();
        var offerStackCount = offer.Items[0].Upd.StackObjectsCount;
        var sellerProfile = _profileHelper.GetPmcProfile(offerOwnerSessionId);

        // Pack or ALL items of a multi-offer were bought - remove entire ofer
        if (offer.SellInOnePiece.GetValueOrDefault(false) || boughtAmount == offerStackCount)
        {
            DeleteOfferById(offerOwnerSessionId, offer.Id);
        }
        else
        {
            var offerRootItem = offer.Items[0];

            // Reduce offer root items stack count
            offerRootItem.Upd.StackObjectsCount -= boughtAmount;
        }

        // Assemble payment to send to seller now offer was purchased
        foreach (var requirement in offer.Requirements) {
            // Create an item template item
            var requestedItem = new Item{
                Id = _hashUtil.Generate(),
                Template = requirement.Template,
                Upd = new Upd{ StackObjectsCount = requirement.Count * boughtAmount },
            };

            var stacks = _itemHelper.SplitStack(requestedItem);
            foreach (var item in stacks) {
                var outItems = new List<Item> { item };

                // TODO - is this code used?, may have been when adding barters to flea was still possible for player
                if (requirement.OnlyFunctional.GetValueOrDefault(false))
                {
                    var presetItems = _ragfairServerHelper.GetPresetItemsByTpl(item);
                    if (presetItems.Count > 0)
                    {
                        outItems.Add(presetItems[0]);
                    }
                }

                paymentItemsToSendToPlayer.AddRange(outItems);
            }
        }

        var ragfairDetails = new MessageContentRagfair{
            OfferId = offer.Id,
            // pack-offers NEED to be the full item count,
            // otherwise it only removes 1 from the pack, leaving phantom offer on client ui
            Count = offer.SellInOnePiece.GetValueOrDefault(false) ? offerStackCount.Value : boughtAmount,
            HandbookId = itemTpl };

        _mailSendService.SendDirectNpcMessageToPlayer(
            offerOwnerSessionId,
            _traderHelper.GetTraderById(Traders.RAGMAN).ToString(),
            MessageType.FLEAMARKET_MESSAGE,
            GetLocalisedOfferSoldMessage(itemTpl, boughtAmount),
            paymentItemsToSendToPlayer,
            _timeUtil.GetHoursAsSeconds((int)_questHelper.GetMailItemRedeemTimeHoursForProfile(sellerProfile).Value),
            null,
            ragfairDetails);

    // Adjust sellers sell sum values
    sellerProfile.RagfairInfo.SellSum ??= 0;
    sellerProfile.RagfairInfo.SellSum += offer.SummaryCost;

        return _eventOutputHolder.GetOutput(offerOwnerSessionId);
    }

    /**
     * Get a localised message for when players offer has sold on flea
     * @param itemTpl Item sold
     * @param boughtAmount How many were purchased
     * @returns Localised message text
     */
    protected string GetLocalisedOfferSoldMessage(string itemTpl, int boughtAmount)
    {
        // Generate a message to inform that item was sold
        var globalLocales = _localeService.GetLocaleDb();
        if (!globalLocales.TryGetValue(_goodSoldTemplate, out var soldMessageLocaleGuid))
        {
            _logger.Error(
                _localisationService.GetText("ragfair-unable_to_find_locale_by_key", _goodSoldTemplate));
        }

        // Used to replace tokens in sold message sent to player
        var messageKey = $"{itemTpl}Name";
        var hasKey = globalLocales.TryGetValue(messageKey, out var value);
        
        var tplVars = new SystemData {
            SoldItem = hasKey ? value : itemTpl,
            BuyerNickname = _botHelper.GetPmcNicknameOfMaxLength(_botConfig.BotNameLengthLimit),
            ItemCount = boughtAmount,
        };

        throw new NotImplementedException();

        //const offerSoldMessageText = soldMessageLocaleGuid.replace(/{\w +}/ g, (matched) => {
        //    return tplVars[matched.replace(/{|}/ g, "")];
        //});

        //return offerSoldMessageText.replace(/ "/g, "");
    }

    /**
     * Check an offer passes the various search criteria the player requested
     * @param searchRequest Client search request
     * @param offer Offer to check
     * @param pmcData Player profile
     * @returns True if offer passes criteria
     */
    protected bool PassesSearchFilterCriteria(SearchRequestData searchRequest, RagfairOffer offer, PmcData pmcData)
    {
        var isDefaultUserOffer = offer.User.MemberType == MemberCategory.Default;
        var offerRootItem = offer.Items[0];
        var offerMoneyTypeTpl = offer.Requirements[0].Template;
        var isTraderOffer = OfferIsFromTrader(offer);

        if (pmcData.Info.Level < _databaseService.GetGlobals().Configuration.RagFair.MinUserLevel && isDefaultUserOffer)
        {
            // Skip item if player is < global unlock level (default is 15) and item is from a dynamically generated source
            return false;
        }

        if (searchRequest.OfferOwnerType == OfferOwnerType.TRADEROWNERTYPE && !isTraderOffer)
        {
            // don't include player offers
            return false;
        }

        if (searchRequest.OfferOwnerType == OfferOwnerType.PLAYEROWNERTYPE && isTraderOffer)
        {
            // don't include trader offers
            return false;
        }

        if (
            searchRequest.OneHourExpiration.GetValueOrDefault(false) &&
            offer.EndTime - _timeUtil.GetTimeStamp() > TimeUtil.OneHourAsSeconds
        )
        {
            // offer expires within an hour
            return false;
        }

        if (searchRequest.QuantityFrom > 0 && searchRequest.QuantityFrom >= offerRootItem.Upd.StackObjectsCount)
        {
            // too little items to offer
            return false;
        }

        if (searchRequest.QuantityTo > 0 && searchRequest.QuantityTo <= offerRootItem.Upd.StackObjectsCount)
        {
            // too many items to offer
            return false;
        }

        if (searchRequest.OnlyFunctional.GetValueOrDefault(false) && !IsItemFunctional(offerRootItem, offer))
        {
            // don't include non-functional items
            return false;
        }

        if (offer.Items.Count == 1)
        {
            // Single item
            if (
                IsConditionItem(offerRootItem) &&
                !ItemQualityInRange(offerRootItem, searchRequest.ConditionFrom.Value, searchRequest.ConditionTo.Value)
            )
            {
                return false;
            }
        }
        else
        {
            var itemQualityPercent = _itemHelper.GetItemQualityModifierForItems(offer.Items) * 100;
            if (itemQualityPercent < searchRequest.ConditionFrom)
            {
                return false;
            }

            if (itemQualityPercent > searchRequest.ConditionTo)
            {
                return false;
            }
        }

        if (searchRequest.Currency > 0 && _paymentHelper.IsMoneyTpl(offerMoneyTypeTpl))
        {
            // Use 'currencies' as mapping for the money choice dropdown, e.g. 0 = all, 2 = "USD;
            string[] currencies = ["all", "RUB", "USD", "EUR"];
            if (_ragfairHelper.GetCurrencyTag(offerMoneyTypeTpl) != currencies[searchRequest.Currency.Value])
            {
                // Don't include item paid in wrong currency
                return false;
            }
        }

        if (searchRequest.PriceFrom > 0 && searchRequest.PriceFrom >= offer.RequirementsCost)
        {
            // price is too low
            return false;
        }

        if (searchRequest.PriceTo > 0 && searchRequest.PriceTo <= offer.RequirementsCost)
        {
            // price is too high
            return false;
        }

        // Passes above checks, search criteria filters have not filtered offer out
        return true;
    }

    /**
     * Check that the passed in offer item is functional
     * @param offerRootItem The root item of the offer
     * @param offer Flea offer to check
     * @returns True if the given item is functional
     */
    public bool IsItemFunctional(Item offerRootItem, RagfairOffer offer)
    {
        // Non-preset weapons/armor are always functional
        if (!_presetHelper.HasPreset(offerRootItem.Template))
        {
            return true;
        }

        // For armor items that can hold mods, make sure the item count is at least the amount of required plates
        if (_itemHelper.ArmorItemCanHoldMods(offerRootItem.Template))
        {
            var offerRootTemplate = _itemHelper.GetItem(offerRootItem.Template).Value;
            var requiredPlateCount = offerRootTemplate.Properties.Slots
                ?.Where(item => item.Required.GetValueOrDefault(false))
                ?.Count();

            return offer.Items.Count > requiredPlateCount;
        }

        // For other presets, make sure the offer has more than 1 item
        return offer.Items.Count > 1;
    }

    /// <summary>
    ///     Should a ragfair offer be visible to the player
    /// </summary>
    /// <param name="searchRequest">Search request</param>
    /// <param name="itemsToAdd">?</param>
    /// <param name="traderAssorts">Trader assort items - used for filtering out locked trader items</param>
    /// <param name="offer">The flea offer</param>
    /// <param name="pmcProfile">Player profile</param>
    /// <param name="playerIsFleaBanned">Optional parameter</param>
    /// <returns>True = should be shown to player</returns>
    public bool DisplayableOffer(
        SearchRequestData searchRequest,
        List<string> itemsToAdd,
        Dictionary<string, TraderAssort> traderAssorts,
        RagfairOffer offer,
        PmcData pmcProfile,
        bool? playerIsFleaBanned = null
    )
    {
        throw new NotImplementedException();
    }

    public bool IsDisplayableOfferThatNeedsItem(SearchRequestData searchRequest, RagfairOffer offer)
    {
        return offer.Requirements.Any(requirement => requirement.Template == searchRequest.NeededSearchId);
    }

    /// <summary>
    ///     Does the passed in item have a condition property
    /// </summary>
    /// <param name="item">Item to check</param>
    /// <returns>True if has condition</returns>
    protected bool IsConditionItem(Item item)
    {
        // thanks typescript, undefined assertion is not returnable since it
        // tries to return a multi-type object
        if (item.Upd is null)
        {
            return false;
        }

        return item.Upd.MedKit is not null ||
               item.Upd.Repairable is not null ||
               item.Upd.Resource is not null ||
               item.Upd.FoodDrink is not null ||
               item.Upd.Key is not null ||
               item.Upd.RepairKit is not null;
    }

    /// <summary>
    ///     Is items quality value within desired range
    /// </summary>
    /// <param name="item">Item to check quality of</param>
    /// <param name="min">Desired minimum quality</param>
    /// <param name="max">Desired maximum quality</param>
    /// <returns>True if in range</returns>
    protected bool ItemQualityInRange(Item item, int min, int max)
    {
        var itemQualityPercentage = 100 * _itemHelper.GetItemQualityModifier(item);
        if (min > 0 && min > itemQualityPercentage)
        {
            // Item condition too low
            return false;
        }

        if (max < 100 && max <= itemQualityPercentage)
        {
            // Item condition too high
            return false;
        }

        return true;
    }

    /// <summary>
    ///     Does this offer come from a trader
    /// </summary>
    /// <param name="offer">Offer to check</param>
    /// <returns>True = from trader</returns>
    public bool OfferIsFromTrader(RagfairOffer offer)
    {
        return offer.User.MemberType == MemberCategory.Trader;
    }
}
