using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Json.Schema;

namespace NeonSprawl.Server.Game.Skills;

/// <summary>Level resolver backed by startup-loaded threshold rows from content files (NEO-39).</summary>
public sealed class ContentBackedSkillLevelCurve : ISkillLevelCurve
{
    private readonly IReadOnlyList<LevelThresholdRow> levels;

    internal ContentBackedSkillLevelCurve(IReadOnlyList<LevelThresholdRow> levels)
    {
        if (levels.Count == 0)
            throw new InvalidOperationException("Level curve must contain at least one level row.");

        this.levels = levels;
    }

    public int LevelFromTotalXp(int totalXp)
    {
        var xp = Math.Max(0, totalXp);
        var level = 1;
        foreach (var row in levels)
        {
            if (xp < row.RequiredXp)
                break;
            level = row.Level;
        }

        return level;
    }

    internal static ContentBackedSkillLevelCurve Load(
        string skillsDirectory,
        string curveFilePath,
        string schemaPath,
        ILogger logger)
    {
        var errors = new List<string>();
        var fullSkillsDirectory = Path.GetFullPath(skillsDirectory);
        var fullCurvePath = Path.GetFullPath(curveFilePath);
        var fullSchemaPath = Path.GetFullPath(schemaPath);

        if (!Directory.Exists(fullSkillsDirectory))
            errors.Add($"error: missing directory {fullSkillsDirectory}");
        if (!File.Exists(fullCurvePath))
            errors.Add($"error: missing level curve file {fullCurvePath}");
        if (!File.Exists(fullSchemaPath))
            errors.Add($"error: missing level curve schema {fullSchemaPath}");
        ThrowIfAny(errors);

        JsonDocument doc;
        JsonNode? rootNode;
        try
        {
            var json = File.ReadAllText(fullCurvePath);
            doc = JsonDocument.Parse(json);
            rootNode = JsonNode.Parse(json);
        }
        catch (JsonException ex)
        {
            throw new InvalidOperationException($"Level curve file is invalid JSON: {fullCurvePath}: {ex.Message}");
        }

        using (doc)
        {
            var schema = JsonSchema.FromText(File.ReadAllText(fullSchemaPath));
            if (rootNode is null)
            {
                errors.Add($"error: {fullCurvePath}: failed to parse JSON document");
            }
            else
            {
                var eval = schema.Evaluate(rootNode, new EvaluationOptions { OutputFormat = OutputFormat.List });
                errors.AddRange(CollectSchemaMessages(eval, fullCurvePath));
            }

            if (doc.RootElement.ValueKind != JsonValueKind.Object)
                errors.Add($"error: {fullCurvePath}: expected top-level object");

            if (!doc.RootElement.TryGetProperty("schemaVersion", out var schemaVersion) ||
                schemaVersion.ValueKind != JsonValueKind.Number ||
                !schemaVersion.TryGetInt32(out var sv) ||
                sv != 1)
            {
                errors.Add($"error: {fullCurvePath}: expected schemaVersion = 1");
            }

            if (!doc.RootElement.TryGetProperty("levels", out var levelsNode) ||
                levelsNode.ValueKind != JsonValueKind.Array)
            {
                errors.Add($"error: {fullCurvePath}: expected top-level 'levels' array");
                ThrowIfAny(errors);
            }

            var parsedRows = new List<LevelThresholdRow>();
            var rowIndex = 0;
            var prevLevel = 0;
            var prevXp = -1;
            foreach (var row in levelsNode.EnumerateArray())
            {
                if (row.ValueKind != JsonValueKind.Object)
                {
                    errors.Add($"error: {fullCurvePath}: levels[{rowIndex}] must be an object");
                    rowIndex++;
                    continue;
                }

                if (!row.TryGetProperty("level", out var levelNode) ||
                    levelNode.ValueKind != JsonValueKind.Number ||
                    !levelNode.TryGetInt32(out var level) ||
                    level < 1)
                {
                    errors.Add($"error: {fullCurvePath}: levels[{rowIndex}].level must be an integer >= 1");
                    rowIndex++;
                    continue;
                }

                if (!row.TryGetProperty("requiredXp", out var xpNode) ||
                    xpNode.ValueKind != JsonValueKind.Number ||
                    !xpNode.TryGetInt32(out var requiredXp) ||
                    requiredXp < 0)
                {
                    errors.Add($"error: {fullCurvePath}: levels[{rowIndex}].requiredXp must be an integer >= 0");
                    rowIndex++;
                    continue;
                }

                if (row.TryGetProperty("level", out _) &&
                    row.TryGetProperty("requiredXp", out _))
                {
                    if (level <= prevLevel)
                        errors.Add($"error: {fullCurvePath}: levels[{rowIndex}].level must be strictly increasing");
                    if (requiredXp <= prevXp)
                        errors.Add($"error: {fullCurvePath}: levels[{rowIndex}].requiredXp must be strictly increasing");
                    prevLevel = level;
                    prevXp = requiredXp;
                    parsedRows.Add(new LevelThresholdRow(level, requiredXp));
                }

                rowIndex++;
            }

            if (parsedRows.Count == 0)
                errors.Add($"error: {fullCurvePath}: levels must contain at least one valid row");
            else if (parsedRows[0].Level != 1 || parsedRows[0].RequiredXp != 0)
                errors.Add($"error: {fullCurvePath}: first level row must be {{\"level\":1,\"requiredXp\":0}}");

            ThrowIfAny(errors);

            logger.LogInformation(
                "Loaded level curve from {CurvePath} with {LevelCount} thresholds.",
                fullCurvePath,
                parsedRows.Count);
            return new ContentBackedSkillLevelCurve(parsedRows);
        }
    }

    internal sealed record LevelThresholdRow(int Level, int RequiredXp);

    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("Level curve validation failed:");
        foreach (var e in errors.OrderBy(static x => x, StringComparer.Ordinal))
            sb.AppendLine(e);
        throw new InvalidOperationException(sb.ToString().TrimEnd());
    }
}
