namespace ByteBard.AsyncAPI.Readers
{
    using ByteBard.AsyncAPI.Extensions;
    using ByteBard.AsyncAPI.Models;
    using ByteBard.AsyncAPI.Readers.ParseNodes;
    using System.Collections.Generic;
    using System.Linq;

    internal static partial class AsyncApiV2Deserializer
    {
        private static FixedFieldMap<AsyncApiDocument> asyncApiFixedFields = new()
        {
            { "asyncapi", (a, n) => { a.Asyncapi = "2.6.0"; } },
            { "id", (a, n) => a.Id = n.GetScalarValue() },
            { "info", (a, n) => a.Info = LoadInfo(n) },
            { "components", (a, n) => a.Components = LoadComponents(n) }, // Load before anything else so upgrading can go smoothly.
            { "servers", (a, n) => a.Servers = n.CreateMap(LoadServer) },
            { "defaultContentType", (a, n) => a.DefaultContentType = n.GetScalarValue() },
            { "channels", (a, n) => a.Channels = n.CreateMap(key => NormalizeChannelKey(key, n), (n2, originalKey) => LoadChannel(n2, channelAddress: originalKey)) },
            { "tags", (a, n) => a.Info.Tags = n.CreateList(LoadTag) },
            { "externalDocs", (a, n) => a.Info.ExternalDocs = LoadExternalDocs(n) },
        };

        private static PatternFieldMap<AsyncApiDocument> asyncApiPatternFields = new()
        {
            { s => s.StartsWith("x-"), (a, p, n) => a.AddExtension(p, LoadExtension(p, n)) },
        };

        private static void SetSecuritySchemeScopes(ParsingContext context, AsyncApiDocument document)
        {
            if (document.Components?.SecuritySchemes == null)
            { return; }
            foreach (var securityScheme in document.Components?.SecuritySchemes)
            {
                var scopes = context.GetFromTempStorage<List<string>>(TempStorageKeys.SecuritySchemeScopes, securityScheme.Key);
                if (scopes == null)
                {
                    return;
                }

                foreach (var scope in scopes)
                {
                    securityScheme.Value.Scopes.Add(scope);
                }
            }
        }

        public static AsyncApiDocument LoadAsyncApi(RootNode rootNode)
        {
            var document = new AsyncApiDocument();

            var asyncApiNode = rootNode.GetMap();
            ParseMap(asyncApiNode, document, asyncApiFixedFields, asyncApiPatternFields);

            asyncApiNode.Context.Workspace.RegisterComponents(document);

            SetSecuritySchemeScopes(asyncApiNode.Context, document);
            SetMessages(asyncApiNode.Context, document);
            SetOperations(asyncApiNode.Context, document);
            SetParameters(asyncApiNode.Context, document);
            return document;
        }

        private static void SetParameters(ParsingContext context, AsyncApiDocument document)
        {
            var parameterReferences =
                context.GetFromTempStorage<Dictionary<AsyncApiParameter, AsyncApiJsonSchemaReference>>(TempStorageKeys
                    .ParameterSchemaReferences);

            if (parameterReferences == null)
            {
                return;
            }

            foreach (var parameterReference in parameterReferences)
            {
                var parameter = parameterReference.Key;
                var multiFormatSchema = context.Workspace.ResolveReference<AsyncApiMultiFormatSchema>(parameterReference.Value.Reference);
                var schema = multiFormatSchema.Schema.As<AsyncApiJsonSchema>();
                if (schema == null)
                {
                    continue;
                }

                if (schema.Enum.Any())
                {
                    parameter.Enum = schema.Enum.Select(e => e.GetValue<string>()).ToList();
                }

                if (schema.Default != null)
                {
                    parameter.Default = schema.Default.GetValue<string>();
                }

                if (schema.Examples.Any())
                {
                    parameter.Examples = schema.Examples.Select(e => e.GetValue<string>()).ToList();
                }
            }
        }

        private static void SetMessages(ParsingContext context, AsyncApiDocument document)
        {
            var messages = context.GetFromTempStorage<Dictionary<string, AsyncApiMessage>>(TempStorageKeys.ComponentMessages);
            if (messages == null)
            {
                return;
            }

            foreach (var message in messages)
            {
                document?.Components?.Messages.Add(message.Key, message.Value);
            }
        }

        private static void SetOperations(ParsingContext context, AsyncApiDocument document)
        {
            var operations = context.GetFromTempStorage<Dictionary<string, AsyncApiOperation>>(TempStorageKeys.Operations);
            if (operations == null)
            {
                return;
            }

            foreach (var operation in operations)
            {
                document.Operations.Add(operation);
                if (operation.Value.Channel != null)
                {
                    var messages = context.GetFromTempStorage<Dictionary<string, AsyncApiMessageReference>>(TempStorageKeys.OperationMessageReferences, operation.Value);
                    var operationChannelFragmentKey = operation.Value.Channel.Reference.Reference.Split('/').Last();
                    var channel = document.Channels.FirstOrDefault(channel => channel.Key == operationChannelFragmentKey);
                    if (channel.Value == null)
                    {
                        // it most likely came from a components channel, so the reference will be wrong.
                        // Find the channel that references this operations channel, and move the reference.
                        var correctChannelReference = document.Channels.FirstOrDefault(channel => channel.Value is AsyncApiChannelReference reference && reference.Reference.Reference.EndsWith(operationChannelFragmentKey));
                        if (correctChannelReference.Key != null)
                        {
                            operation.Value.Channel = new AsyncApiChannelReference("#/channels/" + correctChannelReference.Key);
                            channel = correctChannelReference;
                        }
                        else
                        {
                            continue;
                        }
                    }

                    if (channel.Value is AsyncApiChannelReference channelReference)
                    {
                        channelReference.Reference.Workspace = context.Workspace;
                        // Set reference address to the key, as key in v2 is the address of v3.
                        var addresses = context.GetFromTempStorage<Dictionary<string, string>>(TempStorageKeys.ChannelAddresses) ?? new Dictionary<string, string>();
                        channelReference.Address = addresses.GetValueOrDefault(channel.Key) ?? channel.Key;
                    }

                    if (messages == null)
                    {
                        continue;
                    }

                    foreach (var message in messages)
                    {
                        channel.Value.Messages.TryAdd(message.Key, message.Value);
                        operation.Value.Messages.Add(new AsyncApiMessageReference($"#/channels/{channel.Key}/messages/{message.Key}"));
                    }
                }
            }
        }
    }
}
