{% set type_name = type.name | to_upper_camel_case -%}
{% set enum_type_name %}{{ type_name }}Config{% endset -%}
{% set discriminator_field_name = type.discriminator_field | to_upper_camel_case -%}
{% set content_field_name = type.content_field | to_upper_camel_case -%}
// this file is @generated
using System.Runtime.Serialization;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Svix.Models {
    {%- if type.description is defined %}
    /// <summary>
    {{ type.description | to_doc_comment(style="csharp") | indent(4) }}
    /// <summary>
    {% endif %}
    [JsonConverter(typeof({{ type_name }}Converter))]
    public class {{ type_name }}
    {
        {% include "types/struct_fields.cs.jinja" %}
        [JsonIgnore]
        public required {{ enum_type_name }} {{ content_field_name }} { get; set; }

        [JsonProperty("{{ type.discriminator_field }}")]
        private string {{ discriminator_field_name }} => Config.GetDiscriminator();

        [JsonProperty("{{ type.content_field }}")]
        private object real{{ content_field_name }} { get => Config.GetContent(); set => Config.SetContent(value); }


        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();

            sb.Append("class {{ type_name }} {\n");
            {% for f in type.fields -%}
            sb.Append("  {{ f.name | to_upper_camel_case }}: ").Append({{ f.name | to_upper_camel_case }}).Append('\n');
            {% endfor -%}
            sb.Append("  {{ content_field_name }}: ").Append({{ content_field_name }}).Append('\n');
            sb.Append("}\n");
            return sb.ToString();
        }
    }

    public class {{ enum_type_name }}
    {
        private object _value;
        private readonly {{ content_field_name }}Type _type;

        internal string GetDiscriminator()
        {
            var memberInfo = typeof({{ content_field_name }}Type).GetMember(_type.ToString());
            var enumMember = memberInfo[0].GetCustomAttributes(false)
                .OfType<EnumMemberAttribute>()
                .FirstOrDefault();
            return enumMember?.Value ?? _type.ToString().ToLower();
        }

        internal void SetContent(object value)
        {
            _value = value;
        }

        public object GetContent()
        {
            return _value;
        }

        private {{ enum_type_name }}(object value, {{ content_field_name }}Type type)
        {
            _value = value;
            _type = type;
        }

        {% for v in type.variants -%}
        public static {{ enum_type_name }} {{ v.name | to_upper_camel_case }}(
            {%- if v.schema_ref is defined -%}
                {{ v.schema_ref | to_upper_camel_case }} {{ v.schema_ref | to_lower_camel_case }}
            {%- endif -%}
        ) =>
            new(
                {% if v.schema_ref is defined -%}
                {{ v.schema_ref | to_lower_camel_case }},
                {% else -%}
                new Dictionary<string, string>(),
                {% endif -%}
                {{ content_field_name }}Type.{{ v.name | to_upper_camel_case }}
            );
        {% endfor -%}

        private enum {{ content_field_name }}Type
        {
            {% for v in type.variants -%}
            [EnumMember(Value = "{{ v.name }}")]
            {{ v.name | to_upper_camel_case }},
            {% endfor -%}
        }

        public TResult Match<TResult>(
            {% set func_args -%}
                {%- for v in type.variants -%}
                    {%- if v.schema_ref is defined -%}
                    Func<{{ v.schema_ref | to_upper_camel_case }},TResult> on{{ v.name | to_upper_camel_case }},
                    {%- else -%}
                    Func<TResult> on{{ v.name | to_upper_camel_case }},
                    {%- endif -%}
                {%- endfor -%}
            {% endset -%}
            {{ func_args | strip_trailing_comma }}
        )
        {
            return _type switch
            {
                {% for v in type.variants -%}
                    {{ content_field_name }}Type.{{ v.name | to_upper_camel_case }} => on{{ v.name | to_upper_camel_case }}(
                        {%- if v.schema_ref is defined -%}
                        ({{ v.schema_ref | to_upper_camel_case }})_value
                        {%- endif -%}
                    ),
                {% endfor -%}
                // unreachable
                _ => throw new InvalidOperationException("Unknown config type")
            };

        }
        public void Switch(
            {% set func_args -%}
                {%- for v in type.variants -%}
                    {%- if v.schema_ref is defined -%}
                    Action<{{ v.schema_ref | to_upper_camel_case }}> on{{ v.name | to_upper_camel_case }},
                    {%- else -%}
                    Action on{{ v.name | to_upper_camel_case }},
                    {%- endif -%}
                {%- endfor -%}
            {% endset -%}
            {{ func_args | strip_trailing_comma }}
        )
        {
            switch (_type)
            {
                {% for v in type.variants -%}
                case {{ content_field_name }}Type.{{ v.name | to_upper_camel_case }}:
                    on{{ v.name | to_upper_camel_case }}(
                        {%- if v.schema_ref is defined -%}
                        ({{ v.schema_ref | to_upper_camel_case }})_value
                        {%- endif -%}
                    );
                    break;
                {% endfor -%}
                default:
                    // unreachable
                    throw new InvalidOperationException("Unknown config type");
            }

        }

    }

    internal class {{ type_name }}Surrogate
    {
        {% include "types/struct_fields.cs.jinja" %}
        [JsonProperty("{{ type.discriminator_field }}", Required = Required.Always)]
        public required string {{ discriminator_field_name }} { get; set; }

        [JsonProperty("{{ type.content_field }}", Required = Required.Always)]
        public required JObject {{ content_field_name }} { get; set; }

    }

    public class {{ type_name }}Converter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return objectType == typeof({{ type_name }});
        }
        public override bool CanWrite { get { return false; } }
        public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
        {
            // unreachable: CanWrite tells Newtonsoft not to call this method
            throw new NotImplementedException();
        }


        public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
        {

            var surrogate = serializer.Deserialize<{{ type_name }}Surrogate>(reader) ?? throw new JsonSerializationException("Failed to deserialize JSON to {{ type_name }}Surrogate");
            if (!typeMap.TryGetValue(surrogate.Type, out Func<(JObject, string), {{ enum_type_name }}>? func))
            {
                throw new JsonSerializationException($"Unexpected type {surrogate.Type} for {{ enum_type_name }}.config");
            }

            {{ enum_type_name }} config = func((surrogate.Config,surrogate.Type));

            return new {{ type_name }}
            {
                {% for f in type.fields -%}
                {{ f.name | to_upper_camel_case }} = surrogate.{{ f.name | to_upper_camel_case }},
                {% endfor -%}
                Config = config
            };
        }

        static private T ToObj<T>((JObject, string) args)
        {
            var loadedObj = args.Item1.ToObject<T>() ?? throw new JsonSerializationException($"Failed to deserialize {args.Item2} config");
            return loadedObj;
        }
        private readonly Dictionary<string, Func<(JObject, string), {{ enum_type_name }}>> typeMap = new()
        {
            {% for v in type.variants -%}
            ["{{ v.name }}"] = c => {{ enum_type_name }}.{{ v.name | to_upper_camel_case }}(
                {%- if v.schema_ref is defined -%}
                ToObj<{{ v.schema_ref | to_upper_camel_case }}>(c)
                {%- endif -%}
            ),
            {% endfor -%}
        };
    }
}
