using NeonSprawl.Server.Game.Skills;

namespace NeonSprawl.Server.Game.Mastery;

/// <summary>
/// Server-authoritative perk unlock evaluation from skill level + branch picks (NEO-47).
/// Tier rule: all branches have empty <c>perkIds</c> ⇒ explicit pick tier; otherwise path-auto when a prior pick tier exists.
/// Keep in sync with <see cref="MasteryCatalogLoader"/> tier shape — see <see cref="TierRequiresExplicitPick"/>.
/// </summary>
public sealed class PerkUnlockEngine(
    IMasteryCatalogRegistry catalog,
    IPlayerPerkStateStore perkStore,
    IPlayerSkillProgressionStore xpStore,
    ISkillLevelCurve levelCurve)
{
    /// <summary>Selects a branch for a pick tier; unlocks tier perks and retroactive path-auto tiers at current level.</summary>
    public PerkBranchSelectOutcome TrySelectBranch(
        string playerId,
        string skillIdRaw,
        int tierIndex,
        string branchIdRaw)
    {
        var snapshot = perkStore.GetSnapshot(playerId);
        var skillId = skillIdRaw.Trim();
        var branchId = branchIdRaw.Trim();
        if (skillId.Length == 0 || branchId.Length == 0)
        {
            return Deny(snapshot, PerkUnlockReasonCodes.UnknownTrack);
        }

        if (!catalog.TryGetTrack(skillId, out var track))
        {
            return Deny(snapshot, PerkUnlockReasonCodes.UnknownTrack);
        }

        var tier = FindTier(track, tierIndex);
        if (tier is null)
        {
            return Deny(snapshot, PerkUnlockReasonCodes.UnknownTier);
        }

        if (TierIsPathAuto(tier))
        {
            return Deny(snapshot, PerkUnlockReasonCodes.TierBranchNotSelectable);
        }

        if (!TierContainsBranch(tier, branchId))
        {
            return Deny(snapshot, PerkUnlockReasonCodes.UnknownBranch);
        }

        if (!perkStore.CanWritePlayer(playerId))
        {
            return Deny(snapshot, PerkUnlockReasonCodes.PlayerNotFound);
        }

        var level = GetSkillLevel(playerId, track.SkillId);
        if (level < tier.RequiredLevel)
        {
            return Deny(snapshot, PerkUnlockReasonCodes.LevelTooLow);
        }

        if (snapshot.TryGetBranchPick(track.SkillId, tier.TierIndex, out _))
        {
            return Deny(snapshot, PerkUnlockReasonCodes.BranchAlreadyChosen);
        }

        if (!perkStore.TrySetBranchPick(playerId, track.SkillId, tier.TierIndex, branchId))
        {
            return Deny(snapshot, PerkUnlockReasonCodes.PlayerNotFound);
        }

        var branch = tier.Branches.First(b => string.Equals(b.BranchId, branchId, StringComparison.Ordinal));
        var allEvents = new List<PerkUnlockEvent>();
        if (!TryUnlockPerks(
                playerId,
                track.SkillId,
                tier.TierIndex,
                branch.BranchId,
                branch.PerkIds,
                PerkUnlockSource.BranchPick,
                snapshot,
                out var pickEvents))
        {
            _ = perkStore.TryRemoveBranchPick(playerId, track.SkillId, tier.TierIndex);
            return Deny(perkStore.GetSnapshot(playerId), PerkUnlockReasonCodes.PlayerNotFound);
        }

        allEvents.AddRange(pickEvents);
        snapshot = perkStore.GetSnapshot(playerId);
        allEvents.AddRange(ApplyPathAutoTiersAtOrBelowLevel(playerId, track, level, snapshot));
        snapshot = perkStore.GetSnapshot(playerId);
        return new PerkBranchSelectOutcome(true, null, allEvents, snapshot);
    }

    /// <summary>Unlocks path-auto tier perks after skill level-up (idempotent).</summary>
    public PerkReevaluationOutcome ReevaluateAfterLevelUp(string playerId, string skillIdRaw, int newLevel)
    {
        var skillId = skillIdRaw.Trim();
        if (skillId.Length == 0 || !catalog.TryGetTrack(skillId, out var track))
        {
            return new PerkReevaluationOutcome([], perkStore.GetSnapshot(playerId));
        }

        var snapshot = perkStore.GetSnapshot(playerId);
        var events = ApplyPathAutoTiersAtOrBelowLevel(playerId, track, newLevel, snapshot);
        return new PerkReevaluationOutcome(events, perkStore.GetSnapshot(playerId));
    }

    private PerkBranchSelectOutcome Deny(PerkStateSnapshot snapshot, string reason) =>
        new(false, reason, [], snapshot);

    private int GetSkillLevel(string playerId, string skillId)
    {
        var xpBySkill = xpStore.GetXpTotals(playerId);
        xpBySkill.TryGetValue(skillId, out var xp);
        return levelCurve.LevelFromTotalXp(xp);
    }

    private static MasteryTierRow? FindTier(MasteryTrackRow track, int tierIndex) =>
        track.Tiers.FirstOrDefault(t => t.TierIndex == tierIndex);

    /// <summary>True when every branch has no perks — player must pick a branch explicitly.</summary>
    internal static bool TierRequiresExplicitPick(MasteryTierRow tier) =>
        tier.Branches.All(static b => b.PerkIds.Count == 0);

    internal static bool TierIsPathAuto(MasteryTierRow tier) => !TierRequiresExplicitPick(tier);

    private static bool TierContainsBranch(MasteryTierRow tier, string branchId) =>
        tier.Branches.Any(b => string.Equals(b.BranchId, branchId, StringComparison.Ordinal));

    /// <summary>Branch id from the highest prior tier that required an explicit pick.</summary>
    internal static string? GetPathBranchId(PerkStateSnapshot snapshot, MasteryTrackRow track, MasteryTierRow tier)
    {
        MasteryTierRow? anchor = null;
        foreach (var prior in track.Tiers.OrderByDescending(static t => t.TierIndex))
        {
            if (prior.TierIndex >= tier.TierIndex)
            {
                continue;
            }

            if (TierRequiresExplicitPick(prior))
            {
                anchor = prior;
                break;
            }
        }

        if (anchor is null)
        {
            return null;
        }

        return snapshot.TryGetBranchPick(track.SkillId, anchor.TierIndex, out var bid) ? bid : null;
    }

    private List<PerkUnlockEvent> ApplyPathAutoTiersAtOrBelowLevel(
        string playerId,
        MasteryTrackRow track,
        int maxLevel,
        PerkStateSnapshot snapshot)
    {
        var allEvents = new List<PerkUnlockEvent>();
        foreach (var tier in track.Tiers.OrderBy(static t => t.TierIndex))
        {
            if (tier.RequiredLevel > maxLevel || !TierIsPathAuto(tier))
            {
                continue;
            }

            var pathBranchId = GetPathBranchId(snapshot, track, tier);
            if (pathBranchId is null)
            {
                continue;
            }

            var branch = tier.Branches.FirstOrDefault(b =>
                string.Equals(b.BranchId, pathBranchId, StringComparison.Ordinal));
            if (branch is null)
            {
                continue;
            }

            if (!TryUnlockPerks(
                    playerId,
                    track.SkillId,
                    tier.TierIndex,
                    branch.BranchId,
                    branch.PerkIds,
                    PerkUnlockSource.LevelUp,
                    snapshot,
                    out var batch))
            {
                continue;
            }

            allEvents.AddRange(batch);
            snapshot = perkStore.GetSnapshot(playerId);
        }

        return allEvents;
    }

    private bool TryUnlockPerks(
        string playerId,
        string skillId,
        int tierIndex,
        string branchId,
        IReadOnlyList<string> perkIds,
        PerkUnlockSource source,
        PerkStateSnapshot snapshot,
        out IReadOnlyList<PerkUnlockEvent> events)
    {
        events = [];
        if (perkIds.Count == 0)
        {
            return true;
        }

        var newIds = new List<string>();
        foreach (var perkId in perkIds)
        {
            if (!snapshot.UnlockedPerkIds.Contains(perkId))
            {
                newIds.Add(perkId);
            }
        }

        if (newIds.Count == 0)
        {
            return true;
        }

        if (!perkStore.TryAddUnlockedPerks(playerId, newIds))
        {
            return false;
        }

        events = newIds
            .Select(pid => new PerkUnlockEvent(playerId, pid, skillId, tierIndex, branchId, source))
            .ToList();

        // --- Telemetry hook site (NEO-49): future E9.M1 catalog event `perk_unlock` ---
        // TODO(E9.M1): catalog emit — once per entry in `events` (idempotent reevaluation produces none).
        // Planned payload fields: playerId, perkId, skillId, tierIndex, branchId, source (BranchPick | LevelUp).
        // No ingest or ILogger here (comments-only).

        return true;
    }
}
