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

namespace Theriapolis.Core.Dungeons;

/// <summary>
/// Phase 7 M1 — picks room templates per layout, then hands them to
/// <see cref="RoomGraphAssembler"/>. Pure deterministic given the seed
/// and layout.
///
/// Algorithm:
///   1. Roll <c>roomCount</c> in [<c>RoomCountMin</c>, <c>RoomCountMax</c>].
///   2. Pick the entry-room template (filtered to <c>role: entry</c>).
///   3. Reserve slots for required roles (boss, narrative, etc.). Insert
///      them last so they don't get crowded out by transit picks.
///   4. Fill remaining slots with transit / loot / dead-end picks.
///   5. Hand the template list + role list to the assembler.
///   6. On assembly failure, retry up to
///      <c>C.DUNGEON_LAYOUT_MAX_ATTEMPTS</c> times. If all retries fail,
///      fall back to a guaranteed-valid linear chain of N transit rooms.
///
/// Anchor-locked layouts (Old Howl mine, Imperium showcase) bypass this
/// pipeline — <see cref="DungeonGenerator"/> short-circuits to the pinned
/// template list directly.
/// </summary>
internal static class DungeonLayoutBuilder
{
    /// <summary>
    /// Build a fully-assembled <see cref="RoomGraphAssembler.Plan"/> for
    /// the given layout + content. Returns the plan from the first
    /// successful attempt; falls back to the linear-chain plan if every
    /// attempt fails.
    /// </summary>
    public static RoomGraphAssembler.Plan Build(
        DungeonLayoutDef layout,
        IReadOnlyList<RoomTemplateDef> typeTemplates,
        ulong layoutSeed)
    {
        if (layout is null) throw new ArgumentNullException(nameof(layout));
        if (typeTemplates is null || typeTemplates.Count == 0)
            throw new ArgumentException("no templates available for this dungeon type", nameof(typeTemplates));

        var rng = new SeededRng(layoutSeed);

        for (int attempt = 0; attempt < C.DUNGEON_LAYOUT_MAX_ATTEMPTS; attempt++)
        {
            int roomCount = rng.NextInt(layout.RoomCountMin, layout.RoomCountMax + 1);
            var (picks, roles) = PickTemplates(layout, typeTemplates, roomCount, rng);
            if (picks is null) continue;

            var plan = RoomGraphAssembler.TryAssemble(picks, roles!, layout.Branching, rng);
            if (plan is not null) return plan;
        }

        // Fallback: guaranteed-valid linear chain.
        return BuildLinearFallback(layout, typeTemplates, rng);
    }

    private static (RoomTemplateDef[]? picks, RoomRole[]? roles) PickTemplates(
        DungeonLayoutDef layout,
        IReadOnlyList<RoomTemplateDef> typeTemplates,
        int roomCount,
        SeededRng rng)
    {
        // Group templates by eligible role for fast picking.
        var byRole = new Dictionary<RoomRole, List<RoomTemplateDef>>();
        foreach (var t in typeTemplates)
            foreach (var roleTag in t.RolesEligible)
            {
                RoomRole role;
                try { role = RoomRoleExtensions.Parse(roleTag); } catch { continue; }
                if (!byRole.TryGetValue(role, out var list))
                    byRole[role] = list = new List<RoomTemplateDef>();
                list.Add(t);
            }

        // Required roles must each be satisfiable.
        var requiredRoles = new List<RoomRole>();
        foreach (var raw in layout.RequiredRoles)
        {
            try
            {
                var r = RoomRoleExtensions.Parse(raw);
                if (!byRole.ContainsKey(r) || byRole[r].Count == 0) return (null, null);
                requiredRoles.Add(r);
            }
            catch { return (null, null); }
        }

        // 1. Entry: pick from "entry" pool (always required).
        if (!byRole.TryGetValue(RoomRole.Entry, out var entryPool) || entryPool.Count == 0) return (null, null);
        var entryPick = PickWeighted(entryPool, rng);

        var picks = new List<RoomTemplateDef> { entryPick };
        var roles = new List<RoomRole> { RoomRole.Entry };

        // 2. Reserve required-role slots (excluding entry, which is implicit).
        var deferred = new List<RoomRole>();
        foreach (var r in requiredRoles)
        {
            if (r == RoomRole.Entry) continue;     // already placed
            deferred.Add(r);
        }

        // 3. Fill remaining slots with transit / loot / dead-end.
        var optionalRoles = new List<RoomRole>();
        foreach (var raw in layout.OptionalRoles)
        {
            try { optionalRoles.Add(RoomRoleExtensions.Parse(raw)); } catch { /* ignore */ }
        }
        if (optionalRoles.Count == 0) optionalRoles.Add(RoomRole.Transit);

        int slotsLeft = roomCount - 1 - deferred.Count;
        if (slotsLeft < 0) return (null, null);  // required-role count exceeds room count

        for (int i = 0; i < slotsLeft; i++)
        {
            var role = optionalRoles[rng.NextInt(0, optionalRoles.Count)];
            if (!byRole.TryGetValue(role, out var pool) || pool.Count == 0)
                role = RoomRole.Transit;
            // Fall back to typeTemplates if the role pool is empty.
            var fromPool = byRole.TryGetValue(role, out var p) && p.Count > 0
                ? PickWeighted(p, rng)
                : PickWeighted(typeTemplates, rng);
            picks.Add(fromPool);
            roles.Add(role);
        }

        // 4. Append deferred required-role picks (boss last so it ends the layout).
        deferred.Sort((a, b) => RolePriority(a).CompareTo(RolePriority(b)));
        foreach (var r in deferred)
        {
            picks.Add(PickWeighted(byRole[r], rng));
            roles.Add(r);
        }

        return (picks.ToArray(), roles.ToArray());
    }

    private static int RolePriority(RoomRole r) => r switch
    {
        RoomRole.Narrative => 0,
        RoomRole.Loot      => 1,
        RoomRole.DeadEnd   => 2,
        RoomRole.Boss      => 9,  // boss room last
        _                  => 5,
    };

    private static RoomTemplateDef PickWeighted(IReadOnlyList<RoomTemplateDef> pool, SeededRng rng)
    {
        if (pool.Count == 1) return pool[0];
        float total = 0f;
        foreach (var t in pool) total += t.Weight > 0 ? t.Weight : 1f;
        float roll = rng.NextFloat() * total;
        float acc  = 0f;
        foreach (var t in pool)
        {
            acc += t.Weight > 0 ? t.Weight : 1f;
            if (roll <= acc) return t;
        }
        return pool[pool.Count - 1];
    }

    private static RoomGraphAssembler.Plan BuildLinearFallback(
        DungeonLayoutDef layout,
        IReadOnlyList<RoomTemplateDef> typeTemplates,
        SeededRng rng)
    {
        // Linear chain: entry → N transits → boss (if required). The pure
        // last-resort path; any layout reaches it via a deterministic-but-
        // logged rng state.
        var entryPool = typeTemplates.Where(t => t.RolesEligible.Contains("entry", StringComparer.OrdinalIgnoreCase)).ToList();
        if (entryPool.Count == 0) entryPool = typeTemplates.ToList();
        var transitPool = typeTemplates.Where(t => t.RolesEligible.Contains("transit", StringComparer.OrdinalIgnoreCase)).ToList();
        if (transitPool.Count == 0) transitPool = typeTemplates.ToList();
        var bossPool = typeTemplates.Where(t => t.RolesEligible.Contains("boss", StringComparer.OrdinalIgnoreCase)).ToList();

        bool wantsBoss = layout.RequiredRoles.Contains("boss", StringComparer.OrdinalIgnoreCase) && bossPool.Count > 0;

        int roomCount = layout.RoomCountMin;
        var picks = new List<RoomTemplateDef> { PickWeighted(entryPool, rng) };
        var roles = new List<RoomRole> { RoomRole.Entry };

        int transitCount = wantsBoss ? roomCount - 2 : roomCount - 1;
        for (int i = 0; i < transitCount; i++)
        {
            picks.Add(PickWeighted(transitPool, rng));
            roles.Add(RoomRole.Transit);
        }
        if (wantsBoss)
        {
            picks.Add(PickWeighted(bossPool, rng));
            roles.Add(RoomRole.Boss);
        }

        // Force the linear branching policy here regardless of layout; the
        // fallback exists exactly because branching/loop assembly failed.
        var plan = RoomGraphAssembler.TryAssemble(picks, roles, "linear", rng);
        if (plan is null)
        {
            // Truly degenerate — single-room dungeon (entry only).
            var only = picks[0];
            int pad = C.DUNGEON_AABB_PADDING;
            var rooms = new[]
            {
                new Room
                {
                    Id = 0, TemplateId = only.Id,
                    AabbX = pad, AabbY = pad,
                    AabbW = only.FootprintWTiles, AabbH = only.FootprintHTiles,
                    BuiltBy = only.BuiltBy, Role = RoomRole.Entry,
                    NarrativeText = only.NarrativeText,
                },
            };
            (int X, int Y) entrance = only.Doors.Length > 0
                ? (pad + only.Doors[0].X, pad + only.Doors[0].Y)
                : (pad + only.FootprintWTiles / 2, pad);
            return new RoomGraphAssembler.Plan(
                rooms, Array.Empty<RoomConnection>(),
                only.FootprintWTiles + 2 * pad,
                only.FootprintHTiles + 2 * pad,
                entrance);
        }
        return plan;
    }
}
