using System.Text.Json;

namespace ScadaLink.CentralUI.Components.Shared;

/// <summary>
/// In-memory JSON Schema tree used by <see cref="SchemaBuilder"/>. The editor
/// mutates this graph directly; <see cref="SchemaBuilderModel"/> handles
/// parse / serialize round-tripping to the canonical JSON Schema text stored
/// in TemplateScript / SharedScript / ApiMethod columns.
/// </summary>
internal sealed class SchemaNode
{
    /// <summary>One of: <c>string · integer · number · boolean · object · array</c>.</summary>
    public string Type { get; set; } = "string";

    /// <summary>For <c>type=array</c>: the schema of the array's items.</summary>
    public SchemaNode? Items { get; set; }

    /// <summary>For <c>type=object</c>: ordered list of named properties.</summary>
    public List<SchemaProperty> Properties { get; } = new();
}

internal sealed class SchemaProperty
{
    /// <summary>Stable identity for Blazor <c>@key</c> across renames.</summary>
    public Guid Id { get; } = Guid.NewGuid();
    public string Name { get; set; } = string.Empty;
    public bool Required { get; set; } = true;
    public SchemaNode Schema { get; set; } = new();
}

internal static class SchemaBuilderModel
{
    public static readonly string[] PrimitiveTypes =
        { "string", "integer", "number", "boolean", "object", "array" };

    /// <summary>
    /// Parse a JSON Schema string into a <see cref="SchemaNode"/> tree.
    /// Returns the supplied <paramref name="fallback"/> when the input is
    /// empty or malformed. Also accepts the legacy flat-array parameter
    /// shape (<c>[{name,type,required,itemType?}]</c>) for safety during the
    /// transition window — translates it into an equivalent object schema.
    /// </summary>
    public static SchemaNode Parse(string? json, SchemaNode fallback)
    {
        if (string.IsNullOrWhiteSpace(json)) return fallback;
        try
        {
            using var doc = JsonDocument.Parse(json);
            return doc.RootElement.ValueKind switch
            {
                JsonValueKind.Object => ParseSchema(doc.RootElement),
                JsonValueKind.Array => ParseLegacyArray(doc.RootElement),
                _ => fallback,
            };
        }
        catch
        {
            return fallback;
        }
    }

    /// <summary>Default empty object schema (parameters mode default).</summary>
    public static SchemaNode NewObject() => new() { Type = "object" };

    /// <summary>Default scalar schema (return mode default).</summary>
    public static SchemaNode NewValue() => new() { Type = "string" };

    public static string Serialize(SchemaNode node)
    {
        using var stream = new System.IO.MemoryStream();
        using (var writer = new Utf8JsonWriter(stream))
        {
            WriteNode(writer, node);
        }
        return System.Text.Encoding.UTF8.GetString(stream.ToArray());
    }

    // ── Parse helpers ─────────────────────────────────────────────────────────

    private static SchemaNode ParseSchema(JsonElement el)
    {
        var node = new SchemaNode { Type = "string" };
        if (el.TryGetProperty("type", out var t) && t.ValueKind == JsonValueKind.String)
        {
            node.Type = NormalizeType(t.GetString());
        }

        if (node.Type == "array")
        {
            node.Items = el.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Object
                ? ParseSchema(items)
                : new SchemaNode { Type = "string" };
        }
        else if (node.Type == "object")
        {
            var requiredSet = new HashSet<string>(StringComparer.Ordinal);
            if (el.TryGetProperty("required", out var req) && req.ValueKind == JsonValueKind.Array)
            {
                foreach (var r in req.EnumerateArray())
                {
                    if (r.ValueKind == JsonValueKind.String)
                    {
                        var s = r.GetString();
                        if (!string.IsNullOrEmpty(s)) requiredSet.Add(s);
                    }
                }
            }

            if (el.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Object)
            {
                foreach (var prop in props.EnumerateObject())
                {
                    node.Properties.Add(new SchemaProperty
                    {
                        Name = prop.Name,
                        Required = requiredSet.Contains(prop.Name),
                        Schema = prop.Value.ValueKind == JsonValueKind.Object
                            ? ParseSchema(prop.Value)
                            : new SchemaNode { Type = "string" },
                    });
                }
            }
        }
        return node;
    }

    private static SchemaNode ParseLegacyArray(JsonElement arr)
    {
        var root = new SchemaNode { Type = "object" };
        foreach (var item in arr.EnumerateArray())
        {
            if (item.ValueKind != JsonValueKind.Object) continue;
            var name = item.TryGetProperty("name", out var n) ? n.GetString() : null;
            if (string.IsNullOrEmpty(name)) continue;

            var rawType = item.TryGetProperty("type", out var t) ? t.GetString() : "string";
            var required = !item.TryGetProperty("required", out var rq) || rq.ValueKind != JsonValueKind.False;
            var schema = new SchemaNode { Type = NormalizeType(rawType) };
            if (schema.Type == "array")
            {
                var inner = item.TryGetProperty("itemType", out var it) ? it.GetString() : "string";
                schema.Items = new SchemaNode { Type = NormalizeType(inner) };
            }
            root.Properties.Add(new SchemaProperty
            {
                Name = name,
                Required = required,
                Schema = schema,
            });
        }
        return root;
    }

    private static string NormalizeType(string? raw) => raw?.ToLowerInvariant() switch
    {
        "boolean" or "bool" => "boolean",
        "integer" or "int" or "int32" or "int64" => "integer",
        "number" or "float" or "double" or "decimal" => "number",
        "string" or "datetime" => "string",
        "object" => "object",
        "array" or "list" => "array",
        _ => "string",
    };

    // ── Serialize helpers ─────────────────────────────────────────────────────

    private static void WriteNode(Utf8JsonWriter w, SchemaNode node)
    {
        w.WriteStartObject();
        w.WriteString("type", node.Type);

        if (node.Type == "array")
        {
            w.WritePropertyName("items");
            WriteNode(w, node.Items ?? new SchemaNode { Type = "string" });
        }
        else if (node.Type == "object")
        {
            w.WritePropertyName("properties");
            w.WriteStartObject();
            foreach (var p in node.Properties.Where(p => !string.IsNullOrWhiteSpace(p.Name)))
            {
                w.WritePropertyName(p.Name);
                WriteNode(w, p.Schema);
            }
            w.WriteEndObject();

            var required = node.Properties
                .Where(p => p.Required && !string.IsNullOrWhiteSpace(p.Name))
                .Select(p => p.Name)
                .ToArray();
            if (required.Length > 0)
            {
                w.WritePropertyName("required");
                w.WriteStartArray();
                foreach (var r in required) w.WriteStringValue(r);
                w.WriteEndArray();
            }
        }

        w.WriteEndObject();
    }
}
