using InventoryApi.Models;
using MongoDB.Bson;
using MongoDB.Driver;

namespace InventoryApi.Services;

public class InventoryStore
{
    private const string CharacterOwnerType = "character";
    private const string LocationOwnerType = "location";
    private const int CharacterInventorySlotCount = 6;
    private const string OwnerIndexName = "owner_type_1_owner_id_1";
    private const string SlotIndexName = "owner_type_1_owner_id_1_slot_1";
    private const string EquippedSlotIndexName = "owner_type_1_owner_id_1_equipped_slot_1";
    private const string DefinitionCategoryIndexName = "category_1";

    private readonly IMongoCollection<InventoryItem> _items;
    private readonly IMongoCollection<ItemDefinition> _definitions;
    private readonly IMongoCollection<CharacterOwnerDocument> _characters;
    private readonly IMongoCollection<LocationOwnerDocument> _locations;
    private readonly IMongoClient _client;
    private readonly string _dbName;

    public sealed record GrantResult(List<InventoryItem> Items, int RequestedQuantity, int GrantedQuantity, int OverflowQuantity);

    public InventoryStore(IConfiguration cfg)
    {
        var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
        _dbName = cfg["MongoDB:DatabaseName"] ?? "promiscuity";
        _client = new MongoClient(cs);
        var db = _client.GetDatabase(_dbName);
        _items = db.GetCollection<InventoryItem>("InventoryItems");
        _definitions = db.GetCollection<ItemDefinition>("ItemDefinitions");
        _characters = db.GetCollection<CharacterOwnerDocument>("Characters");
        _locations = db.GetCollection<LocationOwnerDocument>("Locations");

        EnsureIndexes();
    }

    public async Task<OwnerAccessResult> ResolveOwnerAsync(string ownerType, string ownerId, string userId, bool allowAnyOwner)
    {
        var normalizedOwnerType = NormalizeOwnerType(ownerType);
        if (normalizedOwnerType is null)
            return new OwnerAccessResult { IsSupported = false };

        if (normalizedOwnerType == CharacterOwnerType)
        {
            var character = await _characters.Find(c => c.Id == ownerId).FirstOrDefaultAsync();
            if (character is null)
            {
                return new OwnerAccessResult
                {
                    IsSupported = true,
                    Exists = false,
                    OwnerType = normalizedOwnerType,
                    OwnerId = ownerId
                };
            }

            var authorized = allowAnyOwner || character.OwnerUserId == userId;
            return new OwnerAccessResult
            {
                IsSupported = true,
                Exists = true,
                IsAuthorized = authorized,
                OwnerType = normalizedOwnerType,
                OwnerId = ownerId,
                OwnerUserId = character.OwnerUserId
            };
        }

        var location = await _locations.Find(l => l.Id == ownerId).FirstOrDefaultAsync();
        return new OwnerAccessResult
        {
            IsSupported = true,
            Exists = location is not null,
            IsAuthorized = location is not null,
            OwnerType = normalizedOwnerType,
            OwnerId = ownerId,
            OwnerUserId = null
        };
    }

    public Task<List<InventoryItem>> GetByOwnerAsync(string ownerType, string ownerId) =>
        _items.Find(i => i.OwnerType == ownerType && i.OwnerId == ownerId)
            .SortBy(i => i.EquippedSlot)
            .ThenBy(i => i.Slot)
            .ThenBy(i => i.ItemKey)
            .ToListAsync();

    public async Task<Dictionary<string, List<InventoryItem>>> GetByOwnersAsync(string ownerType, IEnumerable<string> ownerIds)
    {
        var normalizedOwnerType = NormalizeOwnerType(ownerType);
        if (normalizedOwnerType is null)
            return [];

        var ids = ownerIds
            .Where(id => !string.IsNullOrWhiteSpace(id))
            .Select(id => id.Trim())
            .Distinct(StringComparer.Ordinal)
            .ToList();
        if (ids.Count == 0)
            return [];

        var items = await _items.Find(i => i.OwnerType == normalizedOwnerType && ids.Contains(i.OwnerId))
            .SortBy(i => i.OwnerId)
            .ThenBy(i => i.EquippedSlot)
            .ThenBy(i => i.Slot)
            .ThenBy(i => i.ItemKey)
            .ToListAsync();

        return items
            .GroupBy(item => item.OwnerId, StringComparer.Ordinal)
            .ToDictionary(group => group.Key, group => group.ToList(), StringComparer.Ordinal);
    }

    public Task<List<ItemDefinition>> ListItemDefinitionsAsync() =>
        _definitions.Find(Builders<ItemDefinition>.Filter.Empty).SortBy(d => d.ItemKey).ToListAsync();

    public async Task<ItemDefinition?> GetItemDefinitionAsync(string itemKey) =>
        await _definitions.Find(d => d.ItemKey == NormalizeItemKey(itemKey)).FirstOrDefaultAsync();

    public async Task<ItemDefinition?> CreateItemDefinitionAsync(CreateItemDefinitionRequest req)
    {
        var itemKey = NormalizeItemKey(req.ItemKey);
        var existing = await GetItemDefinitionAsync(itemKey);
        if (existing is not null)
            return null;

        var definition = new ItemDefinition
        {
            ItemKey = itemKey,
            DisplayName = req.DisplayName.Trim(),
            Stackable = req.Stackable,
            MaxStackSize = req.Stackable ? req.MaxStackSize : 1,
            Category = string.IsNullOrWhiteSpace(req.Category) ? "misc" : req.Category.Trim().ToLowerInvariant(),
            EquipSlot = string.IsNullOrWhiteSpace(req.EquipSlot) ? null : req.EquipSlot.Trim().ToLowerInvariant(),
            CreatedUtc = DateTime.UtcNow,
            UpdatedUtc = DateTime.UtcNow
        };

        await _definitions.InsertOneAsync(definition);
        return definition;
    }

    public async Task<ItemDefinition?> UpdateItemDefinitionAsync(string itemKey, UpdateItemDefinitionRequest req)
    {
        var existing = await GetItemDefinitionAsync(itemKey);
        if (existing is null)
            return null;

        existing.DisplayName = req.DisplayName.Trim();
        existing.Stackable = req.Stackable;
        existing.MaxStackSize = req.Stackable ? req.MaxStackSize : 1;
        existing.Category = string.IsNullOrWhiteSpace(req.Category) ? "misc" : req.Category.Trim().ToLowerInvariant();
        existing.EquipSlot = string.IsNullOrWhiteSpace(req.EquipSlot) ? null : req.EquipSlot.Trim().ToLowerInvariant();
        existing.UpdatedUtc = DateTime.UtcNow;

        await _definitions.ReplaceOneAsync(d => d.ItemKey == existing.ItemKey, existing);
        return existing;
    }

    public async Task<GrantResult> GrantAsync(OwnerAccessResult owner, GrantInventoryItemRequest req, ItemDefinition definition)
    {
        var normalizedKey = NormalizeItemKey(req.ItemKey);
        var remaining = req.Quantity;
        if (definition.Stackable)
        {
            var targetSlot = req.PreferredSlot;
            var existingStacks = await _items.Find(i =>
                i.OwnerType == owner.OwnerType &&
                i.OwnerId == owner.OwnerId &&
                i.ItemKey == normalizedKey &&
                i.EquippedSlot == null &&
                i.Slot != null)
                .SortBy(i => i.Slot)
                .ToListAsync();

            foreach (var existing in existingStacks)
            {
                if (remaining <= 0)
                    break;

                var availableSpace = definition.MaxStackSize - existing.Quantity;
                if (availableSpace <= 0)
                    continue;

                var added = Math.Min(remaining, availableSpace);
                existing.Quantity += added;
                existing.UpdatedUtc = DateTime.UtcNow;
                await ReplaceItemAsync(existing);
                remaining -= added;
            }

            while (remaining > 0)
            {
                var slot = targetSlot ?? await FindFirstOpenSlotAsync(owner.OwnerType, owner.OwnerId);
                if (slot is null)
                    break;

                var stackQuantity = Math.Min(remaining, definition.MaxStackSize);
                await InsertItemAsync(new InventoryItem
                {
                    ItemKey = normalizedKey,
                    Quantity = stackQuantity,
                    OwnerType = owner.OwnerType,
                    OwnerId = owner.OwnerId,
                    OwnerUserId = owner.OwnerUserId,
                    Slot = slot.Value
                });
                remaining -= stackQuantity;

                targetSlot = null;
            }

            var stackItems = await GetByOwnerAsync(owner.OwnerType, owner.OwnerId);
            return new GrantResult(stackItems, req.Quantity, req.Quantity - remaining, remaining);
        }

        var nextPreferredSlot = req.PreferredSlot;
        while (remaining > 0)
        {
            var slot = nextPreferredSlot ?? await FindFirstOpenSlotAsync(owner.OwnerType, owner.OwnerId);
            if (slot is null)
                break;

            await InsertItemAsync(new InventoryItem
            {
                ItemKey = normalizedKey,
                Quantity = 1,
                OwnerType = owner.OwnerType,
                OwnerId = owner.OwnerId,
                OwnerUserId = owner.OwnerUserId,
                Slot = slot.Value
            });
            remaining -= 1;
            nextPreferredSlot = null;
        }
        var nonStackItems = await GetByOwnerAsync(owner.OwnerType, owner.OwnerId);
        return new GrantResult(nonStackItems, req.Quantity, req.Quantity - remaining, remaining);
    }

    public async Task<InventoryMutationResult> MoveAsync(OwnerAccessResult owner, MoveInventoryItemRequest req)
    {
        var item = await _items.Find(i => i.Id == req.ItemId).FirstOrDefaultAsync();
        if (item is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.ItemNotFound };
        if (item.OwnerType != owner.OwnerType || item.OwnerId != owner.OwnerId || item.EquippedSlot is not null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var quantity = req.Quantity ?? item.Quantity;
        if (quantity <= 0 || quantity > item.Quantity)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var definition = await GetItemDefinitionAsync(item.ItemKey);
        if (definition is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var existing = await FindItemBySlotAsync(owner.OwnerType, owner.OwnerId, req.ToSlot);
        if (existing is not null && existing.Id != item.Id)
        {
            if (!CanMerge(item, existing, definition))
                return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };

            var transferable = Math.Min(quantity, definition.MaxStackSize - existing.Quantity);
            if (transferable <= 0)
                return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };

            existing.Quantity += transferable;
            existing.UpdatedUtc = DateTime.UtcNow;
            await ReplaceItemAsync(existing);

            if (transferable == item.Quantity)
                await DeleteItemAsync(item.Id);
            else
            {
                item.Quantity -= transferable;
                item.UpdatedUtc = DateTime.UtcNow;
                await ReplaceItemAsync(item);
            }
        }
        else if (quantity == item.Quantity)
        {
            item.Slot = req.ToSlot;
            item.UpdatedUtc = DateTime.UtcNow;
            await ReplaceItemAsync(item);
        }
        else
        {
            item.Quantity -= quantity;
            item.UpdatedUtc = DateTime.UtcNow;
            await ReplaceItemAsync(item);

            await InsertItemAsync(new InventoryItem
            {
                ItemKey = item.ItemKey,
                Quantity = quantity,
                OwnerType = item.OwnerType,
                OwnerId = item.OwnerId,
                OwnerUserId = item.OwnerUserId,
                Slot = req.ToSlot
            });
        }

        return new InventoryMutationResult
        {
            Status = InventoryMutationStatus.Ok,
            OwnerType = owner.OwnerType,
            OwnerId = owner.OwnerId,
            Items = await GetByOwnerAsync(owner.OwnerType, owner.OwnerId)
        };
    }

    public async Task<InventoryMutationResult> TransferAsync(OwnerAccessResult fromOwner, OwnerAccessResult toOwner, TransferInventoryItemRequest req)
    {
        var item = await _items.Find(i => i.Id == req.ItemId).FirstOrDefaultAsync();
        if (item is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.ItemNotFound };

        if (item.OwnerType != fromOwner.OwnerType || item.OwnerId != fromOwner.OwnerId || item.EquippedSlot is not null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var quantity = req.Quantity ?? item.Quantity;
        if (quantity <= 0 || quantity > item.Quantity)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var definition = await GetItemDefinitionAsync(item.ItemKey);
        if (definition is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        InventoryItem? target = null;
        int? toSlot = req.ToSlot;
        if (toSlot is not null)
        {
            target = await FindItemBySlotAsync(toOwner.OwnerType, toOwner.OwnerId, toSlot.Value);
            if (target is not null && !CanMerge(item, target, definition))
                return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };
        }
        else if (definition.Stackable)
        {
            target = await FindMergeTargetAsync(toOwner.OwnerType, toOwner.OwnerId, item.ItemKey, definition.MaxStackSize);
            if (target is not null)
                toSlot = target.Slot;
        }

        if (toSlot is null)
        {
            toSlot = await FindFirstOpenSlotAsync(toOwner.OwnerType, toOwner.OwnerId);
            if (toSlot is null)
                return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };
            target = null;
        }

        if (target is not null)
        {
            var transferable = Math.Min(quantity, definition.MaxStackSize - target.Quantity);
            if (transferable <= 0)
            {
                return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };
            }

            target.Quantity += transferable;
            target.UpdatedUtc = DateTime.UtcNow;
            await ReplaceItemAsync(target);

            if (transferable == item.Quantity)
                await DeleteItemAsync(item.Id);
            else
            {
                item.Quantity -= transferable;
                item.UpdatedUtc = DateTime.UtcNow;
                await ReplaceItemAsync(item);
            }
        }
        else if (quantity == item.Quantity)
        {
            item.OwnerType = toOwner.OwnerType;
            item.OwnerId = toOwner.OwnerId;
            item.OwnerUserId = toOwner.OwnerUserId;
            item.Slot = toSlot;
            item.EquippedSlot = null;
            item.UpdatedUtc = DateTime.UtcNow;
            await ReplaceItemAsync(item);
        }
        else
        {
            item.Quantity -= quantity;
            item.UpdatedUtc = DateTime.UtcNow;
            await ReplaceItemAsync(item);

            await InsertItemAsync(new InventoryItem
            {
                ItemKey = item.ItemKey,
                Quantity = quantity,
                OwnerType = toOwner.OwnerType,
                OwnerId = toOwner.OwnerId,
                OwnerUserId = toOwner.OwnerUserId,
                Slot = toSlot
            });
        }

        var fromItems = await GetByOwnerAsync(fromOwner.OwnerType, fromOwner.OwnerId);
        var toItems = await GetByOwnerAsync(toOwner.OwnerType, toOwner.OwnerId);
        return new InventoryMutationResult
        {
            Status = InventoryMutationStatus.Ok,
            Items = fromItems.Concat(toItems).ToList()
        };
    }

    public async Task<InventoryMutationResult> ConsumeAsync(string itemId, int quantity, string userId, bool allowAnyOwner)
    {
        var item = await _items.Find(i => i.Id == itemId).FirstOrDefaultAsync();
        if (item is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.ItemNotFound };

        var access = await ResolveOwnerAsync(item.OwnerType, item.OwnerId, userId, allowAnyOwner);
        if (!access.Exists || !access.IsAuthorized)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };
        if (quantity > item.Quantity)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        if (quantity == item.Quantity)
            await DeleteItemAsync(item.Id);
        else
        {
            item.Quantity -= quantity;
            item.UpdatedUtc = DateTime.UtcNow;
            await ReplaceItemAsync(item);
        }

        return new InventoryMutationResult
        {
            Status = InventoryMutationStatus.Ok,
            OwnerType = item.OwnerType,
            OwnerId = item.OwnerId,
            Items = await GetByOwnerAsync(item.OwnerType, item.OwnerId)
        };
    }

    public async Task<InventoryMutationResult> EquipAsync(string itemId, string ownerId, string equipmentSlot, string userId, bool allowAnyOwner)
    {
        var owner = await ResolveOwnerAsync(CharacterOwnerType, ownerId, userId, allowAnyOwner);
        if (!owner.Exists || !owner.IsAuthorized)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var item = await _items.Find(i => i.Id == itemId).FirstOrDefaultAsync();
        if (item is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.ItemNotFound };
        if (item.OwnerType != CharacterOwnerType || item.OwnerId != ownerId || item.Slot is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };
        var definition = await GetItemDefinitionAsync(item.ItemKey);
        if (definition is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };
        if (!string.Equals(definition.EquipSlot, equipmentSlot.Trim(), StringComparison.OrdinalIgnoreCase))
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var equipped = await _items.Find(i =>
            i.OwnerType == CharacterOwnerType &&
            i.OwnerId == ownerId &&
            i.EquippedSlot == equipmentSlot).FirstOrDefaultAsync();
        if (equipped is not null && equipped.Id != item.Id)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };

        item.Slot = null;
        item.EquippedSlot = equipmentSlot.Trim();
        item.UpdatedUtc = DateTime.UtcNow;
        await ReplaceItemAsync(item);

        return new InventoryMutationResult
        {
            Status = InventoryMutationStatus.Ok,
            OwnerType = item.OwnerType,
            OwnerId = item.OwnerId,
            Items = await GetByOwnerAsync(item.OwnerType, item.OwnerId)
        };
    }

    public async Task<InventoryMutationResult> UnequipAsync(string itemId, string ownerId, int? preferredSlot, string userId, bool allowAnyOwner)
    {
        var owner = await ResolveOwnerAsync(CharacterOwnerType, ownerId, userId, allowAnyOwner);
        if (!owner.Exists || !owner.IsAuthorized)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var item = await _items.Find(i => i.Id == itemId).FirstOrDefaultAsync();
        if (item is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.ItemNotFound };
        if (item.OwnerType != CharacterOwnerType || item.OwnerId != ownerId || string.IsNullOrWhiteSpace(item.EquippedSlot))
            return new InventoryMutationResult { Status = InventoryMutationStatus.Invalid };

        var slot = preferredSlot ?? await FindFirstOpenSlotAsync(item.OwnerType, item.OwnerId);
        if (slot is null)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };
        var existing = await FindItemBySlotAsync(item.OwnerType, item.OwnerId, slot.Value);
        if (existing is not null && existing.Id != item.Id)
            return new InventoryMutationResult { Status = InventoryMutationStatus.Conflict };

        item.EquippedSlot = null;
        item.Slot = slot.Value;
        item.UpdatedUtc = DateTime.UtcNow;
        await ReplaceItemAsync(item);

        return new InventoryMutationResult
        {
            Status = InventoryMutationStatus.Ok,
            OwnerType = item.OwnerType,
            OwnerId = item.OwnerId,
            Items = await GetByOwnerAsync(item.OwnerType, item.OwnerId)
        };
    }

    private static string? NormalizeOwnerType(string ownerType)
    {
        var normalized = ownerType.Trim().ToLowerInvariant();
        return normalized switch
        {
            CharacterOwnerType => CharacterOwnerType,
            LocationOwnerType => LocationOwnerType,
            _ => null
        };
    }

    private async Task<int?> FindFirstOpenSlotAsync(string ownerType, string ownerId, IClientSessionHandle? session = null)
    {
        var items = session is null
            ? await GetByOwnerAsync(ownerType, ownerId)
            : await _items.Find(session, i => i.OwnerType == ownerType && i.OwnerId == ownerId).ToListAsync();

        var usedSlots = items.Where(i => i.Slot.HasValue).Select(i => i.Slot!.Value).ToHashSet();
        var maxSlotCount = GetMaxSlotCount(ownerType);
        var slot = 0;
        while (usedSlots.Contains(slot))
            slot += 1;
        if (maxSlotCount.HasValue && slot >= maxSlotCount.Value)
            return null;
        return slot;
    }

    private async Task<InventoryItem?> FindMergeTargetAsync(string ownerType, string ownerId, string itemKey, int maxStackSize)
    {
        var normalizedItemKey = NormalizeItemKey(itemKey);
        return await _items.Find(i =>
                i.OwnerType == ownerType &&
                i.OwnerId == ownerId &&
                i.ItemKey == normalizedItemKey &&
                i.EquippedSlot == null &&
                i.Slot != null &&
                i.Quantity < maxStackSize)
            .SortBy(i => i.Slot)
            .FirstOrDefaultAsync();
    }

    private Task<InventoryItem?> FindItemBySlotAsync(string ownerType, string ownerId, int slot, IClientSessionHandle? session = null)
        => session is null
            ? FindItemBySlotNoSessionAsync(ownerType, ownerId, slot)
            : FindItemBySlotWithSessionAsync(ownerType, ownerId, slot, session);

    private Task<InventoryItem?> FindStackAsync(string ownerType, string ownerId, string itemKey, int slot, IClientSessionHandle? session = null)
        => session is null
            ? FindStackNoSessionAsync(ownerType, ownerId, itemKey, slot)
            : FindStackWithSessionAsync(ownerType, ownerId, itemKey, slot, session);

    private async Task InsertItemAsync(InventoryItem item, IClientSessionHandle? session = null)
    {
        item.ItemKey = NormalizeItemKey(item.ItemKey);
        item.OwnerType = item.OwnerType.Trim().ToLowerInvariant();
        item.CreatedUtc = DateTime.UtcNow;
        item.UpdatedUtc = item.CreatedUtc;

        if (session is null)
            await _items.InsertOneAsync(item);
        else
            await _items.InsertOneAsync(session, item);
    }

    private async Task ReplaceItemAsync(InventoryItem item, IClientSessionHandle? session = null)
    {
        item.UpdatedUtc = DateTime.UtcNow;
        if (session is null)
            await _items.ReplaceOneAsync(i => i.Id == item.Id, item);
        else
            await _items.ReplaceOneAsync(session, i => i.Id == item.Id, item);
    }

    private Task DeleteItemAsync(string itemId, IClientSessionHandle? session = null)
    {
        return session is null
            ? _items.DeleteOneAsync(i => i.Id == itemId)
            : _items.DeleteOneAsync(session, i => i.Id == itemId);
    }

    private static string NormalizeItemKey(string itemKey) => itemKey.Trim().ToLowerInvariant();

    private static bool CanMerge(InventoryItem source, InventoryItem target, ItemDefinition definition) =>
        source.ItemKey == target.ItemKey &&
        source.EquippedSlot is null &&
        target.EquippedSlot is null &&
        definition.Stackable;

    private static int? GetMaxSlotCount(string ownerType) =>
        string.Equals(ownerType, CharacterOwnerType, StringComparison.OrdinalIgnoreCase)
            ? CharacterInventorySlotCount
            : null;

    private void EnsureIndexes()
    {
        _items.Indexes.CreateOne(new CreateIndexModel<InventoryItem>(
            Builders<InventoryItem>.IndexKeys.Ascending(i => i.OwnerType).Ascending(i => i.OwnerId),
            new CreateIndexOptions { Name = OwnerIndexName }));

        _items.Indexes.CreateOne(new CreateIndexModel<InventoryItem>(
            Builders<InventoryItem>.IndexKeys.Ascending(i => i.OwnerType).Ascending(i => i.OwnerId).Ascending(i => i.Slot),
            new CreateIndexOptions<InventoryItem>
            {
                Unique = true,
                Name = SlotIndexName,
                PartialFilterExpression = new BsonDocument("slot", new BsonDocument("$exists", true))
            }));

        _items.Indexes.CreateOne(new CreateIndexModel<InventoryItem>(
            Builders<InventoryItem>.IndexKeys.Ascending(i => i.OwnerType).Ascending(i => i.OwnerId).Ascending(i => i.EquippedSlot),
            new CreateIndexOptions<InventoryItem>
            {
                Unique = true,
                Name = EquippedSlotIndexName,
                PartialFilterExpression = new BsonDocument("equippedSlot", new BsonDocument("$exists", true))
            }));

        _definitions.Indexes.CreateOne(new CreateIndexModel<ItemDefinition>(
            Builders<ItemDefinition>.IndexKeys.Ascending(d => d.Category),
            new CreateIndexOptions { Name = DefinitionCategoryIndexName }));
    }

    private async Task<InventoryItem?> FindItemBySlotNoSessionAsync(string ownerType, string ownerId, int slot) =>
        await _items.Find(i => i.OwnerType == ownerType && i.OwnerId == ownerId && i.Slot == slot).FirstOrDefaultAsync();

    private async Task<InventoryItem?> FindItemBySlotWithSessionAsync(string ownerType, string ownerId, int slot, IClientSessionHandle session) =>
        await _items.Find(session, i => i.OwnerType == ownerType && i.OwnerId == ownerId && i.Slot == slot).FirstOrDefaultAsync();

    private async Task<InventoryItem?> FindStackNoSessionAsync(string ownerType, string ownerId, string itemKey, int slot) =>
        await _items.Find(i => i.OwnerType == ownerType && i.OwnerId == ownerId && i.Slot == slot && i.ItemKey == itemKey && i.EquippedSlot == null).FirstOrDefaultAsync();

    private async Task<InventoryItem?> FindStackWithSessionAsync(string ownerType, string ownerId, string itemKey, int slot, IClientSessionHandle session) =>
        await _items.Find(session, i => i.OwnerType == ownerType && i.OwnerId == ownerId && i.Slot == slot && i.ItemKey == itemKey && i.EquippedSlot == null).FirstOrDefaultAsync();

    [MongoDB.Bson.Serialization.Attributes.BsonIgnoreExtraElements]
    private class CharacterOwnerDocument
    {
        [MongoDB.Bson.Serialization.Attributes.BsonId]
        [MongoDB.Bson.Serialization.Attributes.BsonRepresentation(MongoDB.Bson.BsonType.ObjectId)]
        public string? Id { get; set; }

        public string OwnerUserId { get; set; } = string.Empty;
    }

    [MongoDB.Bson.Serialization.Attributes.BsonIgnoreExtraElements]
    private class LocationOwnerDocument
    {
        [MongoDB.Bson.Serialization.Attributes.BsonId]
        [MongoDB.Bson.Serialization.Attributes.BsonRepresentation(MongoDB.Bson.BsonType.ObjectId)]
        public string? Id { get; set; }
    }
}
