// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.TypeSpec.Generator.Expressions;
using Microsoft.TypeSpec.Generator.Input;
using Microsoft.TypeSpec.Generator.Input.Extensions;
using Microsoft.TypeSpec.Generator.Primitives;
using Microsoft.TypeSpec.Generator.Snippets;
using Microsoft.TypeSpec.Generator.Statements;
using Microsoft.TypeSpec.Generator.Utilities;
using static Microsoft.TypeSpec.Generator.Snippets.Snippet;

namespace Microsoft.TypeSpec.Generator.Providers
{
    public class ModelProvider : TypeProvider
    {
        private const string AdditionalBinaryDataPropsFieldDescription = "Keeps track of any properties unknown to the library.";
        private readonly InputModelType _inputModel;
        // Note the description cannot be built from the constructor as it would lead to a circular dependency between the base
        // and derived models resulting in a stack overflow.
        protected override FormattableString BuildDescription()
        {
            var description = DocHelpers.GetFormattableDescription(_inputModel.Summary, _inputModel.Doc) ??
                              $"The {Name}.";
            if (IsAbstract)
            {
                _derivedModels = BuildDerivedModels();
                var publicDerivedModels = _derivedModels.Where(m => m.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Public)).ToList();
                var derivedClassesDescription =
                    "Please note this is the abstract base class. The derived classes available for instantiation are: ";
                bool addComma = publicDerivedModels.Count > 2;
                for (int i = 0; i < publicDerivedModels.Count; i++)
                {
                    if (i == publicDerivedModels.Count - 1)
                    {
                        derivedClassesDescription += $"{(i > 0 ? "and " : "")}<see cref=\"{publicDerivedModels[i].Type.FullyQualifiedName}\"/>.";
                    }
                    else
                    {
                        derivedClassesDescription += $"<see cref=\"{publicDerivedModels[i].Type.FullyQualifiedName}\"/>{(addComma ? ", " : " ")}";
                    }
                }

                description = $"{description}\n{derivedClassesDescription}";
            }

            return description;
        }

        private readonly bool _isMultiLevelDiscriminator;

        private readonly CSharpType _additionalBinaryDataPropsFieldType = typeof(IDictionary<string, BinaryData>);
        private readonly CSharpType _additionalObjectPropsFieldType = typeof(IDictionary<string, object>);
        private readonly Type _additionalPropsUnknownType = typeof(BinaryData);
        private Lazy<bool> _useObjectAdditionalProperties;
        private FieldProvider? _rawDataField;
        private List<FieldProvider>? _additionalPropertyFields;
        private List<PropertyProvider>? _additionalPropertyProperties;
        private ModelProvider? _baseModelProvider;
        private ConstructorProvider? _fullConstructor;
        internal PropertyProvider? DiscriminatorProperty { get; private set; }
        private ValueExpression DiscriminatorLiteral => Literal(_inputModel.DiscriminatorValue ?? "");
        private bool IsAbstract => _inputModel.DiscriminatorProperty is not null && _inputModel.DiscriminatorValue is null && LastContractView?.DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract) != false;

        public ModelProvider(InputModelType inputModel) : base(inputModel)
        {
            _inputModel = inputModel;
            _isMultiLevelDiscriminator = ComputeIsMultiLevelDiscriminator();
            _useObjectAdditionalProperties = new Lazy<bool>(ShouldUseObjectAdditionalProperties);

            if (_inputModel.BaseModel is not null)
            {
                DiscriminatorValueExpression = EnsureDiscriminatorValueExpression();
            }

            if (_inputModel.Access == "public")
            {
                CodeModelGenerator.Instance.AddTypeToKeep(this);
            }
        }

        public bool IsUnknownDiscriminatorModel => _inputModel.IsUnknownDiscriminatorModel;

        public string? DiscriminatorValue => _inputModel.DiscriminatorValue;
        public ValueExpression? DiscriminatorValueExpression { get; init; }

        private IReadOnlyList<ModelProvider>? _derivedModels;
        public IReadOnlyList<ModelProvider> DerivedModels => _derivedModels ??= BuildDerivedModels();

        private IDictionary<string, CSharpType> LastContractPropertiesMap
            => _lastContractPropertiesMap ??= LastContractView?.Properties.ToDictionary(p => p.Name, p => p.Type) ?? [];

        private IDictionary<string, CSharpType>? _lastContractPropertiesMap;

        private IReadOnlyList<ModelProvider> BuildDerivedModels()
        {
            var derivedModels = new HashSet<ModelProvider>(_inputModel.DiscriminatedSubtypes.Count + _inputModel.DerivedModels.Count);
            // add discriminated subtypes
            foreach (var subtype in _inputModel.DiscriminatedSubtypes)
            {
                var model = CodeModelGenerator.Instance.TypeFactory.CreateModel(subtype.Value);
                if (model != null)
                {
                    derivedModels.Add(model);
                }
            }

            // add derived models
            foreach (var derivedModel in _inputModel.DerivedModels)
            {
                var model = CodeModelGenerator.Instance.TypeFactory.CreateModel(derivedModel);
                if (model != null)
                {
                    derivedModels.Add(model);
                }
            }

            return [.. derivedModels];
        }
        internal override TypeProvider? BaseTypeProvider => _baseTypeProvider ??= BuildBaseTypeProvider();
        private TypeProvider? _baseTypeProvider;

        private TypeProvider? BuildBaseTypeProvider()
        {
            // First check if there's a generated base model
            if (BaseModelProvider != null)
            {
                return BaseModelProvider;
            }

            // If there's a custom base type that's not a generated model, create a provider for it
            if (CustomCodeView?.BaseType != null && !string.IsNullOrEmpty(CustomCodeView.BaseType.Namespace))
            {
                var baseType = CustomCodeView.BaseType;

                // Try to find it in the CSharpTypeMap first
                if (CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(baseType, out var existingProvider))
                {
                    return existingProvider;
                }

                // Try to find the type in the customization compilation (excluding referenced assemblies)
                var baseTypeProvider = CodeModelGenerator.Instance.SourceInputModel.FindForTypeInCustomization(
                    baseType.Namespace,
                    baseType.Name,
                    baseType.DeclaringType?.Name);

                if (baseTypeProvider != null)
                {
                    // Cache it in CSharpTypeMap for future lookups
                    CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = baseTypeProvider;
                    return baseTypeProvider;
                }

                // If we couldn't find the type symbol (e.g., type is from a referenced assembly),
                // create a SystemObjectTypeProvider that represents the external type
                var systemObjectTypeProvider = new SystemObjectTypeProvider(baseType);
                // Cache it in CSharpTypeMap for future lookups
                CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap[baseType] = systemObjectTypeProvider;
                return systemObjectTypeProvider;
            }

            return null;
        }

        public ModelProvider? BaseModelProvider
            => _baseModelProvider ??= BuildBaseModelProvider();
        protected FieldProvider? RawDataField => _rawDataField ??= BuildRawDataField();
        private List<FieldProvider> AdditionalPropertyFields => _additionalPropertyFields ??= BuildAdditionalPropertyFields();
        private List<PropertyProvider> AdditionalPropertyProperties => _additionalPropertyProperties ??= BuildAdditionalPropertyProperties();
        protected internal bool SupportsBinaryDataAdditionalProperties => AdditionalPropertyProperties.Any(p =>
            p.Type.ElementType.Equals(_additionalPropsUnknownType) ||
            (p.Type.ElementType.IsFrameworkType && p.Type.ElementType.FrameworkType == typeof(object)));
        public ConstructorProvider FullConstructor => _fullConstructor ??= BuildFullConstructor();

        protected override string BuildNamespace() => string.IsNullOrEmpty(_inputModel.Namespace) ?
            // TODO remove null check once https://github.com/Azure/typespec-azure/issues/2209 is fixed.
            CodeModelGenerator.Instance.TypeFactory.PrimaryNamespace :
            CodeModelGenerator.Instance.TypeFactory.GetCleanNameSpace(_inputModel.Namespace);

        protected override CSharpType? BuildBaseType()
        {
            return BaseModelProvider?.Type;
        }

        protected override TypeProvider[] BuildSerializationProviders()
        {
            return [.. CodeModelGenerator.Instance.TypeFactory.CreateSerializations(_inputModel, this)];
        }

        protected override string BuildRelativeFilePath() => Path.Combine("src", "Generated", "Models", $"{Name}.cs");

        protected override string BuildName() => _inputModel.Name.ToIdentifierName();

        protected override TypeSignatureModifiers BuildDeclarationModifiers()
        {
            var customCodeModifiers = CustomCodeView?.DeclarationModifiers ?? TypeSignatureModifiers.None;
            var isStruct = false;
            // the information of if this model should be a struct comes from two sources:
            // 1. the customied code
            // 2. the spec
            if (customCodeModifiers.HasFlag(TypeSignatureModifiers.Struct))
            {
                isStruct = true;
            }
            if (_inputModel.ModelAsStruct)
            {
                isStruct = true;
            }
            var declarationModifiers = TypeSignatureModifiers.Partial;

            if (isStruct)
            {
                declarationModifiers |= TypeSignatureModifiers.ReadOnly | TypeSignatureModifiers.Struct;
            }
            else
            {
                declarationModifiers |= TypeSignatureModifiers.Class;
            }

            if (customCodeModifiers != TypeSignatureModifiers.None)
            {
                declarationModifiers |= GetAccessibilityModifiers(customCodeModifiers);
            }
            else if (_inputModel.Access == "internal")
            {
                declarationModifiers |= TypeSignatureModifiers.Internal;
            }

            if (IsAbstract)
            {
                declarationModifiers |= TypeSignatureModifiers.Abstract;
            }

            return declarationModifiers;

            static TypeSignatureModifiers GetAccessibilityModifiers(TypeSignatureModifiers modifiers)
            {
                return modifiers & (TypeSignatureModifiers.Public | TypeSignatureModifiers.Internal | TypeSignatureModifiers.Protected | TypeSignatureModifiers.Private);
            }
        }

        /// <summary>
        /// Builds the fields for the model by adding the raw data field.
        /// </summary>
        /// <returns>The list of <see cref="FieldProvider"/> for the model.</returns>
        protected internal override FieldProvider[] BuildFields()
        {
            List<FieldProvider> fields = [];
            if (RawDataField != null)
            {
                fields.Add(RawDataField);
            }

            // add fields for additional properties
            if (AdditionalPropertyFields.Count > 0)
            {
                fields.AddRange(AdditionalPropertyFields);
            }

            foreach (var property in _inputModel.Properties)
            {
                if (IsDiscriminator(property))
                    continue;

                var derivedProperty = InputDerivedProperties.FirstOrDefault(p => p.Value.ContainsKey(property.Name)).Value?[property.Name];
                if (derivedProperty is not null)
                {
                    if (!DomainEqual(property, derivedProperty))
                    {
                        fields.Add(new FieldProvider(
                            FieldModifiers.Private | FieldModifiers.Protected,
                            CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(property.Type)!,
                            $"_{property.Name.ToVariableName()}",
                            this));
                    }
                }
            }
            return [.. fields];
        }

        private static bool IsDiscriminator(InputProperty property)
        {
            return property is InputModelProperty modelProperty && modelProperty.IsDiscriminator;
        }

        private ModelProvider? BuildBaseModelProvider()
        {
            // consider models that have been customized to inherit from a different generated model
            if (CustomCodeView?.BaseType != null)
            {
                var baseType = CustomCodeView.BaseType;

                // If the custom base type doesn't have a resolved namespace, then try to resolve it from the input model map.
                // This will happen if a model is customized to inherit from another generated model, but that generated model
                // was not also defined in custom code so Roslyn does not recognize it.
                if (string.IsNullOrEmpty(baseType.Namespace))
                {
                    // Cheap check: the base model may already be created and registered under the right name.
                    if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue(
                            baseType.Name, out var resolvedProvider) &&
                        resolvedProvider is ModelProvider resolvedModel)
                    {
                        return resolvedModel;
                    }

                    // Force-create all input models so that visitors run (which may rename models
                    // via TypeProvider.Update) and TypeProvidersByName is fully populated.
                    // This is a no-op for models that have already been created.
                    foreach (var model in CodeModelGenerator.Instance.InputLibrary.InputNamespace.Models)
                    {
                        CodeModelGenerator.Instance.TypeFactory.CreateModel(model);
                    }

                    if (CodeModelGenerator.Instance.TypeFactory.TypeProvidersByName.TryGetValue(
                            baseType.Name, out resolvedProvider) &&
                        resolvedProvider is ModelProvider resolvedAfterCreate)
                    {
                        return resolvedAfterCreate;
                    }
                }

                // Try to find the base type in the CSharpTypeMap
                if (baseType != null && CodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue(
                        baseType,
                        out var customBaseType) &&
                    customBaseType is ModelProvider customBaseModel)
                {
                    return customBaseModel;
                }

                // If the custom base type has a namespace (external type), we don't return it here
                // as it's handled by BuildBaseTypeProvider() which returns a TypeProvider
                if (!string.IsNullOrEmpty(baseType?.Namespace))
                {
                    return null;
                }
            }

            if (_inputModel.BaseModel == null)
            {
                return null;
            }

            return CodeModelGenerator.Instance.TypeFactory.CreateModel(_inputModel.BaseModel);
        }

        private List<FieldProvider> BuildAdditionalPropertyFields()
        {
            var fields = new List<FieldProvider>();

            if (_inputModel.AdditionalProperties != null)
            {
                var valueType = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(_inputModel.AdditionalProperties);
                if (valueType != null)
                {
                    if (valueType.IsUnion)
                    {
                        foreach (var unionType in valueType.UnionItemTypes)
                        {
                            AddFieldForAdditionalProperties(unionType, fields, true);
                        }
                    }
                    else
                    {
                        AddFieldForAdditionalProperties(valueType, fields, false);
                    }
                }
            }

            return fields;
        }

        private void AddFieldForAdditionalProperties(CSharpType valueType, List<FieldProvider> fields, bool isUnionType)
        {
            var originalType = new CSharpType(typeof(IDictionary<,>), typeof(string), valueType);
            var additionalPropsType = ReplaceUnverifiableType(originalType);

            if ((isUnionType && additionalPropsType.ContainsBinaryData)
                || additionalPropsType.Equals(_additionalBinaryDataPropsFieldType))
            {
                return;
            }

            fields.Add(new(
                FieldModifiers.Private,
                additionalPropsType,
                BuildAdditionalTypePropertiesFieldName(additionalPropsType.ElementType),
                this));
        }

        private List<PropertyProvider> BuildAdditionalPropertyProperties()
        {
            var additionalPropertiesFieldCount = AdditionalPropertyFields.Count;
            var properties = new List<PropertyProvider>(additionalPropertiesFieldCount + 1);
            bool containsAdditionalTypeProperties = false;

            for (int i = 0; i < additionalPropertiesFieldCount; i++)
            {
                var field = AdditionalPropertyFields[i];
                var propertyType = !_inputModel.Usage.HasFlag(InputModelTypeUsage.Input) ? field.Type.OutputType : field.Type;
                var assignment = propertyType.IsReadOnlyDictionary
                   ? new ExpressionPropertyBody(New.ReadOnlyDictionary(propertyType.Arguments[0], propertyType.ElementType, field))
                   : new ExpressionPropertyBody(field);

                properties.Add(new(
                    null,
                    MethodSignatureModifiers.Public,
                    propertyType,
                    i == 0 ? AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName : field.Name.ToIdentifierName(),
                    assignment,
                    this)
                {
                    BackingField = field,
                    IsAdditionalProperties = true
                });
                containsAdditionalTypeProperties = true;
            }

            if (RawDataField == null || _inputModel.AdditionalProperties == null)
            {
                return properties;
            }

            var apValueType = CodeModelGenerator.Instance.TypeFactory.CreateCSharpType(_inputModel.AdditionalProperties);
            if (apValueType == null)
            {
                return properties;
            }

            // add public property for raw binary data if the model supports additional binary data properties
            var originalType = new CSharpType(typeof(IDictionary<,>), typeof(string), apValueType);
            var additionalPropsType = ReplaceUnverifiableType(originalType);
            var shouldAddPropForUnionType = additionalPropsType.ElementType.IsUnion
                && additionalPropsType.ElementType.UnionItemTypes.Any(t => !t.IsFrameworkType);

            if (shouldAddPropForUnionType || (!apValueType.IsUnion && additionalPropsType.Equals(_additionalBinaryDataPropsFieldType)))
            {
                var name = !containsAdditionalTypeProperties
                    ? AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName
                    : RawDataField.Name.ToIdentifierName();

                // Use object type if backward compatibility requires it, otherwise use BinaryData type
                var propertyType = _useObjectAdditionalProperties.Value ? _additionalObjectPropsFieldType : additionalPropsType;
                var type = !_inputModel.Usage.HasFlag(InputModelTypeUsage.Input)
                    ? propertyType.OutputType
                    : propertyType;

                var assignment = type.IsReadOnlyDictionary
                    ? new ExpressionPropertyBody(New.ReadOnlyDictionary(type.Arguments[0], type.ElementType, RawDataField))
                    : new ExpressionPropertyBody(RawDataField);
                var property = new PropertyProvider(
                    null,
                    MethodSignatureModifiers.Public,
                    type,
                    name,
                    assignment,
                    this)
                {
                    BackingField = RawDataField,
                    IsAdditionalProperties = true
                };
                properties.Add(property);
            }

            return properties;
        }

        private Dictionary<InputModelType, Dictionary<string, InputModelProperty>>? _inputDerivedProperties;
        private Dictionary<InputModelType, Dictionary<string, InputModelProperty>> InputDerivedProperties => _inputDerivedProperties ??= BuildDerivedProperties();

        private Dictionary<InputModelType, Dictionary<string, InputModelProperty>> BuildDerivedProperties()
        {
            Dictionary<InputModelType, Dictionary<string, InputModelProperty>> derivedProperties = [];
            var derivedModels = new List<InputModelType>();
            EnumerateDerivedModels(_inputModel, derivedModels);
            foreach (var derivedModel in derivedModels)
            {
                var derivedModelProperties = derivedModel.Properties;
                if (derivedModelProperties.Count > 0)
                {
                    derivedProperties[derivedModel] = derivedModelProperties.ToDictionary(p => p.Name);
                }
            }
            return derivedProperties;
        }

        private void EnumerateDerivedModels(InputModelType inputModel, List<InputModelType> derivedModels)
        {
            foreach (var derivedModel in inputModel.DerivedModels)
            {
                derivedModels.Add(derivedModel);
                EnumerateDerivedModels(derivedModel, derivedModels);
            }
        }

        protected internal override PropertyProvider[] BuildProperties()
        {
            var propertiesCount = _inputModel.Properties.Count;
            var properties = new List<PropertyProvider>(propertiesCount + 1);
            Dictionary<string, InputModelProperty> baseProperties = EnumerateBaseModels().SelectMany(m => m.Properties).GroupBy(x => x.Name).Select(g => g.First()).ToDictionary(p => p.Name) ?? [];
            // Build a set of serialized names for base discriminator properties to handle cases where
            // the derived model has a discriminator with a different C# name but the same wire name
            HashSet<string> baseDiscriminatorSerializedNames = EnumerateBaseModels()
                .SelectMany(m => m.Properties)
                .Where(p => p.IsDiscriminator && p.SerializedName is not null)
                .Select(p => p.SerializedName)
                .ToHashSet();
            for (int i = 0; i < propertiesCount; i++)
            {
                var property = _inputModel.Properties[i];
                var isDiscriminator = IsDiscriminator(property);

                // Skip discriminator properties that already exist in the base class
                // Check both by C# property name and by serialized name to handle cases where
                // the derived model has a discriminator with a different C# name but the same wire name
                if (isDiscriminator && (baseProperties.ContainsKey(property.Name) || (property.SerializedName is not null && baseDiscriminatorSerializedNames.Contains(property.SerializedName))))
                {
                    continue;
                }

                var outputProperty = CodeModelGenerator.Instance.TypeFactory.CreateProperty(property, this);

                if (_inputModel.DiscriminatorProperty == property)
                {
                    DiscriminatorProperty = outputProperty;
                }

                if (outputProperty is null)
                {
                    continue;
                }

                // Targeted backcompat fix for the case where properties were previously generated as read-only collections
                if (outputProperty.Type.IsReadWriteList || outputProperty.Type.IsReadWriteDictionary)
                {
                    if (LastContractPropertiesMap.TryGetValue(outputProperty.Name,
                            out CSharpType? lastContractPropertyType) &&
                        !outputProperty.Type.Equals(lastContractPropertyType))
                    {
                        outputProperty.Type = lastContractPropertyType.ApplyInputSpecProperty(property);
                        CodeModelGenerator.Instance.Emitter.Info($"Changed property {Name}.{outputProperty.Name} type to {lastContractPropertyType} to match last contract.");
                    }
                }

                if (!isDiscriminator)
                {
                    var derivedProperty = InputDerivedProperties.FirstOrDefault(p => p.Value.ContainsKey(property.Name)).Value?[property.Name];
                    if (derivedProperty is not null)
                    {
                        if (DomainEqual(property, derivedProperty))
                        {
                            outputProperty.Modifiers |= MethodSignatureModifiers.Virtual;
                        }
                    }
                    var baseProperty = baseProperties.GetValueOrDefault(property.Name);
                    if (baseProperty is not null)
                    {
                        if (DomainEqual(baseProperty, property))
                        {
                            outputProperty.Modifiers |= MethodSignatureModifiers.Override;
                        }
                        else
                        {
                            outputProperty.Modifiers |= MethodSignatureModifiers.New;
                            var fieldName = $"_{baseProperty.Name.ToVariableName()}";
                            outputProperty.Body = new ExpressionPropertyBody(
                                This.Property(fieldName).NullCoalesce(Default),
                                outputProperty.Body.HasSetter ? This.Property(fieldName).Assign(Value) : null);
                            outputProperty.BackingField = BaseModelProvider?.Fields.FirstOrDefault(f => f.Name == fieldName);
                        }
                        outputProperty.BaseProperty = CodeModelGenerator.Instance.TypeFactory.CreateProperty(baseProperty, BaseModelProvider!);
                    }
                }
                properties.Add(outputProperty);
            }

            if (AdditionalPropertyProperties.Count > 0)
            {
                properties.AddRange(AdditionalPropertyProperties);
            }

            return [.. properties];
        }

        private IEnumerable<InputModelType> EnumerateBaseModels()
        {
            var model = BaseModelProvider;
            while (model != null)
            {
                yield return model._inputModel;
                model = model.BaseModelProvider;
            }
        }

        private static bool DomainEqual(InputProperty baseProperty, InputProperty derivedProperty)
        {
            if (baseProperty.Type.Name != derivedProperty.Type.Name)
                return false;
            if (baseProperty.IsRequired != derivedProperty.IsRequired)
                return false;
            var baseNullable = baseProperty.Type is InputNullableType;
            return baseNullable ? derivedProperty.Type is InputNullableType : derivedProperty.Type is not InputNullableType;
        }

        protected internal override ConstructorProvider[] BuildConstructors()
        {
            if (_inputModel.IsUnknownDiscriminatorModel)
            {
                return [FullConstructor];
            }

            // Build the standard single initialization constructor
            var accessibility = DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract)
                ? MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected
                : _inputModel.Usage.HasFlag(InputModelTypeUsage.Input)
                    ? MethodSignatureModifiers.Public
                    : MethodSignatureModifiers.Internal;
            var (constructorParameters, constructorInitializer) = BuildConstructorParameters(true);

            var constructor = new ConstructorProvider(
                signature: new ConstructorSignature(
                    Type,
                    $"Initializes a new instance of {Type:C}",
                    accessibility,
                    constructorParameters,
                    initializer: constructorInitializer),
                bodyStatements: new MethodBodyStatement[]
                {
                    GetPropertyInitializers(true, parameters: constructorParameters)
                },
                this);

            var constructors = new List<ConstructorProvider> { constructor };

            // Add FullConstructor if parameters are different
            if (!constructorParameters.SequenceEqual(FullConstructor.Signature.Parameters))
            {
                constructors.Add(FullConstructor);
            }

            // For multi-level discriminators, add one additional private protected constructor
            if (_isMultiLevelDiscriminator)
            {
                var protectedConstructor = BuildProtectedInheritanceConstructor();
                constructors.Add(protectedConstructor);
            }

            return [.. constructors];
        }

        /// <summary>
        /// Determines if this model should have a dual constructor pattern.
        /// This is needed when the model shares the same discriminator property name as its base model
        /// AND has derived models, indicating it's an intermediate type in a discriminated union hierarchy.
        /// </summary>
        private bool ComputeIsMultiLevelDiscriminator()
        {
            // Only applies to non-abstract models with a base model
            if (IsAbstract || _inputModel.BaseModel == null)
            {
                return false;
            }
            // Must have derived models to be considered an intermediate type
            if (_inputModel.DerivedModels.Count == 0)
            {
                return false;
            }

            // Check if this model has a discriminator property in the input
            if (_inputModel.DiscriminatorProperty == null)
            {
                return false;
            }

            // Check if base model has a discriminator property with the same name
            if (_inputModel.BaseModel.DiscriminatorProperty == null)
            {
                return false;
            }

            // If both models have discriminator properties with the same name,
            // and this model has derived models, it needs the dual constructor pattern
            return _inputModel.DiscriminatorProperty.Name ==
                _inputModel.BaseModel.DiscriminatorProperty.Name;
        }

        /// <summary>
        /// Builds a private protected constructor for multi-level discriminator inheritance.
        /// This allows derived models to call this constructor with their discriminator value.
        /// </summary>
        private ConstructorProvider BuildProtectedInheritanceConstructor()
        {
            var (parameters, initializer) = BuildConstructorParameters(true, includeDiscriminatorParameter: true);

            return new ConstructorProvider(
                signature: new ConstructorSignature(
                    Type,
                    $"Initializes a new instance of {Type:C}",
                    MethodSignatureModifiers.Private | MethodSignatureModifiers.Protected,
                    parameters,
                    initializer: initializer),
                bodyStatements: new MethodBodyStatement[]
                {
                    GetPropertyInitializers(true, parameters: parameters)
                },
                this);
        }

        /// <summary>
        /// Builds the internal constructor for the model which contains all public properties
        /// as parameters.
        /// </summary>
        private ConstructorProvider BuildFullConstructor()
        {
            var (ctorParameters, ctorInitializer) = BuildConstructorParameters(false);
            return new ConstructorProvider(
                signature: new ConstructorSignature(
                    Type,
                    $"Initializes a new instance of {Type:C}",
                    MethodSignatureModifiers.Internal,
                    ctorParameters,
                    initializer: ctorInitializer),
                bodyStatements: new MethodBodyStatement[]
                {
                    GetPropertyInitializers(false)
                },
                this);
        }

        protected internal override IReadOnlyList<ConstructorProvider> BuildConstructorsForBackCompatibility(IEnumerable<ConstructorProvider> originalConstructors)
        {
            // Only handle the case of changing modifiers on abstract base types
            if (!DeclarationModifiers.HasFlag(TypeSignatureModifiers.Abstract))
            {
                return [.. originalConstructors];
            }

            if (LastContractView?.Constructors == null || LastContractView.Constructors.Count == 0)
            {
                return [.. originalConstructors];
            }

            List<ConstructorProvider> constructors = [.. originalConstructors];

            // Check if the last contract had a public constructor with matching parameters
            foreach (var previousConstructor in LastContractView.Constructors)
            {
                if (!previousConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Public))
                {
                    continue;
                }

                // Find a matching constructor in the current version by parameter signature
                for (int i = 0; i < constructors.Count; i++)
                {
                    var currentConstructor = constructors[i];

                    // Check if parameters match (same count and types)
                    if (ParametersMatch(currentConstructor.Signature.Parameters, previousConstructor.Signature.Parameters))
                    {
                        // Change the modifier from private protected to public
                        if (currentConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Private) &&
                            currentConstructor.Signature.Modifiers.HasFlag(MethodSignatureModifiers.Protected))
                        {
                            currentConstructor.Signature.Update(modifiers: MethodSignatureModifiers.Public);
                        }
                    }
                }
            }

            return [.. constructors];
        }

        private bool ParametersMatch(IReadOnlyList<ParameterProvider> params1, IReadOnlyList<ParameterProvider> params2)
        {
            if (params1.Count != params2.Count)
            {
                return false;
            }

            for (int i = 0; i < params1.Count; i++)
            {
                if (!params1[i].Type.AreNamesEqual(params2[i].Type) || params1[i].Name != params2[i].Name)
                {
                    return false;
                }
            }

            return true;
        }

        private IEnumerable<PropertyProvider> GetAllBasePropertiesForConstructorInitialization(bool includeAllHierarchyDiscriminator = false)
        {
            var properties = new Stack<List<PropertyProvider>>();
            var modelProvider = BaseModelProvider;
            bool isDirectBase = true;
            while (modelProvider != null)
            {
                properties.Push([]);
                foreach (var property in modelProvider.CanonicalView.Properties)
                {
                    if (property.IsDiscriminator)
                    {
                        // In the case of nested discriminators, include discriminator property based on the parameter
                        if (isDirectBase || includeAllHierarchyDiscriminator)
                        {
                            properties.Peek().Add(property);
                        }
                    }
                    else
                    {
                        properties.Peek().Add(property);
                    }
                }

                modelProvider = modelProvider.BaseModelProvider;
                isDirectBase = false;
            }

            // parameters need to be ordered from the base-most class to the derived class
            return properties.SelectMany(l => l);
        }

        private IEnumerable<FieldProvider> GetAllBaseFieldsForConstructorInitialization()
        {
            var fields = new Stack<List<FieldProvider>>();
            var modelProvider = BaseModelProvider;
            while (modelProvider != null)
            {
                fields.Push([]);
                foreach (var field in modelProvider.CanonicalView.Fields)
                {
                    fields.Peek().Add(field);
                }
                modelProvider = modelProvider.BaseModelProvider;
            }

            return fields.SelectMany(l => l);
        }

        private (IReadOnlyList<ParameterProvider> Parameters, ConstructorInitializer? Initializer) BuildConstructorParameters(
            bool isInitializationConstructor, bool includeDiscriminatorParameter = false)
        {
            var baseParameters = new List<ParameterProvider>();
            var constructorParameters = new List<ParameterProvider>();
            IEnumerable<PropertyProvider> baseProperties = [];
            IEnumerable<FieldProvider> baseFields = [];

            if (isInitializationConstructor)
            {
                baseProperties = GetAllBasePropertiesForConstructorInitialization(includeDiscriminatorParameter);
                baseFields = GetAllBaseFieldsForConstructorInitialization();
            }
            else if (BaseModelProvider?.FullConstructor.Signature != null)
            {
                baseParameters.AddRange(BaseModelProvider.FullConstructor.Signature.Parameters);
            }

            HashSet<PropertyProvider> overriddenProperties = CanonicalView.Properties.Where(p => p.BaseProperty is not null).Select(p => p.BaseProperty!).ToHashSet();

            // add the base parameters, if any
            foreach (var property in baseProperties)
            {
                AddInitializationParameterForCtor(baseParameters, Type.IsStruct, isInitializationConstructor, property);
            }

            // add the base fields, if any
            foreach (var field in baseFields)
            {
                AddInitializationParameterForCtor(baseParameters, Type.IsStruct, isInitializationConstructor, field: field);
            }

            // Build constructor parameters first so we can use them for initializer
            foreach (var property in CanonicalView.Properties)
            {
                AddInitializationParameterForCtor(constructorParameters, Type.IsStruct, isInitializationConstructor, property);
            }

            foreach (var field in CanonicalView.Fields)
            {
                AddInitializationParameterForCtor(constructorParameters, Type.IsStruct, isInitializationConstructor, field: field);
            }

            constructorParameters.InsertRange(0, _inputModel.IsUnknownDiscriminatorModel
                ? baseParameters
                : baseParameters.Where(p =>
                    p.Property is null
                    || (!overriddenProperties.Contains(p.Property!) && (!p.Property.IsDiscriminator || !isInitializationConstructor || (includeDiscriminatorParameter && _isMultiLevelDiscriminator)))));

            // construct the initializer using the parameters from base signature
            ConstructorInitializer? constructorInitializer = null;
            if (BaseModelProvider != null)
            {
                if (baseParameters.Count > 0)
                {
                    // Check if we should call multi-level discriminator constructor
                    if (isInitializationConstructor && (_isMultiLevelDiscriminator || BaseModelProvider._isMultiLevelDiscriminator))
                    {
                        var baseDiscriminatorParam = baseParameters.FirstOrDefault(p => p.Property?.IsDiscriminator == true);
                        var hasDiscriminatorProperty = BaseModelProvider.CanonicalView.Properties.Any(p => p.IsDiscriminator);

                        ValueExpression discriminatorExpression = (hasDiscriminatorProperty && baseDiscriminatorParam is not null && includeDiscriminatorParameter)
                            ? constructorParameters.FirstOrDefault(p => p.Property?.IsDiscriminator == true) ?? baseDiscriminatorParam
                            : DiscriminatorLiteral;

                        var args = baseParameters.Where(p => p.Property?.IsDiscriminator != true)
                            .Select(p => GetExpressionForCtor(p, overriddenProperties, isInitializationConstructor));

                        constructorInitializer = new ConstructorInitializer(true, [discriminatorExpression, .. args]);
                    }
                    else
                    {
                        // Standard base constructor call
                        constructorInitializer = new ConstructorInitializer(true, [.. baseParameters.Select(p => GetExpressionForCtor(p, overriddenProperties, isInitializationConstructor))]);
                    }
                }
                else
                {
                    // Even when no base parameters, we still need a base constructor call if there's a base model
                    constructorInitializer = new ConstructorInitializer(true, Array.Empty<ValueExpression>());
                }
            }

            if (!isInitializationConstructor)
            {
                foreach (var property in AdditionalPropertyProperties)
                {
                    constructorParameters.Add(property.AsParameter);
                }

                // only add the raw data field if it has not already been added as a parameter for BinaryData additional properties
                if (RawDataField != null && !SupportsBinaryDataAdditionalProperties)
                    constructorParameters.Add(RawDataField.AsParameter);
            }

            return (constructorParameters, constructorInitializer);
        }

        private ValueExpression? EnsureDiscriminatorValueExpression()
        {
            if (_inputModel.BaseModel is not null && _inputModel.DiscriminatorValue is not null)
            {
                var discriminator = BaseModelProvider?.CanonicalView.Properties.Where(p => p.IsDiscriminator).FirstOrDefault();
                if (discriminator != null)
                {
                    var type = discriminator.Type;
                    if (IsUnknownDiscriminatorModel)
                    {
                        return GetUnknownDiscriminatorExpression(discriminator);
                    }

                    if (type is { IsFrameworkType: false, IsEnum: true })
                    {
                        if (_inputModel.BaseModel.DiscriminatorProperty?.Type is InputEnumType inputEnumType)
                        {
                            var discriminatorProvider = CodeModelGenerator.Instance.TypeFactory.CreateEnum(enumType: inputEnumType);

                            var enumMember = discriminatorProvider!.EnumValues.FirstOrDefault(e => e.Value.ToString() == _inputModel.DiscriminatorValue)
                                             ?? throw new InvalidOperationException($"invalid discriminator value {_inputModel.DiscriminatorValue}");
                            var enumMemberName = enumMember.Name;

                            // Check to see if the enum member for this discriminator value has been customized
                            var customEnumProperty = discriminatorProvider.CustomCodeView?.Properties
                                .FirstOrDefault(f => f.OriginalName?.Equals(enumMemberName, StringComparison.OrdinalIgnoreCase) == true);
                            if (customEnumProperty != null)
                            {
                                enumMemberName = customEnumProperty.Name;
                            }
                            /* {KindType}.{enumMember} */
                            return Static(type).Property(enumMemberName);
                        }

                        // Handle custom fixed enum discriminator
                        if (discriminator.CustomProvider?.Value?.IsEnum == true)
                        {
                            var enumMember = discriminator.CustomProvider.Value.Fields
                                .FirstOrDefault(f => f.Name.Equals(_inputModel.DiscriminatorValue, StringComparison.OrdinalIgnoreCase));
                            if (enumMember != null)
                            {
                                return Static(type).Property(enumMember.Name);
                            }
                        }
                    }

                    // fallback to the default value
                    return DiscriminatorLiteral;
                }
            }
            return null;
        }

        private ValueExpression GetExpressionForCtor(ParameterProvider parameter, HashSet<PropertyProvider> overriddenProperties, bool isPrimaryConstructor)
        {
            if (parameter.Property is not null && parameter.Property.IsDiscriminator && _inputModel.DiscriminatorValue != null)
            {
                if (isPrimaryConstructor)
                {
                    return DiscriminatorValueExpression ?? throw new InvalidOperationException($"invalid discriminator {_inputModel.DiscriminatorValue} for property {parameter.Property.Name}");
                }
                else if (IsUnknownDiscriminatorModel)
                {
                    return GetUnknownDiscriminatorExpression(parameter.Property) ?? throw new InvalidOperationException($"invalid discriminator {_inputModel.DiscriminatorValue} for property {parameter.Property.Name}");
                }
                else
                {
                    return parameter;
                }
            }

            var paramToUse = parameter.Property is not null && overriddenProperties.Contains(parameter.Property) ? Properties.First(p => p.Name == parameter.Property.Name).AsParameter : parameter;

            return paramToUse.Property is not null ? GetConversion(paramToUse.Property) : paramToUse;
        }

        private ValueExpression? GetUnknownDiscriminatorExpression(PropertyProvider property)
        {
            if (!property.IsDiscriminator || _inputModel.DiscriminatorValue == null)
            {
                return null;
            }

            var discriminatorExpression = property.AsParameter;
            var type = property.Type;

            if (!type.IsFrameworkType && type.IsEnum)
            {
                if (type.IsStruct)
                {
                    /* kind != default ? kind : "unknown" */
                    return new TernaryConditionalExpression(discriminatorExpression.NotEqual(Default), discriminatorExpression, DiscriminatorLiteral);
                }
                else
                {
                    return discriminatorExpression;
                }
            }
            else
            {
                /* kind ?? "unknown" */
                return discriminatorExpression.NullCoalesce(DiscriminatorLiteral);
            }
        }

        private static void AddInitializationParameterForCtor(
            List<ParameterProvider> parameters,
            bool isStruct,
            bool isPrimaryConstructor,
            PropertyProvider? property = default,
            FieldProvider? field = default)
        {
            var wireInfo = property?.WireInfo ?? field?.WireInfo;
            var type = property?.Type ?? field?.Type;

            // We only add those properties with wire info indicating they are coming from specs.
            if (wireInfo == null)
            {
                return;
            }

            var parameter = property?.AsParameter ?? field!.AsParameter;
            if (isPrimaryConstructor)
            {
                if (isStruct || (wireInfo.IsRequired && !type!.IsLiteral))
                {
                    if (!wireInfo.IsReadOnly)
                    {
                        parameters.Add(parameter.ToPublicInputParameter());
                    }
                }
            }
            else
            {
                // For the serialization constructor, we always add the property as a parameter
                parameters.Add(parameter);
            }
        }

        private MethodBodyStatement GetPropertyInitializers(
            bool isPrimaryConstructor,
            IReadOnlyList<ParameterProvider>? parameters = null)
        {
            List<MethodBodyStatement> methodBodyStatements = new(CanonicalView.Properties.Count + CanonicalView.Fields.Count + 1);
            Dictionary<string, ParameterProvider> parameterMap = parameters?.ToDictionary(p => p.Name) ?? [];

            foreach (var property in CanonicalView.Properties)
            {
                CreatePropertyAssignmentStatement(isPrimaryConstructor, methodBodyStatements, parameterMap, property);
            }

            foreach (var field in CanonicalView.Fields)
            {
                CreatePropertyAssignmentStatement(isPrimaryConstructor, methodBodyStatements, parameterMap, field: field);
            }

            // If discriminator is defined as optional in the base model, but we have an expression for it, assign it in the
            // primary constructor body.
            if (isPrimaryConstructor && DiscriminatorValueExpression != null)
            {
                var baseDiscriminatorProperty = BaseModelProvider?.DiscriminatorProperty;
                if (baseDiscriminatorProperty is { WireInfo.IsRequired: false })
                {
                    methodBodyStatements.Add(baseDiscriminatorProperty.Assign(DiscriminatorValueExpression).Terminate());
                }
            }

            // handle additional properties
            foreach (var property in AdditionalPropertyProperties)
            {
                var backingField = property.BackingField;
                if (backingField != null)
                {
                    AssignmentExpression assignment = backingField.Assign(property.AsParameter);
                    if (isPrimaryConstructor)
                    {
                        assignment = backingField.Assign(New.Instance(backingField.Type.PropertyInitializationType));
                    }
                    else if (property.Type.IsReadOnlyDictionary)
                    {
                        assignment = backingField.Assign(New.Instance(backingField.Type.PropertyInitializationType, property.AsParameter));
                    }

                    methodBodyStatements.Add(assignment.Terminate());
                }
            }

            if (RawDataField != null)
            {
                // initialize the raw data field in the serialization constructor if the model does not explicitly support AP of binary data.
                if (!isPrimaryConstructor && !SupportsBinaryDataAdditionalProperties)
                {
                    methodBodyStatements.Add(RawDataField.Assign(RawDataField.AsParameter).Terminate());
                }
            }

            return methodBodyStatements;
        }

        private void CreatePropertyAssignmentStatement(
            bool isPrimaryConstructor,
            List<MethodBodyStatement> methodBodyStatements,
            Dictionary<string, ParameterProvider> parameterMap,
            PropertyProvider? property = default,
            FieldProvider? field = default)
        {
            var wireInfo = property?.WireInfo ?? field?.WireInfo;
            // skip those non-spec properties
            if (wireInfo == null)
                return;

            // skip if this is an overload / new of a base property
            // also skip if the base was required or the derived property is not required
            if (property?.BaseProperty is not null && (!isPrimaryConstructor || wireInfo.IsRequired == false || property.BaseProperty.WireInfo?.IsRequired == true))
                return;

            ValueExpression assignee = property != null
                ? property.BackingField is null ? property : property.BackingField
                : field!;

            if (!isPrimaryConstructor)
            {
                // always add the property for the serialization constructor
                methodBodyStatements.Add(assignee.Assign(GetConversion(property, field)).Terminate());
                return;
            }

            ValueExpression? initializationValue = null;

            var type = property?.Type ?? field!.Type;

            if (parameterMap.TryGetValue(property?.AsParameter.Name ?? field!.AsParameter.Name, out var parameter) || Type.IsStruct)
            {
                if (parameter != null)
                {
                    initializationValue = parameter;

                    if (CSharpType.RequiresToList(parameter.Type, type))
                    {
                        initializationValue = parameter.Type.IsNullable ?
                            initializationValue.NullConditional().ToList() :
                            initializationValue.ToList();
                    }
                }
            }
            else if (initializationValue == null && type.IsCollection)
            {
                initializationValue = New.Instance(type.PropertyInitializationType);
            }

            if (initializationValue != null)
            {
                methodBodyStatements.Add(assignee.Assign(initializationValue).Terminate());
            }
        }

        private ValueExpression GetConversion(PropertyProvider? property = default, FieldProvider? field = default)
        {
            CSharpType to = property != null
                ? property.BackingField is null ? property.Type : property.BackingField.Type
                : field!.Type;
            CSharpType from = property?.Type ?? field!.Type;

            if (from.IsEnum && to.Equals(from.UnderlyingEnumType))
            {
                return from.ToSerial(property?.AsParameter ?? field!.AsParameter);
            }

            return property?.AsParameter ?? field!.AsParameter;
        }

        /// <summary>
        /// Builds the raw data field for the model to be used for serialization.
        /// </summary>
        /// <returns>The constructed <see cref="FieldProvider"/> if the model should generate the field.</returns>
        private FieldProvider? BuildRawDataField()
        {
            // check if there is a raw data field on any of the base models, if so, we do not have to have one here.
            var baseModelProvider = BaseModelProvider;
            while (baseModelProvider != null)
            {
                if (baseModelProvider.RawDataField != null)
                {
                    return null;
                }
                baseModelProvider = baseModelProvider.BaseModelProvider;
            }

            var modifiers = FieldModifiers.Private;
            if (!DeclarationModifiers.HasFlag(TypeSignatureModifiers.Sealed) && !DeclarationModifiers.HasFlag(TypeSignatureModifiers.Struct))
            {
                modifiers |= FieldModifiers.Protected;
            }
            modifiers |= FieldModifiers.ReadOnly;

            // Use object type for backward compatibility if needed, otherwise use BinaryData
            var fieldType = _useObjectAdditionalProperties.Value ? _additionalObjectPropsFieldType : _additionalBinaryDataPropsFieldType;

            var rawDataField = new FieldProvider(
                modifiers: modifiers,
                type: fieldType,
                description: FormattableStringHelpers.FromString(AdditionalBinaryDataPropsFieldDescription),
                name: AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName,
                enclosingType: this);

            return rawDataField;
        }

        /// <summary>
        /// Replaces unverifiable types, types that do not have value kind checks during deserialization of additional properties,
        /// with the corresponding verifiable types. By default, BinaryData is used as the value type for unknown additional properties.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private CSharpType ReplaceUnverifiableType(CSharpType type)
        {
            return type switch
            {
                _ when type.Equals(_additionalPropsUnknownType, ignoreNullable: true) => type,
                _ when type.IsFrameworkType && AdditionalPropertiesHelper.VerifiableAdditionalPropertyTypes.Contains(type.FrameworkType) => type,
                _ when type.IsUnion => type,
                _ when type.IsList => type.MakeGenericType([ReplaceUnverifiableType(type.Arguments[0])]),
                _ when type.IsDictionary => type.MakeGenericType([ReplaceUnverifiableType(type.Arguments[0]), ReplaceUnverifiableType(type.Arguments[1])]),
                _ => CSharpType.FromUnion([type])
            };
        }

        /// <summary>
        /// Determines whether to use object type for AdditionalProperties based on backward compatibility requirements.
        /// Checks if the last contract (previous version) had an AdditionalProperties property of type IDictionary&lt;string, object&gt;.
        /// </summary>
        /// <returns>True if object type should be used for backward compatibility; otherwise false (uses BinaryData).</returns>
        private bool ShouldUseObjectAdditionalProperties()
        {
            if (LastContractView == null || _inputModel.AdditionalProperties == null)
            {
                return false;
            }

            // Check if the property exists in the last contract by name
            var lastContractProperty = LastContractView.Properties.FirstOrDefault(p =>
                p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName);

            if (lastContractProperty == null)
            {
                return false;
            }

            // Check if it's IDictionary<string, object>
            var propertyType = lastContractProperty.Type;
            if (propertyType.IsDictionary && propertyType.Arguments.Count == 2)
            {
                var keyType = propertyType.Arguments[0];
                var valueType = propertyType.Arguments[1];

                // Check if key is string and value is object
                if (keyType.IsFrameworkType && keyType.FrameworkType == typeof(string) &&
                    valueType.IsFrameworkType && valueType.FrameworkType == typeof(object))
                {
                    return true;
                }
            }

            return false;
        }

        private static string BuildAdditionalTypePropertiesFieldName(CSharpType additionalPropertiesValueType)
        {
            var name = additionalPropertiesValueType.Name;

            while (additionalPropertiesValueType.IsCollection)
            {
                additionalPropertiesValueType = additionalPropertiesValueType.ElementType;
                name += additionalPropertiesValueType.Name;
            }

            return $"_additional{name.ToIdentifierName()}Properties";
        }
    }
}
