using CraftingApi.Models;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;

namespace CraftingApi.Services;

public class CraftingStore
{
    private readonly IMongoCollection<CraftingRecipe> _recipes;
    private readonly IMongoCollection<CharacterDocument> _characters;
    private readonly IMongoCollection<LocationDocument> _locations;
    private readonly IMongoCollection<InventoryItemDocument> _items;
    private readonly IMongoCollection<ItemDefinitionDocument> _definitions;
    private const string CharacterOwnerType = "character";

    public CraftingStore(IConfiguration cfg)
    {
        var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
        var dbName = cfg["MongoDB:DatabaseName"] ?? "promiscuity";
        var client = new MongoClient(cs);
        var db = client.GetDatabase(dbName);

        _recipes = db.GetCollection<CraftingRecipe>("CraftingRecipes");
        _characters = db.GetCollection<CharacterDocument>("Characters");
        _locations = db.GetCollection<LocationDocument>("Locations");
        _items = db.GetCollection<InventoryItemDocument>("InventoryItems");
        _definitions = db.GetCollection<ItemDefinitionDocument>("ItemDefinitions");

        _recipes.Indexes.CreateOne(new CreateIndexModel<CraftingRecipe>(
            Builders<CraftingRecipe>.IndexKeys.Ascending(r => r.Category)));
        _recipes.Indexes.CreateOne(new CreateIndexModel<CraftingRecipe>(
            Builders<CraftingRecipe>.IndexKeys.Ascending(r => r.StationType)));
    }

    public async Task<CharacterAccessResult> ResolveCharacterAsync(string characterId, string userId, bool allowAnyOwner)
    {
        var character = await _characters.Find(c => c.Id == characterId).FirstOrDefaultAsync();
        if (character is null)
            return new CharacterAccessResult { Exists = false };

        return new CharacterAccessResult
        {
            Exists = true,
            IsAuthorized = allowAnyOwner || character.OwnerUserId == userId,
            CharacterId = character.Id ?? string.Empty,
            OwnerUserId = character.OwnerUserId,
            CoordX = character.Coord.X,
            CoordY = character.Coord.Y
        };
    }

    public Task<List<CraftingRecipe>> ListRecipesAsync() =>
        _recipes.Find(Builders<CraftingRecipe>.Filter.Empty).SortBy(r => r.Category).ThenBy(r => r.Name).ToListAsync();

    public async Task<CraftingRecipe?> GetRecipeAsync(string recipeKey) =>
        await _recipes.Find(r => r.RecipeKey == NormalizeRecipeKey(recipeKey)).FirstOrDefaultAsync();

    public async Task<bool> CreateRecipeAsync(CraftingRecipe recipe)
    {
        var existing = await GetRecipeAsync(recipe.RecipeKey);
        if (existing is not null)
            return false;

        await _recipes.InsertOneAsync(recipe);
        return true;
    }

    public CraftingRecipe BuildRecipe(string recipeKey, UpsertCraftingRecipeRequest request)
    {
        return new CraftingRecipe
        {
            RecipeKey = NormalizeRecipeKey(recipeKey),
            Name = request.Name.Trim(),
            Category = NormalizeSimpleKey(request.Category, "misc"),
            StationType = NormalizeSimpleKey(request.StationType, "hand"),
            Inputs = NormalizeIngredients(request.Inputs),
            Outputs = NormalizeIngredients(request.Outputs),
            CraftTimeSeconds = Math.Max(0, request.CraftTimeSeconds),
            Enabled = request.Enabled,
            UpdatedUtc = DateTime.UtcNow
        };
    }

    public async Task<CraftingRecipe> UpsertRecipeAsync(string recipeKey, UpsertCraftingRecipeRequest request)
    {
        var normalizedRecipeKey = NormalizeRecipeKey(recipeKey);
        var recipe = BuildRecipe(normalizedRecipeKey, request);

        var existing = await GetRecipeAsync(normalizedRecipeKey);
        if (existing is not null)
            recipe.CreatedUtc = existing.CreatedUtc;

        await _recipes.ReplaceOneAsync(r => r.RecipeKey == normalizedRecipeKey, recipe, new ReplaceOptions { IsUpsert = true });
        return recipe;
    }

    public async Task<List<AvailableCraftingRecipeResponse>> GetAvailableRecipesAsync(CharacterAccessResult character)
    {
        var recipes = await _recipes.Find(r => r.Enabled).SortBy(r => r.Category).ThenBy(r => r.Name).ToListAsync();
        var items = await GetCharacterItemsAsync(character.CharacterId);
        var itemTotals = items.GroupBy(i => i.ItemKey).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
        var location = await GetLocationAtCoordAsync(character.CoordX, character.CoordY);

        return recipes.Select(recipe =>
        {
            var missing = GetMissingRequirements(recipe, itemTotals, location);
            return new AvailableCraftingRecipeResponse
            {
                Recipe = CraftingRecipeResponse.FromModel(recipe),
                CanCraft = missing.Count == 0,
                MissingRequirements = missing
            };
        }).ToList();
    }

    public async Task<CraftAttemptResult> CraftAsync(CharacterAccessResult character, CraftRecipeRequest request)
    {
        var recipe = await GetRecipeAsync(request.RecipeKey);
        if (recipe is null || !recipe.Enabled)
            return new CraftAttemptResult { Status = CraftStatus.RecipeNotFound };

        var craftCount = Math.Max(1, request.Quantity);
        var location = await GetLocationAtCoordAsync(character.CoordX, character.CoordY);
        if (!CanUseStation(recipe, location, request.LocationId, request.StationObjectId))
            return new CraftAttemptResult { Status = CraftStatus.MissingStation };

        var items = await GetCharacterItemsAsync(character.CharacterId);
        var totals = items.GroupBy(i => i.ItemKey).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
        var missing = GetMissingRequirements(recipe, totals, location, craftCount);
        if (missing.Count > 0)
            return new CraftAttemptResult { Status = CraftStatus.MissingInputs, MissingRequirements = missing };

        var definitions = await _definitions.Find(Builders<ItemDefinitionDocument>.Filter.Empty).ToListAsync();
        var definitionMap = definitions.ToDictionary(d => d.ItemKey, StringComparer.Ordinal);
        foreach (var output in recipe.Outputs)
        {
            if (!definitionMap.ContainsKey(output.ItemKey))
                return new CraftAttemptResult { Status = CraftStatus.InvalidRecipe, MissingRequirements = [$"Missing item definition for output '{output.ItemKey}'"] };
        }

        foreach (var input in recipe.Inputs)
            await ConsumeItemKeyAsync(character, input.ItemKey, input.Quantity * craftCount);

        foreach (var output in recipe.Outputs)
            await GrantItemKeyAsync(character, output.ItemKey, output.Quantity * craftCount, definitionMap[output.ItemKey]);

        return new CraftAttemptResult
        {
            Status = CraftStatus.Ok,
            CraftedQuantity = craftCount,
            Consumed = recipe.Inputs.Select(i => new CraftingIngredient { ItemKey = i.ItemKey, Quantity = i.Quantity * craftCount }).ToList(),
            Produced = recipe.Outputs.Select(i => new CraftingIngredient { ItemKey = i.ItemKey, Quantity = i.Quantity * craftCount }).ToList()
        };
    }

    private async Task<List<InventoryItemDocument>> GetCharacterItemsAsync(string characterId) =>
        await _items.Find(i => i.OwnerType == CharacterOwnerType && i.OwnerId == characterId).SortBy(i => i.Slot).ThenBy(i => i.ItemKey).ToListAsync();

    private async Task<LocationDocument?> GetLocationAtCoordAsync(int x, int y) =>
        await _locations.Find(l => l.Coord.X == x && l.Coord.Y == y).FirstOrDefaultAsync();

    private static List<CraftingIngredient> NormalizeIngredients(IEnumerable<CraftingIngredient> ingredients)
    {
        return ingredients
            .Where(i => !string.IsNullOrWhiteSpace(i.ItemKey) && i.Quantity > 0)
            .Select(i => new CraftingIngredient
            {
                ItemKey = NormalizeSimpleKey(i.ItemKey, ""),
                Quantity = i.Quantity
            })
            .ToList();
    }

    private static string NormalizeRecipeKey(string recipeKey) => NormalizeSimpleKey(recipeKey, "");

    private static string NormalizeSimpleKey(string value, string fallback)
    {
        var normalized = value.Trim().ToLowerInvariant();
        return string.IsNullOrWhiteSpace(normalized) ? fallback : normalized;
    }

    private static List<string> GetMissingRequirements(CraftingRecipe recipe, IReadOnlyDictionary<string, int> itemTotals, LocationDocument? location, int craftCount = 1)
    {
        var missing = new List<string>();
        if (!CanUseStation(recipe, location, null, null))
            missing.Add("Required station is not available");

        foreach (var input in recipe.Inputs)
        {
            var required = input.Quantity * craftCount;
            var available = itemTotals.TryGetValue(input.ItemKey, out var amount) ? amount : 0;
            if (available < required)
                missing.Add("Need %s x%d".Replace("%s", input.ItemKey).Replace("%d", required.ToString()));
        }

        return missing;
    }

    private static bool CanUseStation(CraftingRecipe recipe, LocationDocument? location, string? requestedLocationId, string? requestedStationObjectId)
    {
        if (recipe.StationType == "hand")
            return true;
        if (location is null)
            return false;
        if (!string.IsNullOrWhiteSpace(requestedLocationId) && !string.Equals(location.Id, requestedLocationId, StringComparison.Ordinal))
            return false;
        if (location.LocationObject is null)
            return false;
        if (!string.IsNullOrWhiteSpace(requestedStationObjectId) && !string.Equals(location.LocationObject.ObjectId, requestedStationObjectId, StringComparison.Ordinal))
            return false;
        if (!string.Equals(location.LocationObject.ObjectType, "station", StringComparison.OrdinalIgnoreCase))
            return false;

        var stationType = NormalizeSimpleKey(location.LocationObject.State.StationType ?? string.Empty, "");
        return string.Equals(recipe.StationType, stationType, StringComparison.Ordinal);
    }

    private async Task ConsumeItemKeyAsync(CharacterAccessResult character, string itemKey, int quantity)
    {
        var remaining = quantity;
        var items = await _items.Find(i => i.OwnerType == CharacterOwnerType && i.OwnerId == character.CharacterId && i.ItemKey == itemKey && i.EquippedSlot == null)
            .SortBy(i => i.Slot)
            .ThenBy(i => i.CreatedUtc)
            .ToListAsync();

        foreach (var item in items)
        {
            if (remaining <= 0)
                break;

            if (item.Quantity <= remaining)
            {
                remaining -= item.Quantity;
                await _items.DeleteOneAsync(i => i.Id == item.Id);
            }
            else
            {
                item.Quantity -= remaining;
                item.UpdatedUtc = DateTime.UtcNow;
                await _items.ReplaceOneAsync(i => i.Id == item.Id, item);
                remaining = 0;
            }
        }
    }

    private async Task GrantItemKeyAsync(CharacterAccessResult character, string itemKey, int quantity, ItemDefinitionDocument definition)
    {
        var remaining = quantity;
        if (definition.Stackable)
        {
            var existingStacks = await _items.Find(i =>
                    i.OwnerType == CharacterOwnerType &&
                    i.OwnerId == character.CharacterId &&
                    i.ItemKey == itemKey &&
                    i.EquippedSlot == null &&
                    i.Slot != null)
                .SortBy(i => i.Slot)
                .ToListAsync();

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

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

                var added = Math.Min(remaining, availableSpace);
                stack.Quantity += added;
                stack.UpdatedUtc = DateTime.UtcNow;
                await _items.ReplaceOneAsync(i => i.Id == stack.Id, stack);
                remaining -= added;
            }
        }

        while (remaining > 0)
        {
            var slot = await FindFirstOpenSlotAsync(character.CharacterId);
            if (slot is null)
                throw new InvalidOperationException("Crafting outputs did not fit in the character inventory.");

            var granted = definition.Stackable ? Math.Min(remaining, definition.MaxStackSize) : 1;
            var item = new InventoryItemDocument
            {
                ItemKey = itemKey,
                Quantity = granted,
                OwnerType = CharacterOwnerType,
                OwnerId = character.CharacterId,
                OwnerUserId = character.OwnerUserId,
                Slot = slot.Value,
                CreatedUtc = DateTime.UtcNow,
                UpdatedUtc = DateTime.UtcNow
            };
            await _items.InsertOneAsync(item);
            remaining -= granted;
        }
    }

    private async Task<int?> FindFirstOpenSlotAsync(string characterId)
    {
        var items = await GetCharacterItemsAsync(characterId);
        var used = items.Where(i => i.Slot.HasValue).Select(i => i.Slot!.Value).ToHashSet();
        for (var slot = 0; slot < 6; slot++)
        {
            if (!used.Contains(slot))
                return slot;
        }
        return null;
    }

    public class CharacterAccessResult
    {
        public bool Exists { get; set; }

        public bool IsAuthorized { get; set; }

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

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

        public int CoordX { get; set; }

        public int CoordY { get; set; }
    }

    public class CraftAttemptResult
    {
        public CraftStatus Status { get; set; }

        public int CraftedQuantity { get; set; }

        public List<CraftingIngredient> Consumed { get; set; } = [];

        public List<CraftingIngredient> Produced { get; set; } = [];

        public List<string> MissingRequirements { get; set; } = [];
    }

    public enum CraftStatus
    {
        Ok,
        RecipeNotFound,
        MissingInputs,
        MissingStation,
        InvalidRecipe
    }

    [BsonIgnoreExtraElements]
    private class CharacterDocument
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        public string? Id { get; set; }

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

        public CoordDocument Coord { get; set; } = new();
    }

    [BsonIgnoreExtraElements]
    private class CoordDocument
    {
        public int X { get; set; }

        public int Y { get; set; }
    }

    [BsonIgnoreExtraElements]
    private class LocationDocument
    {
        [BsonId]
        [BsonRepresentation(BsonType.ObjectId)]
        public string? Id { get; set; }

        [BsonElement("coord")]
        public CoordDocument Coord { get; set; } = new();

        [BsonElement("locationObject")]
        public LocationObjectDocument? LocationObject { get; set; }
    }

    [BsonIgnoreExtraElements]
    private class LocationObjectDocument
    {
        [BsonElement("id")]
        public string ObjectId { get; set; } = string.Empty;

        [BsonElement("objectType")]
        public string ObjectType { get; set; } = string.Empty;

        [BsonElement("state")]
        public LocationObjectStateDocument State { get; set; } = new();
    }

    [BsonIgnoreExtraElements]
    private class LocationObjectStateDocument
    {
        [BsonElement("stationType")]
        [BsonIgnoreIfNull]
        public string? StationType { get; set; }
    }

    [BsonIgnoreExtraElements]
    private class InventoryItemDocument
    {
        [BsonId]
        public string Id { get; set; } = Guid.NewGuid().ToString();

        [BsonElement("itemKey")]
        public string ItemKey { get; set; } = string.Empty;

        [BsonElement("quantity")]
        public int Quantity { get; set; }

        [BsonElement("ownerType")]
        public string OwnerType { get; set; } = string.Empty;

        [BsonElement("ownerId")]
        public string OwnerId { get; set; } = string.Empty;

        [BsonElement("ownerUserId")]
        [BsonIgnoreIfNull]
        public string? OwnerUserId { get; set; }

        [BsonElement("slot")]
        [BsonIgnoreIfNull]
        public int? Slot { get; set; }

        [BsonElement("equippedSlot")]
        [BsonIgnoreIfNull]
        public string? EquippedSlot { get; set; }

        [BsonElement("createdUtc")]
        public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;

        [BsonElement("updatedUtc")]
        public DateTime UpdatedUtc { get; set; } = DateTime.UtcNow;
    }

    [BsonIgnoreExtraElements]
    private class ItemDefinitionDocument
    {
        [BsonId]
        [BsonElement("itemKey")]
        public string ItemKey { get; set; } = string.Empty;

        [BsonElement("stackable")]
        public bool Stackable { get; set; }

        [BsonElement("maxStackSize")]
        public int MaxStackSize { get; set; } = 1;
    }
}
