using System.Text;
using System.Text.Json.Nodes;
using Json.Schema;
using Microsoft.Extensions.Logging;

namespace NeonSprawl.Server.Game.Mastery;

/// <summary>Loads and validates <c>content/mastery/*_mastery.json</c> using the same rules as <c>scripts/validate_content.py</c> (NEO-46).</summary>
public static class MasteryCatalogLoader
{
    /// <summary>Loads catalogs from disk or throws <see cref="InvalidOperationException"/> with actionable messages.</summary>
    public static MasteryCatalog Load(
        string masteryDirectory,
        string schemaPath,
        IReadOnlySet<string> knownSkillIds,
        ILogger logger)
    {
        masteryDirectory = Path.GetFullPath(masteryDirectory);
        schemaPath = Path.GetFullPath(schemaPath);

        var errors = new List<string>();

        if (!File.Exists(schemaPath))
            errors.Add($"error: missing schema file {schemaPath}");

        if (!Directory.Exists(masteryDirectory))
            errors.Add($"error: missing directory {masteryDirectory}");

        if (errors.Count > 0)
            ThrowIfAny(errors);

        var jsonFiles = Directory.GetFiles(masteryDirectory, "*_mastery.json", SearchOption.TopDirectoryOnly)
            .OrderBy(p => p, StringComparer.Ordinal)
            .ToArray();
        if (jsonFiles.Length == 0)
            errors.Add($"error: no *_mastery.json files under {masteryDirectory}");

        if (errors.Count > 0)
            ThrowIfAny(errors);

        var schema = JsonSchema.FromText(File.ReadAllText(schemaPath));
        var evalOptions = new EvaluationOptions { OutputFormat = OutputFormat.List };

        var globalPerkIds = new Dictionary<string, string>(StringComparer.Ordinal);
        var globalReferencedPerkIds = new Dictionary<string, string>(StringComparer.Ordinal);
        var allTrackSkillIds = new List<string>();
        var perksById = new Dictionary<string, PerkDefRow>(StringComparer.Ordinal);
        var tracksBySkillId = new Dictionary<string, MasteryTrackRow>(StringComparer.Ordinal);

        foreach (var path in jsonFiles)
        {
            JsonNode? root;
            try
            {
                root = JsonNode.Parse(File.ReadAllText(path));
            }
            catch (System.Text.Json.JsonException ex)
            {
                errors.Add($"error: {path}: invalid JSON: {ex.Message}");
                continue;
            }

            if (root is not JsonObject rootObj)
            {
                errors.Add($"error: {path}: expected JSON object at root");
                continue;
            }

            var eval = schema.Evaluate(rootObj, evalOptions);
            var schemaMsgs = CollectSchemaMessages(eval, path).OrderBy(m => m, StringComparer.Ordinal).ToList();
            if (!eval.IsValid)
            {
                if (schemaMsgs.Count == 0)
                    schemaMsgs.Add($"error: {path} (root): schema validation failed");

                errors.AddRange(schemaMsgs);
                continue;
            }

            var perksNode = rootObj["perks"];
            var tracksNode = rootObj["tracks"];
            if (perksNode is not JsonObject perksObj || tracksNode is not JsonArray tracksArray)
            {
                errors.Add($"error: {path}: expected top-level 'perks' object and 'tracks' array");
                continue;
            }

            var referencedPerkIdsInFile = new HashSet<string>(StringComparer.Ordinal);

            foreach (var (perkKey, perkNode) in perksObj)
            {
                if (perkNode is not JsonObject perkObj)
                {
                    errors.Add($"error: {path}: perks[{perkKey}] must be an object");
                    continue;
                }

                var pid = (perkObj["id"] as JsonValue)?.GetValue<string>();
                if (pid is null)
                    continue;

                if (perkKey != pid)
                {
                    errors.Add($"error: {path}: perks map key '{perkKey}' must match PerkDef.id '{pid}'");
                }

                if (globalPerkIds.TryGetValue(pid, out var prevPath))
                {
                    errors.Add($"error: duplicate perk id '{pid}' in {prevPath} and {path}");
                }
                else
                {
                    globalPerkIds[pid] = path;
                    var displayName = (perkObj["displayName"] as JsonValue)?.GetValue<string>() ?? string.Empty;
                    string? effectKind = null;
                    if (perkObj["effectKind"] is JsonValue ek)
                        effectKind = ek.GetValue<string>();
                    perksById[pid] = new PerkDefRow(pid, displayName, effectKind);
                }
            }

            for (var ti = 0; ti < tracksArray.Count; ti++)
            {
                if (tracksArray[ti] is not JsonObject trackObj)
                {
                    errors.Add($"error: {path}: tracks[{ti}] must be an object");
                    continue;
                }

                var skillId = (trackObj["skillId"] as JsonValue)?.GetValue<string>();
                if (skillId is not null)
                {
                    allTrackSkillIds.Add(skillId);
                    if (!knownSkillIds.Contains(skillId))
                    {
                        var known = string.Join(", ", knownSkillIds.Order(StringComparer.Ordinal).Select(s => "'" + s + "'"));
                        errors.Add(
                            $"error: {path}: tracks[{ti}].skillId '{skillId}' is not a known SkillDef id (known: [{known}])");
                    }

                    if (tracksBySkillId.ContainsKey(skillId))
                    {
                        errors.Add($"error: duplicate mastery track for skillId '{skillId}' in catalog");
                    }
                }

                var tiersNode = trackObj["tiers"];
                if (tiersNode is not JsonArray tiersArray)
                    continue;

                var prevRequiredLevel = 0;
                HashSet<string>? prevTierBranchIds = null;
                var tierIndexValues = new List<int>();

                var parsedTiers = new List<MasteryTierRow>();

                for (var tierI = 0; tierI < tiersArray.Count; tierI++)
                {
                    if (tiersArray[tierI] is not JsonObject tierObj)
                        continue;

                    var tierIndex = (tierObj["tierIndex"] as JsonValue)?.GetValue<int>();
                    if (tierIndex is int idx)
                        tierIndexValues.Add(idx);

                    var requiredLevel = (tierObj["requiredLevel"] as JsonValue)?.GetValue<int>();
                    if (requiredLevel is int rl)
                    {
                        if (rl <= prevRequiredLevel)
                        {
                            errors.Add(
                                $"error: {path}: tracks[{ti}].tiers[{tierI}].requiredLevel must be strictly greater than previous tier ({prevRequiredLevel})");
                        }

                        prevRequiredLevel = rl;
                    }

                    var branchesNode = tierObj["branches"];
                    if (branchesNode is not JsonArray branchesArray)
                        continue;

                    var tierBranchIds = new List<string>();
                    var parsedBranches = new List<MasteryBranchRow>();

                    for (var bi = 0; bi < branchesArray.Count; bi++)
                    {
                        if (branchesArray[bi] is not JsonObject branchObj)
                            continue;

                        var branchId = (branchObj["branchId"] as JsonValue)?.GetValue<string>();
                        if (branchId is not null)
                        {
                            if (tierBranchIds.Contains(branchId, StringComparer.Ordinal))
                            {
                                errors.Add(
                                    $"error: {path}: tracks[{ti}].tiers[{tierI}] duplicate branchId '{branchId}'");
                            }

                            tierBranchIds.Add(branchId);
                        }

                        string? branchDisplayName = null;
                        if (branchObj["displayName"] is JsonValue dn)
                            branchDisplayName = dn.GetValue<string>();

                        var perkIdsList = new List<string>();
                        if (branchObj["perkIds"] is JsonArray perkIdsArray)
                        {
                            foreach (var perkIdNode in perkIdsArray)
                            {
                                if (perkIdNode is not JsonValue pv)
                                    continue;

                                var perkId = pv.GetValue<string>();
                                if (!perksObj.ContainsKey(perkId))
                                {
                                    errors.Add(
                                        $"error: {path}: tracks[{ti}].tiers[{tierI}].branches[{bi}] references unknown perkId '{perkId}'");
                                }

                                if (globalReferencedPerkIds.TryGetValue(perkId, out var prevRef))
                                {
                                    errors.Add($"error: duplicate perk id reference '{perkId}' in {prevRef} and {path}");
                                }
                                else
                                {
                                    globalReferencedPerkIds[perkId] =
                                        $"{path} tracks[{ti}].tiers[{tierI}].branches[{bi}]";
                                }

                                referencedPerkIdsInFile.Add(perkId);
                                perkIdsList.Add(perkId);
                            }
                        }

                        if (branchId is not null)
                            parsedBranches.Add(new MasteryBranchRow(branchId, branchDisplayName, perkIdsList));
                    }

                    var tierBranchSet = tierBranchIds.ToHashSet(StringComparer.Ordinal);
                    if (prevTierBranchIds is not null && !tierBranchSet.SetEquals(prevTierBranchIds))
                    {
                        var current = string.Join(", ", tierBranchSet.Order(StringComparer.Ordinal).Select(s => "'" + s + "'"));
                        var previous = string.Join(", ", prevTierBranchIds.Order(StringComparer.Ordinal).Select(s => "'" + s + "'"));
                        errors.Add(
                            $"error: {path}: tracks[{ti}].tiers[{tierI}] branchId set [{current}] must match previous tier [{previous}]");
                    }

                    if (tierBranchIds.Count > 0)
                        prevTierBranchIds = tierBranchSet;

                    if (tierIndex is int tIdx && requiredLevel is int req)
                        parsedTiers.Add(new MasteryTierRow(tIdx, req, parsedBranches));
                }

                var tierIndexError = TryGetTierIndexGateError(path, ti, tierIndexValues);
                if (tierIndexError is not null)
                    errors.Add(tierIndexError);

                if (skillId is not null && parsedTiers.Count > 0)
                    tracksBySkillId[skillId] = new MasteryTrackRow(skillId, parsedTiers);
            }

            foreach (var perkKey in perksObj.Select(p => p.Key))
            {
                if (!referencedPerkIdsInFile.Contains(perkKey))
                {
                    errors.Add($"error: {path}: perks['{perkKey}'] is not referenced by any branch perkIds");
                }
            }
        }

        ThrowIfAny(errors);

        var slice4 = PrototypeSlice4MasteryCatalogRules.TryGetSlice4GateError(allTrackSkillIds);
        if (slice4 is not null)
        {
            errors.Add(slice4);
            ThrowIfAny(errors);
        }

        logger.LogInformation(
            "Loaded mastery catalog from {MasteryDirectory}: {TrackCount} track(s), {PerkCount} perk(s) across {CatalogFileCount} JSON catalog file(s).",
            masteryDirectory,
            tracksBySkillId.Count,
            perksById.Count,
            jsonFiles.Length);

        return new MasteryCatalog(masteryDirectory, tracksBySkillId, perksById, jsonFiles.Length);
    }

    /// <summary>
    /// Server-only gate: unique tierIndex values must be exactly 1..N (NEO-46 kickoff).
    /// Stable <c>tierIndex</c> keys <see cref="PerkUnlockEngine"/> branch picks and path-auto tier evaluation.
    /// </summary>
    internal static string? TryGetTierIndexGateError(string filePath, int trackIndex, IReadOnlyList<int> tierIndexValues)
    {
        if (tierIndexValues.Count == 0)
            return null;

        var seen = new HashSet<int>();
        foreach (var value in tierIndexValues)
        {
            if (!seen.Add(value))
            {
                return $"error: {filePath}: tracks[{trackIndex}] duplicate tierIndex {value}";
            }
        }

        var expected = Enumerable.Range(1, tierIndexValues.Count).ToHashSet();
        if (!seen.SetEquals(expected))
        {
            var sorted = string.Join(", ", seen.Order().Select(v => v.ToString()));
            return
                $"error: {filePath}: tracks[{trackIndex}] tierIndex values must be unique and sequential 1..{tierIndexValues.Count}, got [{sorted}]";
        }

        return null;
    }

    private static List<string> CollectSchemaMessages(EvaluationResults eval, string filePath)
    {
        var sink = new List<string>();
        AppendSchemaMessages(eval, filePath, sink);
        return sink;
    }

    private static void AppendSchemaMessages(EvaluationResults r, string filePath, List<string> sink)
    {
        if (r.HasDetails)
        {
            foreach (var d in r.Details!)
                AppendSchemaMessages(d, filePath, sink);
        }

        if (!r.HasErrors)
            return;

        foreach (var kv in r.Errors!)
        {
            var loc = r.InstanceLocation?.ToString();
            if (string.IsNullOrEmpty(loc) || loc == "#")
                loc = "(root)";

            sink.Add($"error: {filePath} {loc}: {kv.Key} — {kv.Value}");
        }
    }

    private static void ThrowIfAny(List<string> errors)
    {
        if (errors.Count == 0)
            return;

        var sb = new StringBuilder();
        sb.AppendLine("Mastery catalog validation failed:");
        foreach (var e in errors.OrderBy(x => x, StringComparer.Ordinal))
            sb.AppendLine(e);

        throw new InvalidOperationException(sb.ToString().TrimEnd());
    }
}
