using System.Text.Json.Serialization;
using System.Text.Json.Nodes;
using OneOf;

namespace AsyncApiRenderer.Data;

public class JsonSchemaNode
{
    public List<JsonSchemaNode>? OneOf { get; set; }

    public List<JsonSchemaNode>? AllOf { get; set; }

    [JsonPropertyName("$ref")]
    public string? Ref { get; set; }

    public Dictionary<string, JsonSchemaNode>? Properties { get; set; }

    public JsonSchemaNode? AdditionalProperties { get; set; }

    public List<string>? Required { get; set; }

    public List<string>? Enum { get; set; }

    [JsonConverter(typeof(JsonSchemaTypeConverter))]
    public HashSet<JsonSchemaType>? Type { get; set; }

    public string? Format { get; set; } // password, date-time, 

    public string? Description { get; set; }

    [JsonConverter(typeof(OneOfConverter<JsonSchemaNode, List<JsonSchemaNode>>))]
    public OneOf<JsonSchemaNode, List<JsonSchemaNode>>? Items { get; set; }

    public JsonNode? Default { get; set; }

    public List<JsonNode>? Examples { get; set; }

    [JsonIgnore]
    public bool IsNullable => Type?.Any(x => x == JsonSchemaType.Null) ?? false;

    public JsonSchemaNode Deref(Dictionary<string, JsonSchemaNode> schemas)
    {
        if (OneOf is not null)
            for (int i = 0; i < OneOf.Count; ++i)
                OneOf[i] = OneOf[i].Deref(schemas);
        if (AllOf is not null)
            for (int i = 0; i < AllOf.Count; ++i)
                AllOf[i] = AllOf[i].Deref(schemas);
        if (Properties is not null)
        {
            var props = new Dictionary<string, JsonSchemaNode>();
            foreach (var (key, value) in Properties)
                props[key] = value.Deref(schemas);
            Properties = props;
        }
        if (AdditionalProperties is not null)
            AdditionalProperties = AdditionalProperties.Deref(schemas);
        if (Items is not null)
            Items = Items.Value.Match<OneOf<JsonSchemaNode, List<JsonSchemaNode>>>(
                v => v.Deref(schemas),
                v => v.Select(x => x.Deref(schemas)).ToList()
            );
        if (Ref is null)
            return this;
        const string prefix = "#/components/schemas/";
        if (Ref.StartsWith(prefix) && 
            schemas.TryGetValue(Ref.Substring(prefix.Length), out JsonSchemaNode? node))
            return node;
        else return this;
    }

    public void Merge(JsonSchemaNode other)
    {
        if (other.OneOf is not null)
            (OneOf ??= new List<JsonSchemaNode>()).AddRange(other.OneOf);
        if (other.AllOf is not null)
            (AllOf ??= new List<JsonSchemaNode>()).AddRange(other.AllOf);
        if (other.Properties is not null)
        {
            Properties ??= new Dictionary<string, JsonSchemaNode>();
            foreach (var (key, value) in other.Properties)
                if (!Properties.ContainsKey(key))
                    Properties.Add(key, value);
        }
        if (other.AdditionalProperties is not null)
        {
            AdditionalProperties ??= new JsonSchemaNode();
            AdditionalProperties.Merge(other.AdditionalProperties);
        }
        Required = MergeList(Required, other.Required);
        Enum = MergeList(Enum, other.Enum);
        if (other.Type is not null)
        {
            Type ??= new HashSet<JsonSchemaType>();
            foreach (var type in other.Type)
                Type.Add(type);
        }
        Format ??= other.Format;
        Description ??= other.Description;
        Items ??= other.Items;
        Default ??= other.Default;
        if (other.Examples is not null)
        {
            Examples ??= new List<JsonNode>();
            Examples.AddRange(other.Examples);
        }
    }

    private List<T>? MergeList<T>(List<T>? list, IEnumerable<T>? other)
    {
        if (other is null)
            return list;
        list ??= new List<T>();
        foreach (var item in other)
            if (!list.Contains(item))
                list.Add(item);
        return list;
    }

    public JsonSchemaNode Collapse()
    {
        if (OneOf is not null)
            for (int i = 0; i < OneOf.Count; ++i)
                OneOf[i] = OneOf[i].Collapse();
        if (AllOf is not null)
            for (int i = 0; i < AllOf.Count; ++i)
                AllOf[i] = AllOf[i].Collapse();
        if (Properties is not null)
        {
            var props = new Dictionary<string, JsonSchemaNode>();
            foreach (var (key, value) in Properties)
                props[key] = value.Collapse();
            Properties = props;
        }
        if (AdditionalProperties is not null)
            AdditionalProperties = AdditionalProperties.Collapse();
        if (Items is not null)
            Items = Items.Value.Match<OneOf<JsonSchemaNode, List<JsonSchemaNode>>>(
                v => v.Collapse(),
                v => v.Select(x => x.Collapse()).ToList()
            );

        bool CollapseNullCheck()
        {
            return OneOf is not null
                && AllOf is null
                && Properties is null
                && AdditionalProperties is null
                && Items is null
                && Required is null
                && Enum is null
                && Type is null
                && Format is null
                && OneOf.Count == 2
                && (
                    (OneOf[0].IsNullable && !OneOf[1].IsNullable) ||
                    (!OneOf[0].IsNullable && OneOf[1].IsNullable)
                );
        }

        if (CollapseNullCheck())
        {
            var root = new JsonSchemaNode
            {
                Type = new HashSet<JsonSchemaType>()
                {
                    JsonSchemaType.Null,
                },
                Description = Description,
                Default = Default,
                Examples = Examples,
            };
            foreach (var item in OneOf!)
                root.Merge(item);
            return root.Collapse();
        }

        if (AllOf is not null && AllOf.Count > 0)
        {
            var list = AllOf;
            AllOf = null;
            foreach (var item in list)
                Merge(item);
            return this.Collapse();
        }

        return this;
    }

    public IEnumerable<Rule> GetRules()
        => GetRules("", true, false, false);

    public IEnumerable<Rule> GetRules(string prefix, bool isRequired, bool isChoice, bool isAdditional)
    {
        if (prefix.Length > 0)
            yield return new Rule
            {
                Path = prefix,
                IsRequired = isRequired,
                IsNullable = IsNullable,
                IsChoice = isChoice,
                isAdditional = isAdditional,
                Types = Type,
                Format = Format,
                Enum = Enum,
                Description = Description,
            };
        if (OneOf is not null)
        {
            for (int i = 0; i < OneOf.Count; ++i)
                foreach (var item in OneOf[i].GetRules($"{prefix}.<{i}>", false, true, false))
                    yield return item;
        }
        if (Properties is not null)
        {
            foreach (var (key, value) in Properties)
            {
                var req = Required is null ? false : Required.Contains(key);
                foreach (var item in value.GetRules($"{prefix}.{key}", req, false, false))
                    yield return item;
            }
        }
        if (AdditionalProperties is not null)
        {
            foreach (var item in AdditionalProperties.GetRules($"{prefix}.*", false, false, true))
                yield return item;
        }
        if (Items is not null)
        {
            if (Items.Value.TryPickT0(out JsonSchemaNode node, out List<JsonSchemaNode> list))
            {
                foreach (var item in node.GetRules($"{prefix}[]", true, false, false))
                    yield return item;
            }
            else
            {
                for (int i = 0; i < list.Count; ++i)
                    foreach (var item in list[i].GetRules($"{prefix}[{i}]", true, false, false))
                        yield return item;
            }
        }
    }

    public IEnumerable<JsonExampleNode> GetExamples(string? propertyName = null)
    {
        if (Examples is not null)
        {
            foreach (var node in Examples)
                yield return new JsonExampleNode.JsonValue { Node = node };
            yield break;
        }

        if (Default is not null)
        {
            yield return new JsonExampleNode.JsonValue
            {
                Node = Default
            };
            yield break;
        }

        if (Enum is not null)
        {
            foreach (var val in Enum)
                yield return new JsonExampleNode.JsonValue
                {
                    Node = JsonValue.Create(val)
                };
            yield break;
        }

        if (Items is not null)
        {
            if (Items.Value.TryPickT0(out JsonSchemaNode node, out List<JsonSchemaNode> list))
            {
                var array = new JsonExampleNode.JsonArray();
                array.Nodes.AddRange(node.GetExamples());
                yield return array;
                yield break;
            }
            else
            {
                var result = new List<JsonExampleNode.JsonArray>
                {
                    new JsonExampleNode.JsonArray(),
                };
                var next = new List<JsonExampleNode.JsonArray>();
                foreach (var entry in list)
                {
                    next.Clear();
                    foreach (var ex in entry.GetExamples())
                    {
                        foreach (var res in result)
                        {
                            var copy = (JsonExampleNode.JsonArray)res.Clone();
                            copy.Nodes.Add(ex);
                            next.Add(copy);
                        }
                    }
                    (next, result) = (result, next);
                }
                foreach (var item in result)
                    yield return item;
                yield break;
            }
        }

        if (OneOf is not null)
        {
            foreach (var item in OneOf)
                foreach (var node in item.GetExamples())
                    yield return node;
            yield break;
        }

        if (Type is not null)
        {
            foreach (var type in Type)
            {
                switch (type)
                {
                    case JsonSchemaType.Null:
                        yield return new JsonExampleNode.JsonValue { Node = null };
                        break;
                    case JsonSchemaType.Boolean:
                        yield return new JsonExampleNode.JsonValue { Node = JsonValue.Create(false) };
                        break;
                    case JsonSchemaType.Integer:
                        yield return new JsonExampleNode.JsonValue { Node = JsonValue.Create(42) };
                        break;
                    case JsonSchemaType.Number:
                        yield return new JsonExampleNode.JsonValue { Node = JsonValue.Create(3.14) };
                        break;
                    case JsonSchemaType.String:
                        yield return new JsonExampleNode.JsonValue
                        {
                            Node = JsonValue.Create(Format switch
                            {
                                null => propertyName switch
                                {
                                    "id" => "#id-0123456789abcdef",
                                    "mime" => "text/plain",
                                    "database" => "db-main",
                                    _ => "abc",
                                },
                                "password" => "pa$$w0rd",
                                "date-time" => "2001-01-01T15:17:19.001",
                                _ => Format
                            })
                        };
                        break;
                }
                    
            }
        }

        const string additionalKey = "*foo-bar-baz*";

        if (Properties != null)
        {
            var result = new List<JsonExampleNode.JsonObject>
            {
                new JsonExampleNode.JsonObject()
            };
            var next = new List<JsonExampleNode.JsonObject>();
            foreach (var (key, node) in Properties)
            {
                next.Clear();
                if (Required is null || !Required.Contains(key))
                    continue;
                foreach (var ex in node.GetExamples(key))
                    foreach (var res in result)
                    {
                        var nextResult = (JsonExampleNode.JsonObject)res.Clone();
                        nextResult.Nodes.Add(key, ex);
                        next.Add(nextResult);
                    }
                (result, next) = (next, result);
            }
            if (AdditionalProperties is not null)
            {
                foreach (var ex in AdditionalProperties.GetExamples(additionalKey))
                    foreach (var res in result)
                    {
                        var key = additionalKey;
                        var inc = 1;
                        while (res.Nodes.ContainsKey(key))
                            key = $"{additionalKey}-{++inc}";
                        res.Nodes.Add(additionalKey, ex);
                    }
            }
            foreach (var res in result)
                yield return res;
        }

        if (Properties is null && AdditionalProperties is not null)
        {
            var obj = new JsonExampleNode.JsonObject();
            foreach (var ex in AdditionalProperties.GetExamples(additionalKey))
            {
                var key = additionalKey;
                var inc = 1;
                while (obj.Nodes.ContainsKey(key))
                    key = $"{additionalKey}-{++inc}";
                obj.Nodes.Add(key, ex);
            }
            yield return obj;
        }
    }
}
