using Theriapolis.Core.Data;
using Theriapolis.Core.Entities;
using Theriapolis.Core.Tactical;
using Theriapolis.Core.World;
using Theriapolis.Core.World.Settlements;
using Theriapolis.Core.Util;

namespace Theriapolis.Core.Rules.Combat;

/// <summary>
/// Phase 6 M1 — turns a chunk's <see cref="SpawnKind.Resident"/> records
/// into live <see cref="NpcActor"/>s.
///
/// Each <see cref="TacticalSpawn"/> with kind Resident sits at a
/// world-pixel position that <see cref="SettlementStamper"/> emitted from a
/// <see cref="BuildingResidentSlot"/>. Resolution:
///
///   1. Walk the world's settlements. Find the one whose
///      <see cref="Settlement.Buildings"/> contains a building footprint
///      that contains this spawn point. Within that building, find the
///      slot whose <c>SpawnX/SpawnY</c> match — that's the role tag.
///   2. Look up the resident template. Named (anchor-prefixed) tags hit
///      <see cref="ContentResolver.ResidentsByRoleTag"/> directly. Generic
///      tags hit <see cref="ContentResolver.Residents"/> filtered by
///      <see cref="ResidentTemplateDef.RoleTag"/> equality, weighted by
///      <see cref="ResidentTemplateDef.Weight"/>.
///   3. Build an <see cref="NpcActor"/> from the chosen template. Register
///      named-role NPCs in the <see cref="AnchorRegistry"/> so quest
///      scripts can resolve them by symbolic id.
///
/// The lookup is a linear walk over settlements (small N — &lt; 100) but is
/// deterministic for a given (worldSeed, chunk, spawnIndex).
/// </summary>
public static class ResidentInstantiator
{
    /// <summary>
    /// Resolve and spawn an NpcActor for a single Resident spawn record.
    /// Returns null when the world has no resident template configured for
    /// this slot's role tag (the spawn is silently dropped — the building
    /// just stays empty, which is fine).
    /// </summary>
    public static NpcActor? Spawn(
        ulong worldSeed,
        TacticalChunk chunk,
        int spawnIndex,
        TacticalSpawn spawn,
        WorldState world,
        ContentResolver content,
        ActorManager actors,
        AnchorRegistry? registry = null)
    {
        if (spawn.Kind != SpawnKind.Resident) return null;

        int worldPxX = chunk.OriginX + spawn.LocalX;
        int worldPxY = chunk.OriginY + spawn.LocalY;

        if (!TryFindSlot(world, worldPxX, worldPxY, out var settlement, out var building, out var slot))
            return null;

        var template = ResolveTemplate(slot.RoleTag, content, worldSeed, settlement!.Id, building!.Id, spawnIndex);
        if (template is null) return null;

        var npc = new NpcActor(template)
        {
            Id               = -1,  // ActorManager assigns
            Position         = new Vec2(worldPxX, worldPxY),
            SourceChunk      = chunk.Coord,
            SourceSpawnIndex = spawnIndex,
            // The named role tag wins over the generic one declared on the
            // template — preserves "millhaven.innkeeper" identity even when
            // the generic "innkeeper" template is what spawned.
            RoleTag          = string.IsNullOrEmpty(slot.RoleTag) ? template.RoleTag : slot.RoleTag,
            // Phase 6 M5 — anchor the resident to its host settlement so
            // RepPropagation can compute their local faction standing.
            HomeSettlementId = settlement.Id,
        };

        var spawned = actors.SpawnNpc(npc);
        if (registry is not null)
        {
            if (settlement.Anchor is not null)
                registry.RegisterAnchor(settlement.Anchor.Value, settlement.Id);
            registry.RegisterRole(spawned.RoleTag, spawned.Id);
        }
        return spawned;
    }

    /// <summary>
    /// Pick the resident template for a given role tag. Named anchor-
    /// prefixed tags ("millhaven.innkeeper") prefer named templates;
    /// generic tags ("innkeeper") roll among matching generics by weight.
    /// </summary>
    public static ResidentTemplateDef? ResolveTemplate(
        string roleTag,
        ContentResolver content,
        ulong worldSeed,
        int settlementId,
        int buildingId,
        int spawnIndex)
    {
        // Named, anchor-prefixed: prefer the exact match.
        if (content.ResidentsByRoleTag.TryGetValue(roleTag, out var named))
            return named;

        // Generic: collect all unnamed templates whose RoleTag equals the
        // suffix-stripped tag (e.g. "millhaven.innkeeper" → "innkeeper").
        string suffix = roleTag;
        int dot = roleTag.LastIndexOf('.');
        if (dot >= 0) suffix = roleTag[(dot + 1)..];

        var pool = new List<ResidentTemplateDef>();
        foreach (var r in content.Residents.Values)
            if (!r.Named && string.Equals(r.RoleTag, suffix, System.StringComparison.OrdinalIgnoreCase))
                pool.Add(r);
        if (pool.Count == 0) return null;
        if (pool.Count == 1) return pool[0];

        // Weighted roll, deterministic per (worldSeed, settlementId, buildingId, spawnIndex).
        var rng = SeededRng.ForSubsystem(worldSeed,
            unchecked(C.RNG_NPC_SPAWN ^ (ulong)settlementId
                      ^ ((ulong)buildingId << 16)
                      ^ ((ulong)spawnIndex << 32)));
        // Sort for stable iteration before the RNG roll.
        pool.Sort(static (a, b) => string.Compare(a.Id, b.Id, System.StringComparison.Ordinal));
        float total = 0f;
        foreach (var t in pool) total += System.Math.Max(0f, t.Weight);
        if (total <= 0f) return pool[0];
        float roll = rng.NextFloat() * total;
        float acc  = 0f;
        foreach (var t in pool)
        {
            acc += System.Math.Max(0f, t.Weight);
            if (roll <= acc) return t;
        }
        return pool[^1];
    }

    /// <summary>
    /// Walk the world's settlements to find the one whose building footprint
    /// contains <paramref name="worldPxX"/>/<paramref name="worldPxY"/> AND
    /// whose resident slot sits exactly on that point.
    /// </summary>
    public static bool TryFindSlot(
        WorldState world, int worldPxX, int worldPxY,
        out Settlement? settlement, out BuildingFootprint? building, out BuildingResidentSlot slot)
    {
        settlement = null;
        building   = null;
        slot       = default;

        foreach (var s in world.Settlements)
        {
            if (!s.BuildingsResolved) continue;
            foreach (var b in s.Buildings)
            {
                if (!b.ContainsTile(worldPxX, worldPxY)) continue;
                foreach (var r in b.Residents)
                {
                    if (r.SpawnX == worldPxX && r.SpawnY == worldPxY)
                    {
                        settlement = s;
                        building   = b;
                        slot       = r;
                        return true;
                    }
                }
            }
        }
        return false;
    }
}
