using Theriapolis.Core.Data;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.Util;

namespace Theriapolis.Core.World.Settlements;

/// <summary>
/// Phase 6 M0 — drives building stamping for tactical chunks.
///
/// The original Phase 4 plan promised template-driven building burn-in
/// (<c>theriapolis-rpg-implementation-plan-phase4.md §3.4 step 3</c>) but
/// only a placeholder cobble plaza + outer wall ring shipped. This module
/// catches that up.
///
/// Two paths:
/// 1. **Content-driven** — when a <see cref="SettlementContent"/> is
///    available the stamper resolves a <see cref="SettlementLayoutDef"/>
///    for each settlement (preset by anchor, else procedural by tier),
///    materialises buildings on the settlement (lazily, once), and stamps
///    walls/floors/doors/decos for any building intersecting the chunk.
///    Per-resident <see cref="TacticalSpawn"/>s are emitted for Phase 6 M1
///    instantiation.
/// 2. **Fallback** — when content is unavailable (e.g. headless tools,
///    early-stage tests) the stamper writes the original cobble plaza +
///    outer wall ring so the existing Phase 5 visual baseline holds.
///
/// Determinism:
/// - Building list per settlement is resolved with a SeededRng keyed by
///   <c>worldSeed ^ RNG_BUILDING_LAYOUT ^ settlementId</c>; identical across
///   reloads.
/// - Procedural Tier 2–5 layouts roll templates and offsets from this RNG;
///   preset (Tier 1 / anchor) layouts are entirely data-driven.
/// </summary>
public static class SettlementStamper
{
    /// <summary>
    /// Half-width (in tactical tiles) of the gateway carved through a
    /// settlement's wall ring wherever a road passes. Sized to the widest
    /// road the world can produce plus 1-tile slop on each side. Mirrors
    /// the Phase-4 placeholder constant.
    /// </summary>
    private const int GatewayHaloTiles = 3;

    public static void Stamp(
        ulong worldSeed,
        TacticalChunk chunk,
        WorldState world,
        SettlementContent? content)
    {
        int x0 = chunk.OriginX;
        int y0 = chunk.OriginY;
        int x1 = x0 + C.TACTICAL_CHUNK_SIZE;
        int y1 = y0 + C.TACTICAL_CHUNK_SIZE;

        foreach (var s in world.Settlements)
        {
            // Cheap chunk-vs-settlement overlap reject — covers both the
            // plaza ring (Phase 5 placeholder behaviour) and any building
            // footprints we may have stamped on the settlement.
            if (!OverlapsChunk(s, content, x0, y0, x1, y1)) continue;

            // Always lay the plaza + ring first; buildings stamp on top.
            StampPlazaAndWallRing(chunk, s, x0, y0, x1, y1);

            if (content is not null)
                StampBuildings(worldSeed, chunk, s, content, x0, y0, x1, y1);
        }
    }

    // ── Lazy resolution of per-settlement building list ─────────────────

    /// <summary>
    /// Build the settlement's <see cref="Settlement.Buildings"/> list from
    /// its matched layout. Idempotent — only runs once per settlement.
    /// </summary>
    public static void EnsureBuildingsResolved(ulong worldSeed, Settlement s, SettlementContent content)
    {
        if (s.BuildingsResolved) return;
        var layout = content.ResolveFor(s);
        if (layout is null)
        {
            s.BuildingsResolved = true;  // nothing to stamp — bail with empty list
            return;
        }

        var rng = SeededRng.ForSubsystem(worldSeed, C.RNG_BUILDING_LAYOUT ^ (ulong)s.Id);
        var buildings = layout.Kind == "preset"
            ? ResolvePreset(s, layout, content)
            : ResolveProcedural(s, layout, content, rng);

        s.Buildings.Clear();
        s.Buildings.AddRange(buildings);
        s.BuildingsResolved = true;
    }

    private static IEnumerable<BuildingFootprint> ResolvePreset(
        Settlement s,
        SettlementLayoutDef layout,
        SettlementContent content)
    {
        int cxPx = (int)s.WorldPixelX;
        int cyPx = (int)s.WorldPixelY;
        int next = 0;

        foreach (var p in layout.Buildings)
        {
            if (!content.Buildings.TryGetValue(p.Template, out var def)) continue;
            int ox = p.Offset.Length >= 1 ? p.Offset[0] : 0;
            int oy = p.Offset.Length >= 2 ? p.Offset[1] : 0;

            int minX = cxPx + ox - def.FootprintWTiles / 2;
            int minY = cyPx + oy - def.FootprintHTiles / 2;
            int maxX = minX + def.FootprintWTiles - 1;
            int maxY = minY + def.FootprintHTiles - 1;

            yield return BuildFootprint(next++, def, minX, minY, maxX, maxY, p.RoleOverrides);
        }
    }

    private static IEnumerable<BuildingFootprint> ResolveProcedural(
        Settlement s,
        SettlementLayoutDef layout,
        SettlementContent content,
        SeededRng rng)
    {
        // Pull eligible templates. Tier numbering is inverted: Tier 1 is the
        // capital (largest), Tier 5 is the hamlet (smallest). "Min tier
        // eligible" is the *smallest settlement* where this template fits;
        // a magistrate with min_tier_eligible=2 belongs in Tier 1 capitals
        // and Tier 2 cities only — so the predicate is s.Tier <= MinTier.
        var eligible = content.Buildings.Values
            .Where(b => s.Tier <= b.MinTierEligible)
            .Where(b => layout.CategoryWeights.ContainsKey(b.Category))
            .OrderBy(b => b.Id, System.StringComparer.Ordinal) // stable order before RNG roll
            .ToArray();
        if (eligible.Length == 0) yield break;

        int cxPx = (int)s.WorldPixelX;
        int cyPx = (int)s.WorldPixelY;
        int plazaR = layout.PlazaRadiusTiles > 0
            ? layout.PlazaRadiusTiles
            : DefaultPlazaRadiusTiles(s.Tier);

        // Slot grid: try concentric rings inside the plaza for placement.
        var placed = new List<(int minX, int minY, int maxX, int maxY)>();
        int placedCount = 0;
        int attempts = 0;
        int next = 0;

        while (placedCount < layout.TargetBuildingCount && attempts < layout.TargetBuildingCount * 8)
        {
            attempts++;
            // Weighted roll by category, then template.
            string category = WeightedPick(layout.CategoryWeights, rng);
            var inCategory = eligible.Where(b => string.Equals(b.Category, category, System.StringComparison.OrdinalIgnoreCase)).ToArray();
            if (inCategory.Length == 0) continue;
            var def = WeightedPickByTemplateWeight(inCategory, rng);

            // Pick a candidate offset anywhere inside the plaza, biased toward outer rings to leave a centre.
            int rx = rng.NextInt(-plazaR + def.FootprintWTiles / 2 + 2,
                                  plazaR - def.FootprintWTiles / 2 - 1);
            int ry = rng.NextInt(-plazaR + def.FootprintHTiles / 2 + 2,
                                  plazaR - def.FootprintHTiles / 2 - 1);

            int minX = cxPx + rx - def.FootprintWTiles / 2;
            int minY = cyPx + ry - def.FootprintHTiles / 2;
            int maxX = minX + def.FootprintWTiles - 1;
            int maxY = minY + def.FootprintHTiles - 1;

            // Reject if it overlaps another placed building (with the gap padding).
            bool collide = false;
            foreach (var p in placed)
            {
                if (RectsOverlap(
                        minX - C.SETTLEMENT_BUILDING_GAP_MIN, minY - C.SETTLEMENT_BUILDING_GAP_MIN,
                        maxX + C.SETTLEMENT_BUILDING_GAP_MIN, maxY + C.SETTLEMENT_BUILDING_GAP_MIN,
                        p.minX, p.minY, p.maxX, p.maxY))
                {
                    collide = true;
                    break;
                }
            }
            if (collide) continue;

            placed.Add((minX, minY, maxX, maxY));
            placedCount++;
            yield return BuildFootprint(next++, def, minX, minY, maxX, maxY, roleOverrides: null);
        }
    }

    private static BuildingFootprint BuildFootprint(
        int id, BuildingTemplateDef def,
        int minX, int minY, int maxX, int maxY,
        Dictionary<string, string>? roleOverrides)
    {
        var doors = new (int X, int Y)[def.Doors.Length];
        for (int i = 0; i < def.Doors.Length; i++)
            doors[i] = (minX + def.Doors[i].X, minY + def.Doors[i].Y);

        var residents = new BuildingResidentSlot[def.Roles.Length];
        for (int i = 0; i < def.Roles.Length; i++)
        {
            var r = def.Roles[i];
            string role = r.Tag;
            if (roleOverrides is not null && roleOverrides.TryGetValue(r.Tag, out var named))
                role = named;
            residents[i] = new BuildingResidentSlot(
                role,
                minX + r.SpawnAt[0],
                minY + r.SpawnAt[1],
                def.Category);
        }

        return new BuildingFootprint
        {
            Id         = id,
            TemplateId = def.Id,
            MinX       = minX,
            MinY       = minY,
            MaxX       = maxX,
            MaxY       = maxY,
            Doors      = doors,
            Residents  = residents,
        };
    }

    // ── Tile stamping ────────────────────────────────────────────────────

    private static void StampPlazaAndWallRing(
        TacticalChunk chunk, Settlement s,
        int x0, int y0, int x1, int y1)
    {
        // Replicates the original Phase 5 placeholder behaviour. Buildings
        // (if any) stamp on top.
        int tilesRadius = s.Tier switch { 1 => 2, <= 4 => 1, _ => 0 };
        int cxw = s.TileX, cyw = s.TileY;
        int minWX = Math.Max(0, cxw - tilesRadius);
        int minWY = Math.Max(0, cyw - tilesRadius);
        int maxWX = Math.Min(C.WORLD_WIDTH_TILES  - 1, cxw + tilesRadius);
        int maxWY = Math.Min(C.WORLD_HEIGHT_TILES - 1, cyw + tilesRadius);

        int fx0 = minWX * C.TACTICAL_PER_WORLD_TILE;
        int fy0 = minWY * C.TACTICAL_PER_WORLD_TILE;
        int fx1 = (maxWX + 1) * C.TACTICAL_PER_WORLD_TILE;
        int fy1 = (maxWY + 1) * C.TACTICAL_PER_WORLD_TILE;

        int sx = Math.Max(x0, fx0);
        int sy = Math.Max(y0, fy0);
        int ex = Math.Min(x1, fx1);
        int ey = Math.Min(y1, fy1);
        if (sx >= ex || sy >= ey) return;

        int cxPx = (int)s.WorldPixelX;
        int cyPx = (int)s.WorldPixelY;
        int plazaR = s.Tier switch { 1 => 24, 2 => 18, 3 => 14, 4 => 10, _ => 6 };
        int wallR  = plazaR + 2;

        for (int ty = sy; ty < ey; ty++)
        for (int tx = sx; tx < ex; tx++)
        {
            int dx = tx - cxPx;
            int dy = ty - cyPx;
            int dist = Math.Max(Math.Abs(dx), Math.Abs(dy));
            int lx = tx - chunk.OriginX;
            int ly = ty - chunk.OriginY;
            ref var dst = ref chunk.Tiles[lx, ly];

            if (dist <= plazaR)
            {
                if ((dst.Flags & (byte)TacticalFlags.River) == 0)
                    dst.Surface = TacticalSurface.Cobble;
                dst.Flags |= (byte)TacticalFlags.Settlement;
                dst.Deco   = TacticalDeco.None;
            }
            else if (dist == wallR && !s.IsPoi)
            {
                if ((dst.Flags & (byte)TacticalFlags.River) == 0 &&
                    !HasRoadInHalo(chunk, lx, ly, GatewayHaloTiles))
                {
                    dst.Surface = TacticalSurface.Wall;
                    dst.Flags  |= (byte)TacticalFlags.Settlement | (byte)TacticalFlags.Impassable;
                    dst.Deco    = TacticalDeco.None;
                }
            }
        }
    }

    private static void StampBuildings(
        ulong worldSeed,
        TacticalChunk chunk,
        Settlement s,
        SettlementContent content,
        int x0, int y0, int x1, int y1)
    {
        EnsureBuildingsResolved(worldSeed, s, content);

        foreach (var b in s.Buildings)
        {
            // Quick AABB intersect with this chunk's tactical window.
            if (b.MaxX < x0 || b.MinX >= x1 || b.MaxY < y0 || b.MinY >= y1) continue;

            int sx = Math.Max(x0, b.MinX);
            int sy = Math.Max(y0, b.MinY);
            int ex = Math.Min(x1, b.MaxX + 1);
            int ey = Math.Min(y1, b.MaxY + 1);
            if (sx >= ex || sy >= ey) continue;

            for (int ty = sy; ty < ey; ty++)
            for (int tx = sx; tx < ex; tx++)
            {
                int lx = tx - chunk.OriginX;
                int ly = ty - chunk.OriginY;
                ref var dst = ref chunk.Tiles[lx, ly];

                bool perimeter = (tx == b.MinX || tx == b.MaxX || ty == b.MinY || ty == b.MaxY);
                if (perimeter)
                {
                    dst.Surface = TacticalSurface.Wall;
                    // Walls block but doorways don't — we patch doors next.
                    dst.Flags |= (byte)(TacticalFlags.Settlement | TacticalFlags.Building | TacticalFlags.Impassable);
                    dst.Deco   = TacticalDeco.None;
                }
                else
                {
                    dst.Surface = TacticalSurface.Floor;
                    dst.Flags |= (byte)(TacticalFlags.Settlement | TacticalFlags.Building);
                    // Clear Impassable for floors (a wall may have been stamped previously).
                    dst.Flags &= unchecked((byte)~(byte)TacticalFlags.Impassable);
                    dst.Deco   = TacticalDeco.None;
                }
            }

            // Doors override perimeter walls so the building is enterable.
            foreach (var (dx, dy) in b.Doors)
            {
                if (dx < x0 || dx >= x1 || dy < y0 || dy >= y1) continue;
                int lx = dx - chunk.OriginX;
                int ly = dy - chunk.OriginY;
                ref var dst = ref chunk.Tiles[lx, ly];
                dst.Surface = TacticalSurface.Floor;
                dst.Deco    = TacticalDeco.Door;
                dst.Flags  |= (byte)(TacticalFlags.Settlement | TacticalFlags.Building | TacticalFlags.Doorway);
                dst.Flags  &= unchecked((byte)~(byte)TacticalFlags.Impassable);
            }

            // Interior decos.
            if (content.Buildings.TryGetValue(b.TemplateId, out var def))
            {
                foreach (var deco in def.Decos)
                {
                    int dx = b.MinX + deco.X;
                    int dy = b.MinY + deco.Y;
                    if (dx < x0 || dx >= x1 || dy < y0 || dy >= y1) continue;
                    int lx = dx - chunk.OriginX;
                    int ly = dy - chunk.OriginY;
                    ref var dst = ref chunk.Tiles[lx, ly];
                    dst.Deco = ParseDeco(deco.Deco);
                }
            }

            // Resident spawn records.
            foreach (var r in b.Residents)
            {
                if (r.SpawnX < x0 || r.SpawnX >= x1 || r.SpawnY < y0 || r.SpawnY >= y1) continue;
                int lx = r.SpawnX - chunk.OriginX;
                int ly = r.SpawnY - chunk.OriginY;
                chunk.Spawns.Add(new TacticalSpawn(SpawnKind.Resident, lx, ly));
            }
        }
    }

    // ── Helpers ──────────────────────────────────────────────────────────

    private static bool OverlapsChunk(Settlement s, SettlementContent? content, int x0, int y0, int x1, int y1)
    {
        // If buildings have already been resolved on this settlement, use
        // their union AABB; otherwise fall back to the plaza+wall radius
        // we'd stamp anyway.
        int cxPx = (int)s.WorldPixelX;
        int cyPx = (int)s.WorldPixelY;
        int plazaR = s.Tier switch { 1 => 28, 2 => 22, 3 => 18, 4 => 14, _ => 10 };
        int extentMinX = cxPx - plazaR, extentMinY = cyPx - plazaR;
        int extentMaxX = cxPx + plazaR, extentMaxY = cyPx + plazaR;

        if (s.BuildingsResolved && s.Buildings.Count > 0)
        {
            foreach (var b in s.Buildings)
            {
                extentMinX = Math.Min(extentMinX, b.MinX);
                extentMinY = Math.Min(extentMinY, b.MinY);
                extentMaxX = Math.Max(extentMaxX, b.MaxX);
                extentMaxY = Math.Max(extentMaxY, b.MaxY);
            }
        }
        return !(extentMaxX < x0 || extentMinX >= x1 || extentMaxY < y0 || extentMinY >= y1);
    }

    private static bool HasRoadInHalo(TacticalChunk chunk, int lx, int ly, int halo)
    {
        const byte ROAD = (byte)TacticalFlags.Road;
        int sx = Math.Max(0, lx - halo);
        int sy = Math.Max(0, ly - halo);
        int ex = Math.Min(C.TACTICAL_CHUNK_SIZE - 1, lx + halo);
        int ey = Math.Min(C.TACTICAL_CHUNK_SIZE - 1, ly + halo);
        for (int qy = sy; qy <= ey; qy++)
        for (int qx = sx; qx <= ex; qx++)
            if ((chunk.Tiles[qx, qy].Flags & ROAD) != 0) return true;
        return false;
    }

    private static bool RectsOverlap(int aMinX, int aMinY, int aMaxX, int aMaxY,
                                     int bMinX, int bMinY, int bMaxX, int bMaxY)
        => !(aMaxX < bMinX || bMaxX < aMinX || aMaxY < bMinY || bMaxY < aMinY);

    private static int DefaultPlazaRadiusTiles(int tier) => tier switch
    {
        1 => 24,
        2 => 18,
        3 => 14,
        4 => 10,
        _ => 6,
    };

    private static TacticalDeco ParseDeco(string raw) => raw.ToLowerInvariant() switch
    {
        "counter" => TacticalDeco.Counter,
        "bed"     => TacticalDeco.Bed,
        "hearth"  => TacticalDeco.Hearth,
        "sign"    => TacticalDeco.Sign,
        _         => TacticalDeco.None,
    };

    private static string WeightedPick(IReadOnlyDictionary<string, float> weights, SeededRng rng)
    {
        // Stable order — keys sorted ascending — so identical seeds always
        // pick identically regardless of dictionary insertion order.
        var keys = weights.Keys.OrderBy(k => k, System.StringComparer.Ordinal).ToArray();
        float total = 0f;
        foreach (var k in keys) total += System.Math.Max(0f, weights[k]);
        if (total <= 0f) return keys[0];
        float roll = rng.NextFloat() * total;
        float acc  = 0f;
        foreach (var k in keys)
        {
            acc += System.Math.Max(0f, weights[k]);
            if (roll <= acc) return k;
        }
        return keys[^1];
    }

    private static BuildingTemplateDef WeightedPickByTemplateWeight(BuildingTemplateDef[] templates, SeededRng rng)
    {
        float total = 0f;
        foreach (var t in templates) total += System.Math.Max(0f, t.Weight);
        if (total <= 0f) return templates[0];
        float roll = rng.NextFloat() * total;
        float acc  = 0f;
        foreach (var t in templates)
        {
            acc += System.Math.Max(0f, t.Weight);
            if (roll <= acc) return t;
        }
        return templates[^1];
    }
}
