using LocationsApi.Models;
using MongoDB.Bson;
using MongoDB.Driver;

namespace LocationsApi.Services;

public class LocationStore
{
    private readonly IMongoCollection<Location> _col;
    private readonly IMongoCollection<BsonDocument> _rawCol;
    private readonly IMongoCollection<CharacterDocument> _characters;
    private readonly IMongoCollection<BiomeDefinition> _biomeDefinitions;
    private const string CoordIndexName = "coord_x_1_coord_y_1";

    public LocationStore(IConfiguration cfg)
    {
        var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
        var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb";
        var client = new MongoClient(cs);
        var db = client.GetDatabase(dbName);
        var collectionName = "Locations";
        EnsureLocationSchema(db, collectionName);
        _col = db.GetCollection<Location>(collectionName);
        _rawCol = db.GetCollection<BsonDocument>(collectionName);
        _characters = db.GetCollection<CharacterDocument>("Characters");
        _biomeDefinitions = db.GetCollection<BiomeDefinition>("BiomeDefinitions");

        EnsureCoordIndexes();

        EnsureOriginLocation();
    }

    private static void EnsureLocationSchema(IMongoDatabase db, string collectionName)
    {
        var validator = new BsonDocument
        {
            {
                "$jsonSchema", new BsonDocument
                {
                    { "bsonType", "object" },
                    { "required", new BsonArray { "name", "coord", "biomeKey", "elevation", "createdUtc" } },
                    {
                        "properties", new BsonDocument
                        {
                            { "name", new BsonDocument { { "bsonType", "string" } } },
                            {
                                "coord", new BsonDocument
                                {
                                    { "bsonType", "object" },
                                    { "required", new BsonArray { "x", "y" } },
                                    {
                                        "properties", new BsonDocument
                                        {
                                            { "x", new BsonDocument { { "bsonType", "int" } } },
                                            { "y", new BsonDocument { { "bsonType", "int" } } }
                                        }
                                    }
                                }
                            },
                            { "biomeKey", new BsonDocument { { "bsonType", "string" } } },
                            { "elevation", new BsonDocument { { "bsonType", "int" } } },
                            {
                                "resources", new BsonDocument
                                {
                                    { "bsonType", new BsonArray { "array", "null" } },
                                    {
                                        "items", new BsonDocument
                                        {
                                            { "bsonType", "object" },
                                            { "required", new BsonArray { "itemKey", "remainingQuantity", "gatherQuantity" } },
                                            {
                                                "properties", new BsonDocument
                                                {
                                                    { "itemKey", new BsonDocument { { "bsonType", "string" } } },
                                                    { "remainingQuantity", new BsonDocument { { "bsonType", "int" }, { "minimum", 0 } } },
                                                    { "gatherQuantity", new BsonDocument { { "bsonType", "int" }, { "minimum", 1 } } }
                                                }
                                            }
                                        }
                                    }
                                }
                            },
                            {
                                "locationObject", new BsonDocument
                                {
                                    { "bsonType", new BsonArray { "object", "null" } },
                                    {
                                        "properties", new BsonDocument
                                        {
                                            { "id", new BsonDocument { { "bsonType", "string" } } },
                                            { "objectType", new BsonDocument { { "bsonType", "string" } } },
                                            { "objectKey", new BsonDocument { { "bsonType", "string" } } },
                                            { "name", new BsonDocument { { "bsonType", "string" } } },
                                            {
                                                "state", new BsonDocument
                                                {
                                                    { "bsonType", new BsonArray { "object", "null" } },
                                                    {
                                                        "properties", new BsonDocument
                                                        {
                                                            { "itemKey", new BsonDocument { { "bsonType", "string" } } },
                                                            { "remainingQuantity", new BsonDocument { { "bsonType", "int" }, { "minimum", 0 } } },
                                                            { "gatherQuantity", new BsonDocument { { "bsonType", "int" }, { "minimum", 1 } } }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            },
                            { "locationObjectResolved", new BsonDocument { { "bsonType", new BsonArray { "bool", "null" } } } },
                            { "createdUtc", new BsonDocument { { "bsonType", "date" } } }
                        }
                    }
                }
            }
        };

        var collections = db.ListCollectionNames().ToList();
        if (!collections.Contains(collectionName))
        {
            var createCommand = new BsonDocument
            {
                { "create", collectionName },
                { "validator", validator },
                { "validationAction", "error" }
            };
            db.RunCommand<BsonDocument>(createCommand);
            return;
        }

        var command = new BsonDocument
        {
            { "collMod", collectionName },
            { "validator", validator },
            { "validationAction", "error" }
        };
        db.RunCommand<BsonDocument>(command);
    }

    public Task CreateAsync(Location location) => _col.InsertOneAsync(location);

    public Task<List<Location>> GetAllAsync() =>
        _col.Find(Builders<Location>.Filter.Empty).ToListAsync();

    public async Task<bool> DeleteAsync(string id)
    {
        var filter = Builders<Location>.Filter.Eq(l => l.Id, id);
        var result = await _col.DeleteOneAsync(filter);
        return result.DeletedCount > 0;
    }

    public async Task<bool> UpdateNameAsync(string id, string name)
    {
        var filter = Builders<Location>.Filter.Eq(l => l.Id, id);
        var update = Builders<Location>.Update.Set(l => l.Name, name);
        var result = await _col.UpdateOneAsync(filter, update);
        return result.ModifiedCount > 0;
    }

    public sealed record InteractResult(
        InteractStatus Status,
        string ObjectId = "",
        string ObjectType = "",
        string ItemKey = "",
        int QuantityGranted = 0,
        int RemainingQuantity = 0,
        bool Consumed = false,
        LocationObject? PreviousObject = null);

    public async Task<InteractResult> InteractWithObjectAsync(string locationId, string characterId, string objectId, string userId, bool allowAnyOwner)
    {
        var location = await _col.Find(l => l.Id == locationId).FirstOrDefaultAsync();
        if (location is null)
            return new InteractResult(InteractStatus.LocationNotFound);

        location = await EnsureLocationMetadataAsync(location);

        var character = await _characters.Find(c => c.Id == characterId).FirstOrDefaultAsync();
        if (character is null)
            return new InteractResult(InteractStatus.CharacterNotFound);
        if (!allowAnyOwner && character.OwnerUserId != userId)
            return new InteractResult(InteractStatus.Forbidden);
        if (character.Coord.X != location.Coord.X || character.Coord.Y != location.Coord.Y)
            return new InteractResult(InteractStatus.Invalid);

        var locationObject = location.LocationObject;
        if (locationObject is null)
            return new InteractResult(InteractStatus.ObjectNotFound);
        if (!string.Equals(locationObject.ObjectId, objectId, StringComparison.Ordinal))
            return new InteractResult(InteractStatus.ObjectNotFound);
        if (string.Equals(locationObject.ObjectType, "mailbox", StringComparison.OrdinalIgnoreCase))
        {
            return new InteractResult(
                InteractStatus.Ok,
                locationObject.ObjectId,
                locationObject.ObjectType,
                "",
                0,
                0,
                false,
                CloneLocationObject(locationObject));
        }
        if (!string.Equals(locationObject.ObjectType, "gatherable", StringComparison.OrdinalIgnoreCase))
            return new InteractResult(InteractStatus.UnsupportedObjectType);
        if (locationObject.State.RemainingQuantity <= 0)
            return new InteractResult(InteractStatus.ObjectConsumed);

        var quantityGranted = Math.Min(locationObject.State.GatherQuantity, locationObject.State.RemainingQuantity);
        var remainingQuantity = locationObject.State.RemainingQuantity - quantityGranted;
        var objectFilter = Builders<Location>.Filter.And(
            Builders<Location>.Filter.Eq(l => l.Id, locationId),
            Builders<Location>.Filter.Eq("locationObject.id", locationObject.ObjectId),
            Builders<Location>.Filter.Eq("locationObject.state.remainingQuantity", locationObject.State.RemainingQuantity)
        );

        UpdateDefinition<Location> update;
        if (remainingQuantity <= 0)
        {
            update = Builders<Location>.Update.Unset("locationObject");
        }
        else
        {
            update = Builders<Location>.Update.Set("locationObject.state.remainingQuantity", remainingQuantity);
        }

        var result = await _col.UpdateOneAsync(objectFilter, update);
        if (result.ModifiedCount == 0)
            return new InteractResult(InteractStatus.ObjectConsumed);

        return new InteractResult(
            InteractStatus.Ok,
            locationObject.ObjectId,
            locationObject.ObjectType,
            locationObject.State.ItemKey,
            quantityGranted,
            Math.Max(0, remainingQuantity),
            remainingQuantity <= 0,
            CloneLocationObject(locationObject));
    }

    public async Task RestoreObjectInteractionAsync(string locationId, LocationObject previousObject)
    {
        var filter = Builders<Location>.Filter.Eq(l => l.Id, locationId);
        var update = Builders<Location>.Update.Set(l => l.LocationObject, previousObject);
        await _col.UpdateOneAsync(filter, update);
    }

    public async Task<VisibleLocationWindowResponse> GetOrCreateVisibleLocationsAsync(int x, int y, int radius)
    {
        var generatedCount = await EnsureVisibleLocationsExistAsync(x, y, radius);
        var locations = await GetVisibleLocationsAsync(x, y, radius, ensureMetadata: true);
        return new VisibleLocationWindowResponse
        {
            GeneratedCount = generatedCount,
            Locations = locations
        };
    }

    public Task<List<BiomeDefinition>> GetBiomeDefinitionsAsync() =>
        _biomeDefinitions.Find(Builders<BiomeDefinition>.Filter.Empty)
            .SortBy(definition => definition.BiomeKey)
            .ToListAsync();

    public async Task<BiomeDefinition?> GetBiomeDefinitionAsync(string biomeKey)
    {
        var normalizedBiomeKey = NormalizeBiomeKey(biomeKey);
        return await _biomeDefinitions.Find(definition => definition.BiomeKey == normalizedBiomeKey)
            .FirstOrDefaultAsync();
    }

    public async Task<bool> CreateBiomeDefinitionAsync(BiomeDefinition definition)
    {
        definition.BiomeKey = NormalizeBiomeKey(definition.BiomeKey);
        definition.TransitionWeights = NormalizeTransitionWeights(definition.TransitionWeights);
        definition.ObjectSpawnRules = NormalizeObjectSpawnRules(definition.ObjectSpawnRules);
        definition.UpdatedUtc = DateTime.UtcNow;

        var existing = await GetBiomeDefinitionAsync(definition.BiomeKey);
        if (existing is not null)
            return false;

        await _biomeDefinitions.InsertOneAsync(definition);
        return true;
    }

    public async Task<BiomeDefinition> UpsertBiomeDefinitionAsync(string biomeKey, UpsertBiomeDefinitionRequest request)
    {
        var normalizedBiomeKey = NormalizeBiomeKey(biomeKey);
        var definition = new BiomeDefinition
        {
            BiomeKey = normalizedBiomeKey,
            ContinuationWeight = request.ContinuationWeight,
            TransitionWeights = NormalizeTransitionWeights(request.TransitionWeights),
            ObjectSpawnRules = NormalizeObjectSpawnRules(request.ObjectSpawnRules),
            UpdatedUtc = DateTime.UtcNow
        };

        var filter = Builders<BiomeDefinition>.Filter.Eq(existing => existing.BiomeKey, normalizedBiomeKey);
        await _biomeDefinitions.ReplaceOneAsync(filter, definition, new ReplaceOptions { IsUpsert = true });
        return definition;
    }

    private void EnsureCoordIndexes()
    {
        var indexes = _rawCol.Indexes.List().ToList();
        foreach (var index in indexes)
        {
            var name = index.GetValue("name", "").AsString;
            if (name == "_id_")
                continue;

            var keyDoc = index.GetValue("key", new BsonDocument()).AsBsonDocument;
            if (IsLegacyCoordIndex(keyDoc) || IsUnexpectedCoordIndex(keyDoc))
                _rawCol.Indexes.DropOne(name);
        }

        var coordIndex = new BsonDocument
        {
            { "coord.x", 1 },
            { "coord.y", 1 }
        };
        var coordIndexOptions = new CreateIndexOptions { Unique = true, Name = CoordIndexName };
        _rawCol.Indexes.CreateOne(new CreateIndexModel<BsonDocument>(coordIndex, coordIndexOptions));
    }

    private static bool IsLegacyCoordIndex(BsonDocument keyDoc) =>
        keyDoc.ElementCount == 2 &&
        keyDoc.TryGetValue("Coord.X", out var xValue) &&
        xValue.IsInt32 && xValue.AsInt32 == 1 &&
        keyDoc.TryGetValue("Coord.Y", out var yValue) &&
        yValue.IsInt32 && yValue.AsInt32 == 1;

    private static bool IsUnexpectedCoordIndex(BsonDocument keyDoc)
    {
        var hasLower = keyDoc.Contains("coord.x") || keyDoc.Contains("coord.y");
        var hasUpper = keyDoc.Contains("Coord.X") || keyDoc.Contains("Coord.Y");
        return hasUpper || (hasLower && !(keyDoc.Contains("coord.x") && keyDoc.Contains("coord.y")));
    }

    private void EnsureOriginLocation()
    {
        var biomeDefinitions = LoadBiomeDefinitions();
        if (biomeDefinitions.Count == 0)
            return;

        var originBiomeKey = biomeDefinitions.Any(definition => definition.BiomeKey == "plains")
            ? "plains"
            : biomeDefinitions[0].BiomeKey;
        var filter = Builders<Location>.Filter.And(
            Builders<Location>.Filter.Eq(l => l.Coord.X, 0),
            Builders<Location>.Filter.Eq(l => l.Coord.Y, 0)
        );
        var existing = _col.Find(filter).FirstOrDefault();
        if (existing is not null)
            return;

        var origin = new Location
        {
            Name = "Origin",
            Coord = new Coord { X = 0, Y = 0 },
            BiomeKey = originBiomeKey,
            Elevation = 0,
            LocationObject = CreateLocationObjectForBiome(biomeDefinitions, originBiomeKey, 0, 0),
            LocationObjectResolved = true,
            CreatedUtc = DateTime.UtcNow
        };

        try
        {
            _col.InsertOne(origin);
        }
        catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
        {
            // Another instance seeded it first.
        }
    }

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

    private async Task<List<VisibleLocationResponse>> GetVisibleLocationsAsync(int x, int y, int radius, bool ensureMetadata)
    {
        var minX = x - radius;
        var maxX = x + radius;
        var minY = y - radius;
        var maxY = y + radius;

        var filter = Builders<Location>.Filter.And(
            Builders<Location>.Filter.Gte(location => location.Coord.X, minX),
            Builders<Location>.Filter.Lte(location => location.Coord.X, maxX),
            Builders<Location>.Filter.Gte(location => location.Coord.Y, minY),
            Builders<Location>.Filter.Lte(location => location.Coord.Y, maxY)
        );

        var locations = await _col.Find(filter).ToListAsync();
        if (ensureMetadata)
        {
            for (var index = 0; index < locations.Count; index++)
                locations[index] = await EnsureLocationMetadataAsync(locations[index]);
        }

        return locations.Select(MapVisibleLocation).ToList();
    }

    private async Task<int> EnsureVisibleLocationsExistAsync(int x, int y, int radius)
    {
        var biomeDefinitions = await LoadBiomeDefinitionsAsync();
        var generatedCount = 0;

        for (var currentX = x - radius; currentX <= x + radius; currentX++)
        {
            for (var currentY = y - radius; currentY <= y + radius; currentY++)
            {
                if (await EnsureLocationStateAsync(currentX, currentY, biomeDefinitions))
                    generatedCount += 1;
            }
        }

        return generatedCount;
    }

    private async Task<bool> EnsureLocationStateAsync(int x, int y, IReadOnlyList<BiomeDefinition> biomeDefinitions)
    {
        var filter = Builders<BsonDocument>.Filter.And(
            Builders<BsonDocument>.Filter.Eq("coord.x", x),
            Builders<BsonDocument>.Filter.Eq("coord.y", y)
        );

        var existing = await _rawCol.Find(filter).FirstOrDefaultAsync();
        if (existing is not null)
        {
            var typedLocation = await _col.Find(location => location.Id == existing["_id"].AsObjectId.ToString()).FirstOrDefaultAsync();
            if (typedLocation is not null)
                await EnsureLocationMetadataAsync(typedLocation);
            return false;
        }

        var biomeKey = await DetermineBiomeKeyAsync(x, y, biomeDefinitions);
        var elevation = await DetermineElevationAsync(x, y, biomeKey);
        var locationObject = CreateLocationObjectForBiome(biomeDefinitions, biomeKey, x, y);
        BsonValue locationObjectValue = locationObject is null ? BsonNull.Value : locationObject.ToBsonDocument();
        var update = Builders<BsonDocument>.Update
            .SetOnInsert("_id", ObjectId.GenerateNewId())
            .SetOnInsert("name", DefaultLocationName(x, y))
            .SetOnInsert("coord", new BsonDocument { { "x", x }, { "y", y } })
            .SetOnInsert("biomeKey", biomeKey)
            .SetOnInsert("elevation", elevation)
            .SetOnInsert("locationObject", locationObjectValue)
            .SetOnInsert("locationObjectResolved", true)
            .SetOnInsert("createdUtc", DateTime.UtcNow);

        try
        {
            var result = await _rawCol.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true });
            return result.UpsertedId is not null;
        }
        catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
        {
            return false;
        }
    }

    private async Task<Location> EnsureLocationMetadataAsync(Location location)
    {
        var locationId = location.Id ?? string.Empty;
        var hasElevation = !string.IsNullOrWhiteSpace(locationId) && await HasStoredElevationAsync(locationId);
        if (!string.IsNullOrWhiteSpace(location.BiomeKey) && location.LocationObjectResolved && hasElevation)
            return location;

        var biomeDefinitions = await LoadBiomeDefinitionsAsync();
        var biomeKey = location.BiomeKey;
        if (string.IsNullOrWhiteSpace(biomeKey))
            biomeKey = await DetermineBiomeKeyAsync(location.Coord.X, location.Coord.Y, biomeDefinitions);
        var elevation = hasElevation ? location.Elevation : await DetermineElevationAsync(location.Coord.X, location.Coord.Y, biomeKey);

        var migratedObject = TryMigrateLegacyResources(location);
        if (migratedObject is null)
            migratedObject = CreateLocationObjectForBiome(biomeDefinitions, biomeKey, location.Coord.X, location.Coord.Y);
        var filter = Builders<Location>.Filter.And(
            Builders<Location>.Filter.Eq(l => l.Id, location.Id)
        );
        var update = Builders<Location>.Update
            .Set(l => l.BiomeKey, biomeKey)
            .Set(l => l.Elevation, elevation)
            .Set(l => l.LocationObjectResolved, true)
            .Set(l => l.LocationObject, migratedObject);
        await _col.UpdateOneAsync(filter, update);
        location.BiomeKey = biomeKey;
        location.Elevation = elevation;
        location.LocationObject = migratedObject;
        location.LocationObjectResolved = true;
        return location;
    }

    private static VisibleLocationResponse MapVisibleLocation(Location location)
    {
        return new VisibleLocationResponse
        {
            Id = location.Id,
            Name = location.Name,
            Coord = new Coord { X = location.Coord.X, Y = location.Coord.Y },
            BiomeKey = location.BiomeKey,
            Elevation = location.Elevation,
            LocationObject = MapVisibleLocationObject(location.LocationObject)
        };
    }

    private static VisibleLocationObjectResponse? MapVisibleLocationObject(LocationObject? locationObject)
    {
        if (locationObject is null)
            return null;

        return new VisibleLocationObjectResponse
        {
            Id = locationObject.ObjectId,
            ObjectType = locationObject.ObjectType,
            ObjectKey = locationObject.ObjectKey,
            Name = locationObject.Name,
            State = new VisibleLocationObjectStateResponse
            {
                ItemKey = locationObject.State.ItemKey,
                RemainingQuantity = locationObject.State.RemainingQuantity,
                GatherQuantity = locationObject.State.GatherQuantity
            }
        };
    }

    private static LocationObject? TryMigrateLegacyResources(Location location)
    {
        var legacyResource = location.Resources.FirstOrDefault(r => r.RemainingQuantity > 0);
        if (legacyResource is null)
            return null;

        return CreateGatherableObject(
            legacyResource.ItemKey,
            legacyResource.RemainingQuantity,
            legacyResource.GatherQuantity);
    }

    private async Task<string> DetermineBiomeKeyAsync(int x, int y, IReadOnlyList<BiomeDefinition> biomeDefinitions)
    {
        if (x == 0 && y == 0)
            return biomeDefinitions.Any(definition => definition.BiomeKey == "plains")
                ? "plains"
                : biomeDefinitions[0].BiomeKey;

        var neighbors = await LoadNeighborBiomeKeysAsync(x, y);
        var baseBiome = DetermineBaseBiomeKey(x, y);
        if (neighbors.Count == 0)
            return biomeDefinitions.Any(definition => definition.BiomeKey == baseBiome)
                ? baseBiome
                : biomeDefinitions[0].BiomeKey;

        var dominantNeighbor = neighbors
            .GroupBy(key => key)
            .OrderByDescending(group => group.Count())
            .ThenBy(group => group.Key)
            .First().Key;

        var bestBiome = baseBiome;
        var bestScore = double.NegativeInfinity;
        foreach (var candidate in biomeDefinitions)
        {
            var score = candidate.BiomeKey == baseBiome ? 2.5 : 0.35;
            if (candidate.BiomeKey == dominantNeighbor)
                score += 1.8;

            foreach (var neighbor in neighbors)
            {
                var neighborDefinition = biomeDefinitions.FirstOrDefault(definition => definition.BiomeKey == neighbor);
                if (neighborDefinition is null)
                    continue;

                if (candidate.BiomeKey == neighbor)
                {
                    score += neighborDefinition.ContinuationWeight;
                    continue;
                }

                var transition = neighborDefinition.TransitionWeights
                    .FirstOrDefault(weight => weight.TargetBiomeKey == candidate.BiomeKey);
                if (transition is not null)
                    score += transition.Weight;
            }

            score += StableNoise(x, y, StableHash(candidate.BiomeKey)) * 0.25;
            if (score > bestScore)
            {
                bestScore = score;
                bestBiome = candidate.BiomeKey;
            }
        }

        return bestBiome;
    }

    private async Task<int> DetermineElevationAsync(int x, int y, string biomeKey)
    {
        if (x == 0 && y == 0)
            return 0;

        var baseElevation = DetermineBaseElevation(x, y, biomeKey);
        var neighbors = await LoadNeighborElevationsAsync(x, y);
        if (neighbors.Count == 0)
            return baseElevation;

        var averageNeighbor = (int)Math.Round(neighbors.Average());
        var blended = (int)Math.Round((baseElevation + averageNeighbor) / 2.0);
        var minNeighbor = neighbors.Min();
        var maxNeighbor = neighbors.Max();
        return Math.Clamp(blended, minNeighbor - 1, maxNeighbor + 1);
    }

    private static LocationObject? CreateLocationObjectForBiome(IReadOnlyList<BiomeDefinition> biomeDefinitions, string biomeKey, int x, int y)
    {
        var biome = biomeDefinitions.FirstOrDefault(definition => definition.BiomeKey == biomeKey)
            ?? throw new InvalidOperationException($"Missing biome definition for '{biomeKey}'.");

        var totalWeight = biome.ObjectSpawnRules.Sum(rule => rule.Weight);
        if (totalWeight <= 0)
            return null;

        var roll = StableNoise(x, y, 401) * totalWeight;
        var cumulative = 0.0;
        foreach (var rule in biome.ObjectSpawnRules)
        {
            cumulative += rule.Weight;
            if (roll > cumulative)
                continue;

            if (string.Equals(rule.ResultType, "none", StringComparison.OrdinalIgnoreCase))
                return null;
            if (!string.Equals(rule.ResultType, "gatherable", StringComparison.OrdinalIgnoreCase) || string.IsNullOrWhiteSpace(rule.ItemKey))
                return null;

            return CreateGatherableObject(
                rule.ItemKey,
                Math.Max(0, rule.RemainingQuantity),
                Math.Max(1, rule.GatherQuantity),
                rule.ObjectKey,
                rule.DisplayName);
        }

        return null;
    }

    private static LocationObject CreateGatherableObject(string itemKey, int remainingQuantity, int gatherQuantity, string? objectKey = null, string? displayName = null)
    {
        var normalizedItemKey = NormalizeItemKey(itemKey);
        return new LocationObject
        {
            ObjectId = Guid.NewGuid().ToString("N"),
            ObjectType = "gatherable",
            ObjectKey = string.IsNullOrWhiteSpace(objectKey) ? $"{normalizedItemKey}_node" : objectKey,
            Name = string.IsNullOrWhiteSpace(displayName) ? HumanizeItemKey(normalizedItemKey) : displayName,
            State = new LocationObjectState
            {
                ItemKey = normalizedItemKey,
                RemainingQuantity = remainingQuantity,
                GatherQuantity = gatherQuantity
            }
        };
    }

    private static string HumanizeItemKey(string itemKey)
    {
        return string.Join(" ", itemKey.Split('_', StringSplitOptions.RemoveEmptyEntries)
            .Select(part => char.ToUpperInvariant(part[0]) + part[1..]));
    }

    private static int FloorDiv(int value, int divisor)
    {
        var quotient = value / divisor;
        var remainder = value % divisor;
        if (remainder != 0 && ((remainder < 0) != (divisor < 0)))
            quotient -= 1;
        return quotient;
    }

    private async Task<List<string>> LoadNeighborBiomeKeysAsync(int x, int y)
    {
        var coords = new[] { (x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1) };
        var filters = coords.Select(coord =>
            Builders<BsonDocument>.Filter.And(
                Builders<BsonDocument>.Filter.Eq("coord.x", coord.Item1),
                Builders<BsonDocument>.Filter.Eq("coord.y", coord.Item2)))
            .ToList();
        var filter = Builders<BsonDocument>.Filter.Or(filters);
        var neighbors = await _rawCol.Find(filter).ToListAsync();

        return neighbors
            .Where(doc => doc.Contains("biomeKey"))
            .Select(doc => doc.GetValue("biomeKey", "").AsString)
            .Where(key => !string.IsNullOrWhiteSpace(key))
            .ToList();
    }

    private async Task<List<int>> LoadNeighborElevationsAsync(int x, int y)
    {
        var coords = new[] { (x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1) };
        var filters = coords.Select(coord =>
            Builders<BsonDocument>.Filter.And(
                Builders<BsonDocument>.Filter.Eq("coord.x", coord.Item1),
                Builders<BsonDocument>.Filter.Eq("coord.y", coord.Item2)))
            .ToList();
        var filter = Builders<BsonDocument>.Filter.Or(filters);
        var neighbors = await _rawCol.Find(filter).ToListAsync();

        return neighbors
            .Where(doc => doc.Contains("elevation") && doc["elevation"].IsInt32)
            .Select(doc => doc["elevation"].AsInt32)
            .ToList();
    }

    private async Task<bool> HasStoredElevationAsync(string locationId)
    {
        var filter = Builders<BsonDocument>.Filter.And(
            Builders<BsonDocument>.Filter.Eq("_id", ObjectId.Parse(locationId)),
            Builders<BsonDocument>.Filter.Exists("elevation", true)
        );
        return await _rawCol.Find(filter).AnyAsync();
    }

    private List<BiomeDefinition> LoadBiomeDefinitions()
    {
        return _biomeDefinitions.Find(Builders<BiomeDefinition>.Filter.Empty)
            .SortBy(definition => definition.BiomeKey)
            .ToList();
    }

    private async Task<List<BiomeDefinition>> LoadBiomeDefinitionsAsync()
    {
        var definitions = await _biomeDefinitions.Find(Builders<BiomeDefinition>.Filter.Empty)
            .SortBy(definition => definition.BiomeKey)
            .ToListAsync();
        if (definitions.Count == 0)
            throw new InvalidOperationException("No biome definitions exist in the BiomeDefinitions collection.");
        return definitions;
    }

    private static string DetermineBaseBiomeKey(int x, int y)
    {
        var temperature = StableNoise(x, y, 101);
        var moisture = StableNoise(x, y, 202);
        var ruggedness = StableNoise(x, y, 303);

        if (ruggedness > 0.74)
            return "rocky";
        if (moisture > 0.72 && temperature < 0.75)
            return "wetlands";
        if (moisture > 0.56)
            return "forest";
        if (moisture < 0.22 && temperature > 0.58)
            return "desert";
        return "plains";
    }

    private static int DetermineBaseElevation(int x, int y, string biomeKey)
    {
        var macro = StableNoise(x, y, 404);
        var micro = StableNoise(x, y, 505);

        return biomeKey switch
        {
            "wetlands" => (int)Math.Round((macro * 2.0) - 1.0),
            "plains" => (int)Math.Round((macro * 3.0) - 1.0),
            "forest" => (int)Math.Round((macro * 4.0) - 1.5),
            "desert" => (int)Math.Round((macro * 3.0) - 1.0 + ((micro - 0.5) * 0.75)),
            "rocky" => (int)Math.Round((macro * 5.0) - 1.0 + ((micro - 0.5) * 1.25)),
            _ => (int)Math.Round((macro * 3.0) - 1.0)
        };
    }

    private static double StableNoise(int x, int y, int salt)
    {
        var value = Math.Sin((x * 12.9898) + (y * 78.233) + ((1729 + salt) * 0.1597)) * 43758.5453;
        return value - Math.Floor(value);
    }

    private static int StableHash(string value)
    {
        unchecked
        {
            var hash = 17;
            foreach (var ch in value)
                hash = (hash * 31) + ch;
            return hash;
        }
    }

    private static string NormalizeBiomeKey(string biomeKey) => biomeKey.Trim().ToLowerInvariant();

    private static List<BiomeTransitionWeight> NormalizeTransitionWeights(IEnumerable<BiomeTransitionWeight> transitionWeights)
    {
        return transitionWeights
            .Where(weight => !string.IsNullOrWhiteSpace(weight.TargetBiomeKey))
            .Select(weight => new BiomeTransitionWeight
            {
                TargetBiomeKey = NormalizeBiomeKey(weight.TargetBiomeKey),
                Weight = weight.Weight
            })
            .ToList();
    }

    private static List<BiomeObjectSpawnRule> NormalizeObjectSpawnRules(IEnumerable<BiomeObjectSpawnRule> objectSpawnRules)
    {
        return objectSpawnRules.Select(rule => new BiomeObjectSpawnRule
        {
            ResultType = string.IsNullOrWhiteSpace(rule.ResultType) ? "none" : rule.ResultType.Trim().ToLowerInvariant(),
            ItemKey = string.IsNullOrWhiteSpace(rule.ItemKey) ? null : NormalizeItemKey(rule.ItemKey),
            ObjectKey = string.IsNullOrWhiteSpace(rule.ObjectKey) ? null : rule.ObjectKey.Trim(),
            DisplayName = string.IsNullOrWhiteSpace(rule.DisplayName) ? null : rule.DisplayName.Trim(),
            RemainingQuantity = rule.RemainingQuantity,
            GatherQuantity = rule.GatherQuantity,
            Weight = rule.Weight
        }).ToList();
    }

    private static string DefaultLocationName(int x, int y)
    {
        if (x == 0 && y == 0)
            return "Origin";
        return $"Location {x},{y}";
    }

    private static LocationObject CloneLocationObject(LocationObject source)
    {
        return new LocationObject
        {
            ObjectId = source.ObjectId,
            ObjectType = source.ObjectType,
            ObjectKey = source.ObjectKey,
            Name = source.Name,
            State = new LocationObjectState
            {
                ItemKey = source.State.ItemKey,
                RemainingQuantity = source.State.RemainingQuantity,
                GatherQuantity = source.State.GatherQuantity
            }
        };
    }

    [MongoDB.Bson.Serialization.Attributes.BsonIgnoreExtraElements]
    private class CharacterDocument
    {
        [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;

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

    [MongoDB.Bson.Serialization.Attributes.BsonIgnoreExtraElements]
    private class CharacterCoord
    {
        public int X { get; set; }

        public int Y { get; set; }
    }
}

public enum InteractStatus
{
    Ok,
    LocationNotFound,
    CharacterNotFound,
    Forbidden,
    Invalid,
    ObjectNotFound,
    UnsupportedObjectType,
    ObjectConsumed
}
