using Theriapolis.Core.Data;
using Theriapolis.Core.World;

namespace Theriapolis.Core.Dungeons;

/// <summary>
/// Phase 7 M1 — top-level deterministic entry point for generating the
/// interior of a PoI on first visit.
///
/// Determinism contract: <c>(worldSeed, poiId)</c> → byte-identical
/// <see cref="Dungeon"/> across runs. Internally:
///   <c>dungeonLayoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ poiId</c>
///
/// Anchor-locked PoIs (Old Howl mine, Imperium Ruin showcase, Phase 7 M5
/// content) bypass the procedural pipeline by routing to a pinned-rooms
/// layout JSON. The pinned layout names the exact templates to use, in
/// order; the assembler's branching policy still applies (typically
/// linear). M1 does NOT pin any specific anchor PoI to a layout — that
/// wiring lands in M5 alongside <c>side_act_i_old_howl.json</c> and the
/// showcase rebuild. M1 ships the routing infrastructure.
/// </summary>
public static class DungeonGenerator
{
    /// <summary>
    /// Pure deterministic generator. Caller supplies the PoI's id (used as
    /// the per-dungeon sub-seed nonce), the dungeon type (drives layout
    /// selection + tile family), and the resolved content.
    ///
    /// <paramref name="anchorOverride"/> — optional anchor id; when set,
    /// the generator looks up
    /// <see cref="ContentResolver.DungeonLayoutsByAnchor"/> first. M1
    /// callers leave it null; M5+ wires the Old Howl + Imperium showcase
    /// anchor routing.
    /// </summary>
    public static Dungeon Generate(
        ulong worldSeed,
        int poiId,
        PoiType type,
        ContentResolver content,
        string? anchorOverride = null)
    {
        if (content is null) throw new ArgumentNullException(nameof(content));

        ulong layoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ (ulong)poiId;

        DungeonLayoutDef layout = ResolveLayout(type, content, anchorOverride);

        // Build the room-graph plan.
        RoomGraphAssembler.Plan plan;
        if (layout.PinnedRooms.Length > 0)
        {
            plan = AssemblePinnedLayout(layout, content, layoutSeed);
        }
        else
        {
            // Procedural layout — pick from typed templates.
            string typeKey = TypeKeyFor(type);
            if (!content.RoomTemplatesByType.TryGetValue(typeKey, out var typeTemplates) || typeTemplates.Count == 0)
                throw new InvalidOperationException(
                    $"No room templates for dungeon type '{typeKey}' (PoiType.{type}). " +
                    "Did you author Content/Data/room_templates/{type}/*.json?");

            plan = DungeonLayoutBuilder.Build(layout, typeTemplates, layoutSeed);
        }

        // Paint the tiles.
        var tiles = RoomTilePainter.Paint(plan, content.RoomTemplates, type);

        return new Dungeon(
            poiId:        poiId,
            type:         type,
            tiles:        tiles,
            rooms:        plan.Rooms,
            connections:  plan.Connections,
            entranceTile: plan.EntranceTile);
    }

    /// <summary>
    /// Find a procedural layout for the given type. M1 picks the first
    /// matching layout deterministically (room-count band tied to layout
    /// id); M2+ may add LevelBand-driven small/medium/large selection.
    /// </summary>
    private static DungeonLayoutDef ResolveLayout(PoiType type, ContentResolver content, string? anchorOverride)
    {
        if (!string.IsNullOrEmpty(anchorOverride)
            && content.DungeonLayoutsByAnchor.TryGetValue(anchorOverride, out var pinned))
            return pinned;

        // Find the first non-anchor layout for this type. Stable order:
        // dictionary iteration is unordered in C# but DungeonLayouts is built
        // from a sorted file list (LoadDungeonLayouts orders by file path),
        // so iteration follows that order on .NET 8.
        foreach (var l in content.DungeonLayouts.Values)
        {
            if (!string.IsNullOrEmpty(l.Anchor)) continue;
            if (string.Equals(l.DungeonType, type.ToString(), StringComparison.OrdinalIgnoreCase))
                return l;
        }
        throw new InvalidOperationException(
            $"No dungeon layout found for PoiType.{type}. " +
            "Author Content/Data/dungeon_layouts/<type>_<size>.json.");
    }

    private static string TypeKeyFor(PoiType type) => type switch
    {
        PoiType.ImperiumRuin        => "imperium",
        PoiType.AbandonedMine       => "mine",
        PoiType.CultDen             => "cult",
        PoiType.NaturalCave         => "cave",
        PoiType.OvergrownSettlement => "overgrown",
        _                           => "imperium",
    };

    /// <summary>
    /// Build a plan from a pinned-rooms layout. Pinned layouts always use
    /// linear branching (the canonical Old Howl + Imperium showcase shape)
    /// — no retry, no fallback, no random pick.
    /// </summary>
    private static RoomGraphAssembler.Plan AssemblePinnedLayout(
        DungeonLayoutDef layout, ContentResolver content, ulong layoutSeed)
    {
        var picks = new List<RoomTemplateDef>(layout.PinnedRooms.Length);
        var roles = new List<RoomRole>(layout.PinnedRooms.Length);
        foreach (var pin in layout.PinnedRooms)
        {
            if (!content.RoomTemplates.TryGetValue(pin.Template, out var def))
                throw new InvalidOperationException(
                    $"Pinned layout '{layout.Id}' references unknown template '{pin.Template}'. " +
                    "ContentLoader should have caught this — re-run content-validate.");
            picks.Add(def);
            roles.Add(RoomRoleExtensions.Parse(pin.Role));
        }
        var rng = new Util.SeededRng(layoutSeed);
        var plan = RoomGraphAssembler.TryAssemble(picks, roles, "linear", rng);
        if (plan is null)
            throw new InvalidOperationException(
                $"Pinned layout '{layout.Id}' failed to assemble. Pinned layouts must be hand-validated.");
        return plan;
    }
}
