using AWSPowerShellGenerator.Analysis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace AWSPowerShellGenerator.ServiceConfig
{
    class XmlReportWriter
    {
        public static void SerializeReport(string folderPath, IEnumerable<ConfigModel> models)
        {
            if (!Directory.Exists(folderPath))
            {
                Directory.CreateDirectory(folderPath);
            }

            var overrides = XmlOverridesMerger.GetOverridesDescription(folderPath, out var errorMessage);

            var filename = Path.Combine(folderPath, "report.xml");
            try
            {
                var writerSettings = new XmlWriterSettings
                {
                    Encoding = new UTF8Encoding(false),
                    Indent = true,
                    IndentChars = "    "
                };

                //We only include services with errors, new operations or operations requiring a configuration update
                var configModelsToOutput = models.Where(configModel =>
                        configModel.AnalysisErrors.Any() ||
                        overrides.ContainsKey(configModel.C2jFilename) ||
                        configModel.ServiceOperationsList.Where(op => op.IsAutoConfiguring || op.AnalysisErrors.Any()).Any())
                    .ToArray();

                var doc = new XDocument();

                using (var writer = doc.CreateWriter())
                {
                    writer.WriteStartElement("Overrides");
                    writer.WriteAttributeString("xmlns", "xsd", null, "http://www.w3.org/2001/XMLSchema");
                    writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance");
                    writer.WriteAttributeString("xsi", "noNamespaceSchemaLocation", null, "https://raw.githubusercontent.com/aws/aws-tools-for-powershell/master/XmlSchemas/ConfigurationOverrides/overrides.xsd");

                    if (errorMessage != null)
                    {
                        writer.WriteComment($"ERROR - {errorMessage}");
                    }

                    foreach (var configModel in configModelsToOutput)
                    {
                        var xmlAttributeOverrides = new XmlAttributeOverrides();
                        xmlAttributeOverrides.Add(typeof(ConfigModel), new XmlAttributes() { XmlRoot = new XmlRootAttribute("Service") });
                        var serializer = new XmlSerializer(typeof(ConfigModel), xmlAttributeOverrides);
                        serializer.Serialize(writer, configModel);
                    }

                    writer.WriteEndElement(); //Overrides
                }

                foreach (var configModel in doc.Root.Elements().Zip(configModelsToOutput, (element, model) => (element, model)))
                {
                    List<XComment> serviceComments = new List<XComment>();

                    XmlOverridesMerger.OverrideDescription modelOverrides;
                    if (overrides.TryGetValue(configModel.model.C2jFilename, out modelOverrides) && modelOverrides.FileVersion != configModel.model.FileVersion)
                    {
                        AnalysisError.WrongFileVersionNumber(configModel.model);
                        modelOverrides = null;
                    }

                    serviceComments.Add(new XComment($"The current full configuration for this service is available at https://raw.githubusercontent.com/aws/aws-tools-for-powershell/master/generator/AWSPSGeneratorLib/Config/ServiceConfig/{configModel.model.C2jFilename}.xml."));

                    foreach (var error in configModel.model.AnalysisErrors)
                    {
                        serviceComments.Add(new XComment($"ERROR - {error.Message}"));
                    }

                    foreach (var serviceElement in configModel.element.Elements().ToArray())
                    {
                        switch (serviceElement.Name.LocalName)
                        {
                            case nameof(ConfigModel.C2jFilename):
                            case nameof(ConfigModel.ServiceOperations):
                            case nameof(ConfigModel.FileVersion):
                                //Preserve these elements
                                break;
                            case nameof(ConfigModel.SkipCmdletGeneration):
                            case nameof(ConfigModel.AssemblyName):
                            case nameof(ConfigModel.ServiceNounPrefix):
                            case nameof(ConfigModel.ServiceName):
                            case nameof(ConfigModel.ServiceClientInterface):
                            case nameof(ConfigModel.ServiceClient):
                            case nameof(ConfigModel.ServiceModuleGuid):
                                //Remove unless present in the override file
                                if (!(modelOverrides?.ElementNames.Contains(serviceElement.Name.LocalName) ?? false))
                                {
                                    serviceElement.Remove();
                                }
                                break;
                            default:
                                //Change into comments unless present in the override file
                                if (!(modelOverrides?.ElementNames.Contains(serviceElement.Name.LocalName) ?? false))
                                {
                                    serviceComments.Add(new XComment(serviceElement.ToString()));
                                    serviceElement.Remove();
                                }
                                break;
                        }
                    }

                    var serviceOperationsElement = configModel.element.Element("ServiceOperations");

                    serviceOperationsElement.AddBeforeSelf(serviceComments);

                    var operations = serviceOperationsElement.Elements()
                        .Join(configModel.model.ServiceOperationsList, element => element.Attribute("MethodName").Value, operation => operation.MethodName, (element, operation) => (element, operation))
                        .ToArray();

                    foreach (var operation in operations)
                    {
                        var isConfigurationOverridden = modelOverrides?.MethodNames.Contains(operation.operation.MethodName) ?? false;

                        //We only include in the report new operations (IsAutoConfiguring=true) and operations requiring configuration updated. We also retain in the report
                        //any operation that was manually configured.
                        if (operation.operation.IsAutoConfiguring ||
                            operation.operation.AnalysisErrors.Any() ||
                            isConfigurationOverridden)
                        {
                            var firstOperationChildElement = operation.element.Elements().FirstOrDefault();

                            if (operation.operation.IsAutoConfiguring)
                            {
                                operation.element.AddFirst(new XComment($"INFO - This is a new cmdlet."));
                            }
                            if (isConfigurationOverridden)
                            {
                                operation.element.AddFirst(new XComment($"INFO - The configuration of this cmdlet is being changed through overrides."));
                            }
                            foreach (var error in operation.operation.AnalysisErrors)
                            {
                                operation.element.AddFirst(new XComment($"ERROR - {error.Message}"));
                            }

                            try
                            {
                                //Excluded operations have Analyzer == null
                                if (operation.operation.Analyzer != null)
                                {
                                    if (operation.operation.Analyzer.AnalyzedParameters.Any())
                                    {
                                        var parametersComments = operation.operation.Analyzer.AnalyzedParameters.Select(PropertyChangedEventArgs => GetParameterCommentForReport(PropertyChangedEventArgs, operation.operation.Analyzer));
                                        operation.element.Add(new XComment($"INFO - Parameters:\n            {string.Join(";\n            ", parametersComments)}."));
                                    }

                                    var returnTypeComment = GetReturnTypeComment(operation.operation.Analyzer);
                                    if (returnTypeComment != null)
                                    {
                                        operation.element.Add(new XComment($"INFO - {returnTypeComment}"));
                                    }
                                }
                            }
                            catch (Exception e)
                            {
                                operation.element.AddFirst(new XComment($"ERROR - Exception while generating operation report: {e.ToString()}"));
                            }
                        }
                        else
                        {
                            operation.element.Remove();
                        }
                    }
                }

                doc.Save(filename);
            }
            catch (Exception e)
            {
                throw new IOException("Unable to serialize report to " + filename, e);
            }

            if (!string.IsNullOrEmpty(errorMessage))
            {
                throw new Exception(errorMessage);
            }
        }
        private static string GetReturnTypeComment(OperationAnalyzer operationAnalyzer)
        {
            var returnType = operationAnalyzer.ReturnType;
            if (returnType != null)
            {
                var allOutputProperties = returnType
                    .GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)
                    .ToArray();

                var returnTypeFields = "";
                if (returnType.Namespace.StartsWith("Amazon.") && allOutputProperties.Any())
                {
                    returnTypeFields = $" {{{Environment.NewLine}            {string.Join($";{Environment.NewLine}            ", allOutputProperties.Select(property => $"{OperationAnalyzer.FormatTypeName(property.PropertyType)} {property.Name}"))} }}";
                }

                return $"Operation result type: {OperationAnalyzer.FormatTypeName(returnType)}{returnTypeFields}.";
            }

            return null;
        }

        private static string GetParameterCommentForReport(Generators.SimplePropertyInfo property, OperationAnalyzer operationAnalyzer)
        {
            var comment = new StringBuilder();
            comment.Append($" {property.PropertyTypeName} {property.AnalyzedName}");
            if (property.AnalyzedName != property.CmdletParameterName)
            {
                comment.Append($" as {property.CmdletParameterName}");
                var aliases = operationAnalyzer.GetAllParameterAliases(property);
                if (aliases.Any())
                {
                    comment.Append($" (aliases: {string.Join(", ", aliases)})");
                }
            }
            if (property.IsRecursivelyRequired)
            {
                comment.Append(" [required]");
            }
            return comment.ToString();
        }


    }
}