using System.Collections.Immutable;
using System.Text;
using Motus.Codegen.Model;

namespace Motus.Codegen.Emit;

/// <summary>
/// Emits a complete C# source file for a single CDP domain.
/// </summary>
internal static class DomainEmitter
{
    public static string Emit(CdpDomain domain, TypeResolver resolver)
    {
        var sb = new StringBuilder(8192);
        var domainClass = $"{domain.Name}Domain";

        sb.AppendLine("// <auto-generated/>");
        sb.AppendLine("#nullable enable");
        sb.AppendLine();
        sb.AppendLine("using System.Text.Json;");
        sb.AppendLine("using System.Text.Json.Serialization;");
        sb.AppendLine();
        sb.AppendLine("namespace Motus.Protocol;");
        sb.AppendLine();

        if (domain.Deprecated)
            sb.AppendLine("[System.Obsolete(\"Deprecated by CDP spec.\")]");
        else if (domain.Experimental)
            sb.AppendLine("[System.Obsolete(\"Experimental CDP domain - may change without notice.\")]");

        sb.AppendLine($"public static partial class {domainClass}");
        sb.AppendLine("{");

        // Emit types
        foreach (var type in domain.Types)
        {
            EmitType(sb, type, domain.Name, resolver);
        }

        // Emit command params and response records
        foreach (var command in domain.Commands)
        {
            EmitCommand(sb, command, domain.Name, resolver);
        }

        // Emit event records
        foreach (var evt in domain.Events)
        {
            EmitEvent(sb, evt, domain.Name, resolver);
        }

        sb.AppendLine("}");

        return sb.ToString();
    }

    private static void EmitType(StringBuilder sb, CdpType type, string domainName, TypeResolver resolver)
    {
        switch (type.Kind)
        {
            case CdpTypeKind.StringEnum:
                EmitEnum(sb, type);
                break;
            case CdpTypeKind.Object:
                EmitObjectRecord(sb, type.Id, type.Properties, domainName, resolver,
                    type.Deprecated, type.Experimental);
                break;
            // Alias and ArrayType types don't need their own C# type declaration;
            // they resolve to primitives or arrays at usage sites.
        }
    }

    private static void EmitEnum(StringBuilder sb, CdpType type)
    {
        var enumName = NamingHelper.ToPascalCase(type.Id);

        sb.AppendLine();
        if (type.Deprecated)
            sb.AppendLine("    [System.Obsolete(\"Deprecated by CDP spec.\")]");
        else if (type.Experimental)
            sb.AppendLine("    [System.Obsolete(\"Experimental - may change without notice.\")]");

        sb.AppendLine($"    [JsonConverter(typeof(JsonStringEnumConverter<{enumName}>))]");
        sb.AppendLine($"    public enum {enumName}");
        sb.AppendLine("    {");

        for (int i = 0; i < type.EnumValues.Length; i++)
        {
            var rawValue = type.EnumValues[i];
            var memberName = NamingHelper.ToSafeIdentifier(rawValue);

            sb.Append($"        [JsonStringEnumMemberName(\"{rawValue}\")] {memberName}");
            if (i < type.EnumValues.Length - 1)
                sb.AppendLine(",");
            else
                sb.AppendLine();
        }

        sb.AppendLine("    }");
    }

    private static void EmitObjectRecord(StringBuilder sb, string id, ImmutableArray<CdpProperty> properties,
        string domainName, TypeResolver resolver, bool deprecated, bool experimental)
    {
        var typeName = NamingHelper.ToPascalCase(id);

        sb.AppendLine();
        if (deprecated)
            sb.AppendLine("    [System.Obsolete(\"Deprecated by CDP spec.\")]");
        else if (experimental)
            sb.AppendLine("    [System.Obsolete(\"Experimental - may change without notice.\")]");

        if (properties.Length == 0)
        {
            sb.AppendLine($"    public sealed record {typeName}();");
            return;
        }

        sb.AppendLine($"    public sealed record {typeName}(");
        EmitRecordParameters(sb, properties, domainName, resolver);
        sb.AppendLine("    );");
    }

    private static void EmitCommand(StringBuilder sb, CdpCommand command, string domainName, TypeResolver resolver)
    {
        var pascalName = NamingHelper.ToPascalCase(command.Name);

        // Params record
        sb.AppendLine();
        if (command.Deprecated)
            sb.AppendLine("    [System.Obsolete(\"Deprecated by CDP spec.\")]");
        else if (command.Experimental)
            sb.AppendLine("    [System.Obsolete(\"Experimental - may change without notice.\")]");

        if (command.Parameters.Length == 0)
        {
            sb.AppendLine($"    public sealed record {pascalName}Params();");
        }
        else
        {
            sb.AppendLine($"    public sealed record {pascalName}Params(");
            EmitRecordParameters(sb, command.Parameters, domainName, resolver);
            sb.AppendLine("    );");
        }

        // Response record
        sb.AppendLine();
        if (command.Deprecated)
            sb.AppendLine("    [System.Obsolete(\"Deprecated by CDP spec.\")]");
        else if (command.Experimental)
            sb.AppendLine("    [System.Obsolete(\"Experimental - may change without notice.\")]");

        if (command.Returns.Length == 0)
        {
            sb.AppendLine($"    public sealed record {pascalName}Response();");
        }
        else
        {
            sb.AppendLine($"    public sealed record {pascalName}Response(");
            EmitRecordParameters(sb, command.Returns, domainName, resolver);
            sb.AppendLine("    );");
        }
    }

    private static void EmitEvent(StringBuilder sb, CdpEvent evt, string domainName, TypeResolver resolver)
    {
        var pascalName = NamingHelper.ToPascalCase(evt.Name);

        sb.AppendLine();
        if (evt.Deprecated)
            sb.AppendLine("    [System.Obsolete(\"Deprecated by CDP spec.\")]");
        else if (evt.Experimental)
            sb.AppendLine("    [System.Obsolete(\"Experimental - may change without notice.\")]");

        if (evt.Parameters.Length == 0)
        {
            sb.AppendLine($"    public sealed record {pascalName}Event();");
        }
        else
        {
            sb.AppendLine($"    public sealed record {pascalName}Event(");
            EmitRecordParameters(sb, evt.Parameters, domainName, resolver);
            sb.AppendLine("    );");
        }
    }

    private static void EmitRecordParameters(StringBuilder sb, ImmutableArray<CdpProperty> properties,
        string domainName, TypeResolver resolver)
    {
        // Sort: required parameters first, then optional parameters
        var sorted = properties.OrderBy(p => p.Optional ? 1 : 0).ToList();

        for (int i = 0; i < sorted.Count; i++)
        {
            var prop = sorted[i];
            var csType = resolver.Resolve(prop, domainName);
            var paramName = NamingHelper.ToPascalCase(prop.Name);

            // Add JsonPropertyName if the CDP name differs from the C# name
            var cdpName = prop.Name;
            var needsJsonAttr = cdpName != char.ToLowerInvariant(paramName[0]) + paramName.Substring(1);
            if (needsJsonAttr)
                sb.AppendLine($"        [property: JsonPropertyName(\"{cdpName}\")]");

            if (prop.Deprecated)
                sb.AppendLine($"        [property: System.Obsolete(\"Deprecated by CDP spec.\")]");

            sb.Append($"        {csType} {paramName}");

            if (prop.Optional)
                sb.Append(" = default");

            if (i < sorted.Count - 1)
                sb.AppendLine(",");
            else
                sb.AppendLine();
        }
    }
}
