// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Routing.Template;
using Microsoft.Extensions.CommandLineUtils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Swaggatherer
{
    internal class SwaggathererApplication : CommandLineApplication
    {
        public SwaggathererApplication()
        {
            Invoke = InvokeCore;

            HttpMethods = Option("-m|--method", "allow multiple endpoints with different http method", CommandOptionType.NoValue);
            Input = Option("-i", "input swagger 2.0 JSON file", CommandOptionType.MultipleValue);
            InputDirectory = Option("-d", "input directory", CommandOptionType.SingleValue);
            Output = Option("-o", "output", CommandOptionType.SingleValue);

            HelpOption("-h|--help");
        }

        public CommandOption Input { get; }

        public CommandOption InputDirectory { get; }

        // Support multiple endpoints that are distinguished only by http method.
        public CommandOption HttpMethods { get; }

        public CommandOption Output { get; }

        private int InvokeCore()
        {
            if (!Input.HasValue() && !InputDirectory.HasValue())
            {
                ShowHelp();
                return 1;
            }

            if (Input.HasValue() && InputDirectory.HasValue())
            {
                ShowHelp();
                return 1;
            }

            if (!Output.HasValue())
            {
                Output.Values.Add("Out.generated.cs");
            }

            if (InputDirectory.HasValue())
            {
                Input.Values.AddRange(Directory.EnumerateFiles(InputDirectory.Value(), "*.json", SearchOption.AllDirectories));
            }

            Console.WriteLine($"Processing {Input.Values.Count} files...");
            var entries = new List<RouteEntry>();
            for (var i = 0; i < Input.Values.Count; i++)
            {
                var input = ReadInput(Input.Values[i]);
                ParseEntries(input, entries);
            }

            // We don't yet want to support complex segments.
            for (var i = entries.Count - 1; i >= 0; i--)
            {
                if (HasComplexSegment(entries[i]))
                {
                    Out.WriteLine("Skipping route with complex segment: " + entries[i].Template.TemplateText);
                    entries.RemoveAt(i);
                }
            }

            // The data that we're provided by might be unambiguous.
            // Remove any routes that would be ambiguous in our system.
            var routesByPrecedence = new Dictionary<decimal, List<RouteEntry>>();
            for (var i = entries.Count - 1; i >= 0; i--)
            {
                var entry = entries[i];
                var precedence = RoutePrecedence.ComputeInbound(entries[i].Template);

                if (!routesByPrecedence.TryGetValue(precedence, out var matches))
                {
                    matches = new List<RouteEntry>();
                    routesByPrecedence.Add(precedence, matches);
                }

                if (IsDuplicateTemplate(entry, matches))
                {
                    Out.WriteLine("Duplicate route template: " + entries[i].Template.TemplateText);
                    entries.RemoveAt(i);
                    continue;
                }

                matches.Add(entry);
            }

            // We're not too sophisticated with how we generate parameter values, just hoping for
            // the best. For parameters we generate a segment that is the same length as the parameter name
            // but with a minimum of 5 characters to avoid collisions.
            for (var i = entries.Count - 1; i >= 0; i--)
            {
                entries[i].RequestUrl = GenerateRequestUrl(entries[i].Template);
                if (entries[i].RequestUrl == null)
                {
                    Out.WriteLine("Failed to create a request for: " + entries[i].Template.TemplateText);
                    entries.RemoveAt(i);
                    continue;
                }
            }

            Sort(entries);

            var text = Template.Execute(entries);
            File.WriteAllText(Output.Value(), text);
            return 0;
        }

        private JObject ReadInput(string input)
        {
            using (var reader = File.OpenText(input))
            {
                try
                {
                    return JObject.Load(new JsonTextReader(reader));
                }
                catch (JsonReaderException ex)
                {
                    Out.WriteLine($"Error reading: {input}");
                    Out.WriteLine(ex);
                    return new JObject();
                }
            }
        }

        private void ParseEntries(JObject input, List<RouteEntry> entries)
        {
            var basePath = "";
            if (input["basePath"] is JProperty basePathProperty)
            {
                basePath = basePathProperty.Value<string>();
            }

            if (input["paths"] is JObject paths)
            {
                foreach (var path in paths.Properties())
                {
                    foreach (var method in ((JObject)path.Value).Properties())
                    {
                        var template = basePath + path.Name;
                        var parsed = TemplateParser.Parse(template);
                        entries.Add(new RouteEntry()
                        {
                            Method = HttpMethods.HasValue() ? method.Name.ToString() : null,
                            Template = parsed,
                            Precedence = RoutePrecedence.ComputeInbound(parsed),
                        });
                    }
                }
            }
        }

        private bool HasComplexSegment(RouteEntry entry)
        {
            for (var i = 0; i < entry.Template.Segments.Count; i++)
            {
                if (!entry.Template.Segments[i].IsSimple)
                {
                    return true;
                }
            }

            return false;
        }

        private bool IsDuplicateTemplate(RouteEntry entry, List<RouteEntry> others)
        {
            for (var j = 0; j < others.Count; j++)
            {
                // This is another route with the same precedence. It is guaranteed to have the same number of segments
                // of the same kinds and in the same order. We just need to check the literals.
                var other = others[j];

                var isSame = true;
                for (var k = 0; k < entry.Template.Segments.Count; k++)
                {
                    if (!string.Equals(
                        entry.Template.Segments[k].Parts[0].Text,
                        other.Template.Segments[k].Parts[0].Text,
                        StringComparison.OrdinalIgnoreCase))
                    {
                        isSame = false;
                        break;
                    }

                    if (HttpMethods.HasValue() && 
                        !string.Equals(entry.Method, other.Method, StringComparison.OrdinalIgnoreCase))
                    {
                        isSame = false;
                        break;
                    }
                }

                if (isSame)
                {
                    return true;
                }
            }

            return false;
        }

        private static void Sort(List<RouteEntry> entries)
        {
            // We need to sort these in precedence order for the linear matchers.
            entries.Sort((x, y) =>
            {
                var comparison = RoutePrecedence.ComputeInbound(x.Template).CompareTo(RoutePrecedence.ComputeInbound(y.Template));
                if (comparison != 0)
                {
                    return comparison;
                }

                return x.Template.TemplateText.CompareTo(y.Template.TemplateText);
            });
        }

        private static string GenerateRequestUrl(RouteTemplate template)
        {
            if (template.Segments.Count == 0)
            {
                return "/";
            }

            var url = new StringBuilder();
            for (var i = 0; i < template.Segments.Count; i++)
            {
                // We don't yet handle complex segments
                var part = template.Segments[i].Parts[0];

                url.Append("/");
                url.Append(part.IsLiteral ? part.Text : GenerateParameterValue(part));
            }

            return url.ToString();
        }

        private static string GenerateParameterValue(TemplatePart part)
        {
            var text = Guid.NewGuid().ToString();
            var length = Math.Min(text.Length, Math.Max(5, part.Name.Length));
            return text.Substring(0, length);
        }
    }
}
