using Theriapolis.Core.Data;
using Theriapolis.Core.Items;
using Theriapolis.Core.Loot;
using Theriapolis.Core.World;

namespace Theriapolis.Core.Dungeons;

/// <summary>
/// Phase 7 M2 — populate a generated <see cref="Dungeon"/>'s encounter
/// and container slots. Pure deterministic given the same inputs:
/// <c>(worldSeed, poiId, dungeon, content, levelBand)</c>.
///
/// For each room:
///   - Walk the room's source <see cref="RoomTemplateDef.EncounterSlots"/>
///     and resolve each slot to a concrete <see cref="NpcTemplateDef"/> via
///     <see cref="NpcTemplateContent.SpawnKindToTemplateByDungeonType"/>
///     (with a fallback to <see cref="NpcTemplateContent.SpawnKindToTemplateByZone"/>
///     middle tier if the per-dungeon-type table doesn't list that kind).
///   - Walk the source <see cref="RoomTemplateDef.ContainerSlots"/> and
///     pre-roll each container's loot via
///     <see cref="LootGenerator.RollContainer"/>, mapping the container's
///     <c>loot_table_band</c> through the layout's
///     <c>loot_table_per_band</c> to a concrete table id.
///
/// Boss-room encounter slots use the dungeon-type's <c>"Boss"</c>
/// template if available, otherwise fall back to the <c>"PoiGuard"</c>
/// template — no boss → strong-guard graceful degradation.
///
/// Per-NPC-spawn RNG sub-seed:
///   <c>populateSeed = dungeonLayoutSeed ^ C.RNG_DUNGEON_POPULATE ^ roomId ^ slotIdx</c>
/// — present so future variant rolls (e.g. random which-of-N-equivalent-
/// templates) stay deterministic. M2 doesn't read it (per-kind template
/// is fixed by the override map) but the helper is wired up so M3+
/// content can use it without a contract change.
/// </summary>
public static class DungeonPopulator
{
    /// <summary>
    /// Populate a freshly-generated dungeon. <paramref name="layoutId"/>
    /// is the dungeon-layout id (from <see cref="DungeonLayoutDef.Id"/>);
    /// the populator looks it up via <paramref name="content"/> to read
    /// loot-band → table mappings. <paramref name="levelBand"/> is the
    /// PoI's authored level band (0..3) and selects which loot tier
    /// each container slot rolls.
    /// </summary>
    public static DungeonPopulation Populate(
        Dungeon dungeon,
        DungeonLayoutDef layout,
        ContentResolver content,
        int levelBand,
        ulong worldSeed)
    {
        if (dungeon is null) throw new System.ArgumentNullException(nameof(dungeon));
        if (layout  is null) throw new System.ArgumentNullException(nameof(layout));
        if (content is null) throw new System.ArgumentNullException(nameof(content));

        ulong dungeonLayoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ (ulong)dungeon.PoiId;

        // Resolve loot-band → table-id mapping for this layout + level band.
        string lootBand = ResolveLootBand(layout, levelBand);
        layout.LootTablePerBand.TryGetValue(lootBand, out var lootTableForBand);

        var spawns     = new List<DungeonSpawn>();
        var containers = new List<DungeonContainer>();

        // Per-dungeon spawn-kind resolver lookup.
        string typeKey = dungeon.Type.ToString();
        content.Npcs.SpawnKindToTemplateByDungeonType.TryGetValue(typeKey, out var kindMap);

        int globalContainerIdx = 0;
        int globalSpawnIdx     = 0;

        foreach (var room in dungeon.Rooms)
        {
            if (!content.RoomTemplates.TryGetValue(room.TemplateId, out var def))
                continue;

            // Encounter slots → concrete spawns.
            foreach (var slot in def.EncounterSlots)
            {
                var template = ResolveSpawnTemplate(slot.Kind, room.Role, kindMap, content.Npcs, dungeon.Type);
                if (template is null) { globalSpawnIdx++; continue; }

                int absX = room.AabbX + slot.X;
                int absY = room.AabbY + slot.Y;
                spawns.Add(new DungeonSpawn(
                    RoomId:   room.Id,
                    X:        absX,
                    Y:        absY,
                    Template: template,
                    Kind:     slot.Kind));
                globalSpawnIdx++;
            }

            // Container slots → pre-rolled loot.
            foreach (var slot in def.ContainerSlots)
            {
                int absX = room.AabbX + slot.X;
                int absY = room.AabbY + slot.Y;
                // Per-container band: room's container slot may declare its
                // own band (e.g. boss room slot says "t3"); otherwise we
                // use the layout's level-band → loot-band lookup.
                string slotBand = !string.IsNullOrEmpty(slot.LootTableBand) ? slot.LootTableBand : lootBand;
                layout.LootTablePerBand.TryGetValue(slotBand, out var slotTableId);
                slotTableId ??= lootTableForBand ?? "";

                var drops = string.IsNullOrEmpty(slotTableId)
                    ? System.Array.Empty<ItemInstance>()
                    : LootGenerator.RollContainer(
                          tableId: slotTableId,
                          dungeonLayoutSeed: dungeonLayoutSeed,
                          slotIdx: globalContainerIdx,
                          tables: content.LootTables,
                          items:  content.Items);

                containers.Add(new DungeonContainer(
                    RoomId:   room.Id,
                    X:        absX,
                    Y:        absY,
                    TableId:  slotTableId ?? "",
                    Drops:    drops,
                    Locked:   slot.Locked,
                    LockTier: slot.Lock));
                globalContainerIdx++;
            }
        }

        return new DungeonPopulation(spawns.ToArray(), containers.ToArray());
    }

    private static string ResolveLootBand(DungeonLayoutDef layout, int levelBand)
    {
        var key = levelBand.ToString(System.Globalization.CultureInfo.InvariantCulture);
        if (layout.LevelBandToLootBand.TryGetValue(key, out var band) && !string.IsNullOrEmpty(band))
            return band;
        // Default per the Phase 7 plan §5.5 thresholds:
        // levelBand 0..1 → t1, 2 → t2, 3+ → t3.
        return levelBand switch
        {
            <= 1 => "t1",
            2    => "t2",
            _    => "t3",
        };
    }

    /// <summary>
    /// Resolve a slot's spawn-kind tag to a concrete NPC template. Boss
    /// slots get the dungeon-type's "Boss" template if listed; otherwise
    /// the slot's own kind, with a graceful fall-through to the
    /// existing per-zone table at the mid tier.
    /// </summary>
    private static NpcTemplateDef? ResolveSpawnTemplate(
        string slotKind,
        RoomRole roomRole,
        IReadOnlyDictionary<string, string>? kindMap,
        NpcTemplateContent npcs,
        PoiType dungeonType)
    {
        // Boss-role rooms with a boss kind: prefer the per-dungeon-type
        // "Boss" entry. If neither the slot says "Boss" nor the room is
        // a boss room, this branch doesn't fire.
        string effectiveKind = slotKind;
        if (roomRole == RoomRole.Boss && string.Equals(slotKind, "Boss", System.StringComparison.OrdinalIgnoreCase))
        {
            effectiveKind = "Boss";
        }

        // 1. Per-dungeon-type override.
        if (kindMap is not null && kindMap.TryGetValue(effectiveKind, out var tplId))
        {
            foreach (var t in npcs.Templates)
                if (string.Equals(t.Id, tplId, System.StringComparison.OrdinalIgnoreCase))
                    return t;
        }

        // 2. Boss kind unmapped → fall back to PoiGuard for the same dungeon type.
        if (effectiveKind == "Boss" && kindMap is not null && kindMap.TryGetValue("PoiGuard", out var guardId))
        {
            foreach (var t in npcs.Templates)
                if (string.Equals(t.Id, guardId, System.StringComparison.OrdinalIgnoreCase))
                    return t;
        }

        // 3. Final fallback: the per-zone table at zone 2 (mid).
        if (npcs.SpawnKindToTemplateByZone.TryGetValue(slotKind, out var byZone) && byZone.Length > 0)
        {
            int z = System.Math.Min(2, byZone.Length - 1);
            string id = byZone[z];
            foreach (var t in npcs.Templates)
                if (string.Equals(t.Id, id, System.StringComparison.OrdinalIgnoreCase))
                    return t;
        }
        return null;
    }
}
