using Theriapolis.Core.Util;

namespace Theriapolis.Core.World.Generation.Stages;

/// <summary>
/// Stage 14 — SettlementPlace
/// Places Tier 2–5 settlements (narrative anchors were placed in stage 13).
/// Narrative anchors are pre-placed; this stage fills in the rest.
/// </summary>
public sealed class SettlementPlaceStage : IWorldGenStage
{
    public string Name => "SettlementPlace";

    private const int W = C.WORLD_WIDTH_TILES;
    private const int H = C.WORLD_HEIGHT_TILES;

    public void Run(WorldGenContext ctx)
    {
        var world = ctx.World;
        var rng   = ctx.Rngs["settle"];

        // Confine settlements to the main (largest) landmass. Smaller islands
        // can't be road-connected across ocean, so a settlement there would
        // either be unreachable or force a sea-crossing straight-line stub.
        var (componentIds, componentSizes) = LandmassMap.Compute(world);
        int mainLandmassId = LandmassMap.LargestComponentId(componentSizes);

        // Sort all land tiles by habitability descending
        var landTiles = new List<(float hab, int x, int y)>(W * H / 3);
        for (int y = 0; y < H; y += 2)
        for (int x = 0; x < W; x += 2)
        {
            if (world.Tiles[x, y].Biome == BiomeId.Ocean) continue;
            if (componentIds[x, y] != mainLandmassId) continue;
            float h = world.Habitability?[x, y] ?? 0f;
            if (h > 0f) landTiles.Add((h, x, y));
        }
        landTiles.Sort((a, b) => b.hab.CompareTo(a.hab));

        int nextId = world.Settlements.Count + 1;

        // Subtract narrative anchor counts so the TOTAL for each tier hits the target range.
        int existingTier2 = world.Settlements.Count(s => s.Tier == 2 && !s.IsPoi);
        int existingTier3 = world.Settlements.Count(s => s.Tier == 3 && !s.IsPoi);
        int existingTier4 = world.Settlements.Count(s => s.Tier == 4 && !s.IsPoi);

        // ── Tier 2 ────────────────────────────────────────────────────────────
        int tier2Target = rng.NextInt(C.SETTLE_TIER2_MIN, C.SETTLE_TIER2_MAX + 1);
        int tier2Count  = Math.Max(0, tier2Target - existingTier2);
        PlaceTier(world, landTiles, ref nextId, 2, tier2Count, C.SETTLE_MIN_DIST_TIER2, rng,
            (x, y, tile) =>
                (tile.Features & (FeatureFlags.HasRiver | FeatureFlags.RiverAdjacent)) != 0 ||
                tile.Biome == BiomeId.Coastal);

        // ── Tier 3 ────────────────────────────────────────────────────────────
        int tier3Target = rng.NextInt(C.SETTLE_TIER3_MIN, C.SETTLE_TIER3_MAX + 1);
        int tier3Count  = Math.Max(0, tier3Target - existingTier3);
        PlaceTier(world, landTiles, ref nextId, 3, tier3Count, C.SETTLE_MIN_DIST_TIER3, rng,
            (x, y, tile) => IsNearHigherTier(x, y, world.Settlements, 2, 60) ||
                            (tile.Features & (FeatureFlags.HasRiver | FeatureFlags.RiverAdjacent)) != 0);

        // ── Tier 4 ────────────────────────────────────────────────────────────
        int tier4Target = rng.NextInt(C.SETTLE_TIER4_MIN, C.SETTLE_TIER4_MAX + 1);
        int tier4Count  = Math.Max(0, tier4Target - existingTier4);
        PlaceTier(world, landTiles, ref nextId, 4, tier4Count, C.SETTLE_MIN_DIST_TIER4, rng,
            (x, y, tile) => true);

        // ── Tier 5 (PoIs) ─────────────────────────────────────────────────────
        int tier5Count = rng.NextInt(C.SETTLE_TIER5_MIN, C.SETTLE_TIER5_MAX + 1);
        PlacePoIs(world, landTiles, ref nextId, tier5Count, rng);

        world.StageHashes["SettlementPlace"] = world.HashSettlements();
        ctx.LogMessage($"[SettlementPlace] Total settlements: {world.Settlements.Count}");
    }

    private static void PlaceTier(
        WorldState world,
        List<(float hab, int x, int y)> sortedTiles,
        ref int nextId,
        int tier,
        int target,
        int minDist,
        SeededRng rng,
        Func<int, int, WorldTile, bool> extraFilter)
    {
        int placed = 0;
        foreach (var (_, x, y) in sortedTiles)
        {
            if (placed >= target) break;
            ref var tile = ref world.TileAt(x, y);
            if (tile.Biome == BiomeId.Ocean) continue;
            if ((tile.Features & FeatureFlags.IsSettlement) != 0) continue;
            if (!extraFilter(x, y, tile)) continue;
            if (IsTooClose(x, y, world.Settlements, tier, minDist)) continue;

            var (nx, ny) = NudgeOffRiver(world, x, y);
            var s = new Settlement { Id = nextId++, Tier = tier, TileX = nx, TileY = ny };
            world.Settlements.Add(s);
            MarkTiles(world, s);
            placed++;
        }
    }

    private static void PlacePoIs(
        WorldState world,
        List<(float hab, int x, int y)> sortedTiles,
        ref int nextId,
        int target,
        SeededRng rng)
    {
        int placed = 0;
        // PoIs go in LOW habitability cells — reverse the list
        for (int i = sortedTiles.Count - 1; i >= 0 && placed < target; i--)
        {
            var (hab, x, y) = sortedTiles[i];
            if (hab > 0.5f) break; // stop when habitability gets too high
            if (world.Tiles[x, y].Biome == BiomeId.Ocean) continue;
            if ((world.Tiles[x, y].Features & FeatureFlags.IsSettlement) != 0) continue;
            if (IsTooClose(x, y, world.Settlements, 5, C.SETTLE_MIN_DIST_TIER5)) continue;
            if (IsNearHigherTier(x, y, world.Settlements, 4, C.POI_MIN_DIST_FROM_SETTLE)) continue;

            var s = new Settlement
            {
                Id     = nextId++,
                Tier   = 5,
                TileX  = x,
                TileY  = y,
                IsPoi  = true,
                PoiType = PickPoiType(world, x, y, rng),
            };
            world.Settlements.Add(s);
            world.TileAt(x, y).Features |= FeatureFlags.IsPoi;
            placed++;
        }
    }

    private static PoiType PickPoiType(WorldState world, int x, int y, SeededRng rng)
    {
        var biome = world.Tiles[x, y].Biome;
        return biome switch
        {
            BiomeId.MountainAlpine or BiomeId.MountainForested => rng.NextBool() ? PoiType.AbandonedMine : PoiType.NaturalCave,
            BiomeId.TemperateDeciduous or BiomeId.Boreal or BiomeId.ForestEdge => rng.NextBool() ? PoiType.CultDen : PoiType.OvergrownSettlement,
            BiomeId.Tundra or BiomeId.Boreal => PoiType.ImperiumRuin,
            BiomeId.Wetland or BiomeId.SubtropicalForest or BiomeId.Mangrove => rng.NextBool() ? PoiType.NaturalCave : PoiType.CultDen,
            _ => (PoiType)(rng.NextInt(1, 6)),
        };
    }

    private static bool IsTooClose(int x, int y, List<Settlement> settlements, int tier, int minDist)
    {
        foreach (var s in settlements)
        {
            if (s.Tier > tier) continue; // only compare same or higher tiers
            int dx = x - s.TileX, dy = y - s.TileY;
            if (dx * dx + dy * dy < minDist * minDist) return true;
        }
        return false;
    }

    private static bool IsNearHigherTier(int x, int y, List<Settlement> settlements, int maxTier, int maxDist)
    {
        foreach (var s in settlements)
        {
            if (s.Tier > maxTier) continue;
            int dx = x - s.TileX, dy = y - s.TileY;
            if (dx * dx + dy * dy < maxDist * maxDist) return true;
        }
        return false;
    }

    /// <summary>
    /// If the candidate tile sits on a river, nudge to the nearest adjacent tile
    /// that is river-adjacent but not itself a river tile.  Keeps settlements
    /// beside rivers rather than on top of them.
    /// </summary>
    internal static (int x, int y) NudgeOffRiver(WorldState world, int x, int y)
    {
        ref var tile = ref world.TileAt(x, y);
        if ((tile.Features & FeatureFlags.HasRiver) == 0) return (x, y);

        // Scan outward in fixed order (deterministic) at radius 1, then 2
        for (int r = 1; r <= 2; r++)
        for (int dy = -r; dy <= r; dy++)
        for (int dx = -r; dx <= r; dx++)
        {
            if (dx == 0 && dy == 0) continue;
            if (Math.Abs(dx) != r && Math.Abs(dy) != r) continue; // shell only
            int nx = x + dx, ny = y + dy;
            if ((uint)nx >= W || (uint)ny >= H) continue;
            ref var neighbor = ref world.TileAt(nx, ny);
            if (neighbor.Biome == BiomeId.Ocean) continue;
            if ((neighbor.Features & FeatureFlags.HasRiver) != 0) continue;
            if ((neighbor.Features & FeatureFlags.IsSettlement) != 0) continue;
            return (nx, ny);
        }

        return (x, y); // no valid neighbor found — keep original
    }

    private static void MarkTiles(WorldState world, Settlement s)
    {
        for (int dy = -1; dy <= 1; dy++)
        for (int dx = -1; dx <= 1; dx++)
        {
            int nx = s.TileX + dx, ny = s.TileY + dy;
            if ((uint)nx >= W || (uint)ny >= H) continue;
            ref var tile = ref world.TileAt(nx, ny);
            tile.Features    |= FeatureFlags.IsSettlement;
            tile.SettlementId = (ushort)Math.Min(s.Id, ushort.MaxValue);
        }
    }
}
