// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Amazon.ElasticBeanstalk;
using Amazon.ElasticBeanstalk.Model;
using AWS.Deploy.Common;
using AWS.Deploy.Common.IO;
using AWS.Deploy.Common.Recipes;
using System.Text.Json.Serialization;
namespace AWS.Deploy.Orchestration.ServiceHandlers
{
    public interface IElasticBeanstalkHandler
    {
        /// 
        /// Deployments to Windows Elastic Beanstalk envvironments require a manifest file to be included with the binaries.
        /// This method creates the manifest file if it doesn't exist, or it creates a new one.
        /// The two main settings that are updated are IIS Website and IIS App Path.
        /// 
        void SetupWindowsDeploymentManifest(Recommendation recommendation, string dotnetZipFilePath);
        /// 
        /// When deploying a self contained deployment bundle, Beanstalk needs a Procfile to tell the environment what process to start up.
        /// Check out the AWS Elastic Beanstalk developer guide for more information on Procfiles
        /// https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/dotnet-linux-procfile.html
        /// 
        void SetupProcfileForSelfContained(string dotnetZipFilePath);
        Task CreateApplicationStorageLocationAsync(string applicationName, string versionLabel, string deploymentPackage);
        Task CreateApplicationVersionAsync(string applicationName, string versionLabel, S3Location sourceBundle);
        Task UpdateEnvironmentAsync(string applicationName, string environmentName, string versionLabel, List optionSettings);
        List GetEnvironmentConfigurationSettings(Recommendation recommendation);
    }
    /// 
    /// This class represents the structure of the Windows manifest file to be included with Windows Elastic Beanstalk deployments.
    /// 
    public class ElasticBeanstalkWindowsManifest
    {
        [JsonPropertyName("manifestVersion")]
        public int ManifestVersion { get; set; } = 1;
        [JsonPropertyName("deployments")]
        public ManifestDeployments Deployments { get; set; } = new();
        public class ManifestDeployments
        {
            [JsonPropertyName("aspNetCoreWeb")]
            public List AspNetCoreWeb { get; set; } = new();
            public class AspNetCoreWebDeployments
            {
                [JsonPropertyName("name")]
                public string Name { get; set; } = "app";
                [JsonPropertyName("parameters")]
                public AspNetCoreWebParameters Parameters { get; set; } = new();
                public class AspNetCoreWebParameters
                {
                    [JsonPropertyName("appBundle")]
                    public string AppBundle { get; set; } = ".";
                    [JsonPropertyName("iisPath")]
                    public string IISPath { get; set; } = "/";
                    [JsonPropertyName("iisWebSite")]
                    public string IISWebSite { get; set; } = "Default Web Site";
                }
            }
        }
    }
    public class AWSElasticBeanstalkHandler : IElasticBeanstalkHandler
    {
        private readonly IAWSClientFactory _awsClientFactory;
        private readonly IOrchestratorInteractiveService _interactiveService;
        private readonly IFileManager _fileManager;
        private readonly IOptionSettingHandler _optionSettingHandler;
        public AWSElasticBeanstalkHandler(IAWSClientFactory awsClientFactory, IOrchestratorInteractiveService interactiveService, IFileManager fileManager, IOptionSettingHandler optionSettingHandler)
        {
            _awsClientFactory = awsClientFactory;
            _interactiveService = interactiveService;
            _fileManager = fileManager;
            _optionSettingHandler = optionSettingHandler;
        }
        private T GetOrCreateNode(object? json) where T : new()
        {
            try
            {
                return JsonSerializer.Deserialize(json?.ToString() ?? string.Empty, new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Skip }) ?? new T();
            }
            catch
            {
                return new T();
            }
        }
        /// 
        /// Deployments to Windows Elastic Beanstalk envvironments require a manifest file to be included with the binaries.
        /// This method creates the manifest file if it doesn't exist, or it creates a new one.
        /// The two main settings that are updated are IIS Website and IIS App Path.
        /// 
        public void SetupWindowsDeploymentManifest(Recommendation recommendation, string dotnetZipFilePath)
        {
            var iisWebSiteOptionSetting = _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISWebSiteOptionId);
            var iisAppPathOptionSetting = _optionSettingHandler.GetOptionSetting(recommendation, Constants.ElasticBeanstalk.IISAppPathOptionId);
            var iisWebSiteValue = _optionSettingHandler.GetOptionSettingValue(recommendation, iisWebSiteOptionSetting);
            var iisAppPathValue = _optionSettingHandler.GetOptionSettingValue(recommendation, iisAppPathOptionSetting);
            var iisWebSite = !string.IsNullOrEmpty(iisWebSiteValue) ? iisWebSiteValue : "Default Web Site";
            var iisAppPath = !string.IsNullOrEmpty(iisAppPathValue) ? iisAppPathValue : "/";
            var newManifestFile = new ElasticBeanstalkWindowsManifest();
            newManifestFile.Deployments.AspNetCoreWeb.Add(new ElasticBeanstalkWindowsManifest.ManifestDeployments.AspNetCoreWebDeployments
            {
                Parameters = new ElasticBeanstalkWindowsManifest.ManifestDeployments.AspNetCoreWebDeployments.AspNetCoreWebParameters
                {
                    IISPath = iisAppPath,
                    IISWebSite = iisWebSite
                }
            });
            using (var zipArchive = ZipFile.Open(dotnetZipFilePath, ZipArchiveMode.Update))
            {
                var zipEntry = zipArchive.GetEntry(Constants.ElasticBeanstalk.WindowsManifestName);
                var serializedManifest = JsonSerializer.Serialize(new Dictionary());
                if (zipEntry != null)
                {
                    using (var streamReader = new StreamReader(zipEntry.Open()))
                    {
                        serializedManifest = streamReader.ReadToEnd();
                    }
                }
                var jsonDoc = GetOrCreateNode>(serializedManifest);
                if (!jsonDoc.ContainsKey("manifestVersion"))
                {
                    jsonDoc["manifestVersion"] = newManifestFile.ManifestVersion;
                }
                if (jsonDoc.ContainsKey("deployments"))
                {
                    var deploymentNode = GetOrCreateNode>(jsonDoc["deployments"]);
                    if (deploymentNode.ContainsKey("aspNetCoreWeb"))
                    {
                        var aspNetCoreWebNode = GetOrCreateNode>(deploymentNode["aspNetCoreWeb"]);
                        if (aspNetCoreWebNode.Count == 0)
                        {
                            aspNetCoreWebNode.Add(newManifestFile.Deployments.AspNetCoreWeb[0]);
                        }
                        else
                        {
                            // We only need 1 entry in the 'aspNetCoreWeb' node that defines the parameters we are interested in. Typically, only 1 entry exists.
                            var aspNetCoreWebEntry = GetOrCreateNode>(JsonSerializer.Serialize(aspNetCoreWebNode[0]));
                            var nameValue = aspNetCoreWebEntry.ContainsKey("name") ? aspNetCoreWebEntry["name"].ToString() : string.Empty;
                            aspNetCoreWebEntry["name"] = !string.IsNullOrEmpty(nameValue) ? nameValue : newManifestFile.Deployments.AspNetCoreWeb[0].Name;
                            if (aspNetCoreWebEntry.ContainsKey("parameters"))
                            {
                                var parametersNode = GetOrCreateNode>(aspNetCoreWebEntry["parameters"]);
                                parametersNode["appBundle"] = ".";
                                parametersNode["iisPath"] = iisAppPath;
                                parametersNode["iisWebSite"] = iisWebSite;
                                aspNetCoreWebEntry["parameters"] = parametersNode;
                            }
                            else
                            {
                                aspNetCoreWebEntry["parameters"] = newManifestFile.Deployments.AspNetCoreWeb[0].Parameters;
                            }
                            aspNetCoreWebNode[0] = aspNetCoreWebEntry;
                        }
                        deploymentNode["aspNetCoreWeb"] = aspNetCoreWebNode;
                    }
                    else
                    {
                        deploymentNode["aspNetCoreWeb"] = newManifestFile.Deployments.AspNetCoreWeb;
                    }
                    jsonDoc["deployments"] = deploymentNode;
                }
                else
                {
                    jsonDoc["deployments"] = newManifestFile.Deployments;
                }
                using (var jsonStream = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(jsonDoc, new JsonSerializerOptions { WriteIndented = true })))
                {
                    zipEntry ??= zipArchive.CreateEntry(Constants.ElasticBeanstalk.WindowsManifestName);
                    using var zipEntryStream = zipEntry.Open();
                    jsonStream.Position = 0;
                    jsonStream.CopyTo(zipEntryStream);
                }
            }
        }
        /// 
        /// When deploying a self contained deployment bundle, Beanstalk needs a Procfile to tell the environment what process to start up.
        /// Check out the AWS Elastic Beanstalk developer guide for more information on Procfiles
        /// https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/dotnet-linux-procfile.html
        ///
        /// This code is a copy of the code in the AspNetAppElasticBeanstalkLinux CDK recipe definition. Any changes to this method
        /// should be made into that version as well.
        /// 
        /// 
        public void SetupProcfileForSelfContained(string dotnetZipFilePath)
        {
            const string RUNTIME_CONFIG_SUFFIX = ".runtimeconfig.json";
            const string PROCFILE_NAME = "Procfile";
            string runtimeConfigFilename;
            string runtimeConfigJson;
            using (var zipArchive = ZipFile.Open(dotnetZipFilePath, ZipArchiveMode.Read))
            {
                // Skip Procfile setup if one already exists.
                if (zipArchive.GetEntry(PROCFILE_NAME) != null)
                {
                    return;
                }
                var runtimeConfigEntry = zipArchive.Entries.FirstOrDefault(x => x.Name.EndsWith(RUNTIME_CONFIG_SUFFIX));
                if (runtimeConfigEntry == null)
                {
                    return;
                }
                runtimeConfigFilename = runtimeConfigEntry.Name;
                using var stream = runtimeConfigEntry.Open();
                runtimeConfigJson = new StreamReader(stream).ReadToEnd();
            }
            var runtimeConfigDoc = JsonDocument.Parse(runtimeConfigJson);
            if (!runtimeConfigDoc.RootElement.TryGetProperty("runtimeOptions", out var runtimeOptionsNode))
            {
                return;
            }
            // If there are includedFrameworks then the zip file is a self contained deployment bundle.
            if (!runtimeOptionsNode.TryGetProperty("includedFrameworks", out _))
            {
                return;
            }
            var executableName = runtimeConfigFilename.Substring(0, runtimeConfigFilename.Length - RUNTIME_CONFIG_SUFFIX.Length);
            var procCommand = $"web: ./{executableName}";
            using (var zipArchive = ZipFile.Open(dotnetZipFilePath, ZipArchiveMode.Update))
            {
                var procfileEntry = zipArchive.CreateEntry(PROCFILE_NAME);
                using var zipEntryStream = procfileEntry.Open();
                zipEntryStream.Write(System.Text.UTF8Encoding.UTF8.GetBytes(procCommand));
            }
        }
        public async Task CreateApplicationStorageLocationAsync(string applicationName, string versionLabel, string deploymentPackage)
        {
            string bucketName;
            try
            {
                var ebClient = _awsClientFactory.GetAWSClient();
                bucketName = (await ebClient.CreateStorageLocationAsync()).S3Bucket;
            }
            catch (Exception e)
            {
                throw new ElasticBeanstalkException(DeployToolErrorCode.FailedToCreateElasticBeanstalkStorageLocation, "An error occured while creating the Elastic Beanstalk storage location", e);
            }
            var key = string.Format("{0}/AWSDeploymentArchive_{0}_{1}{2}",
                                        applicationName.Replace(' ', '-'),
                                        versionLabel.Replace(' ', '-'),
                                        _fileManager.GetExtension(deploymentPackage));
            return new S3Location { S3Bucket = bucketName, S3Key = key };
        }
        public async Task CreateApplicationVersionAsync(string applicationName, string versionLabel, S3Location sourceBundle)
        {
            _interactiveService.LogInfoMessage("Creating new application version: " + versionLabel);
            try
            {
                var ebClient = _awsClientFactory.GetAWSClient();
                var response = await ebClient.CreateApplicationVersionAsync(new CreateApplicationVersionRequest
                {
                    ApplicationName = applicationName,
                    VersionLabel = versionLabel,
                    SourceBundle = sourceBundle
                });
                return response;
            }
            catch (Exception e)
            {
                throw new ElasticBeanstalkException(DeployToolErrorCode.FailedToCreateElasticBeanstalkApplicationVersion, "An error occured while creating the Elastic Beanstalk application version", e);
            }
        }
        public List GetEnvironmentConfigurationSettings(Recommendation recommendation)
        {
            var additionalSettings = new List();
            List<(string OptionSettingId, string OptionSettingNameSpace, string OptionSettingName)> tupleList;
            switch (recommendation.Recipe.Id)
            {
                case Constants.RecipeIdentifier.EXISTING_BEANSTALK_ENVIRONMENT_RECIPE_ID:
                    tupleList = Constants.ElasticBeanstalk.OptionSettingQueryList;
                    break;
                case Constants.RecipeIdentifier.EXISTING_BEANSTALK_WINDOWS_ENVIRONMENT_RECIPE_ID:
                    tupleList = Constants.ElasticBeanstalk.WindowsOptionSettingQueryList;
                    break;
                default:
                    throw new InvalidOperationException($"The recipe '{recommendation.Recipe.Id}' is not supported.");
            };
            foreach (var tuple in tupleList)
            {
                var optionSetting = _optionSettingHandler.GetOptionSetting(recommendation, tuple.OptionSettingId);
                if (!optionSetting.Updatable)
                    continue;
                var optionSettingValue = optionSetting.GetValue(new Dictionary());
                additionalSettings.Add(new ConfigurationOptionSetting
                {
                    Namespace = tuple.OptionSettingNameSpace,
                    OptionName = tuple.OptionSettingName,
                    Value = optionSettingValue
                });
            }
            return additionalSettings;
        }
        public async Task UpdateEnvironmentAsync(string applicationName, string environmentName, string versionLabel, List optionSettings)
        {
            _interactiveService.LogInfoMessage("Getting latest environment event date before update");
            var startingEventDate = await GetLatestEventDateAsync(applicationName, environmentName);
            _interactiveService.LogInfoMessage($"Updating environment {environmentName} to new application version {versionLabel}");
            var updateRequest = new UpdateEnvironmentRequest
            {
                ApplicationName = applicationName,
                EnvironmentName = environmentName,
                VersionLabel = versionLabel,
                OptionSettings = optionSettings
            };
            try
            {
                var ebClient = _awsClientFactory.GetAWSClient();
                var updateEnvironmentResponse = await ebClient.UpdateEnvironmentAsync(updateRequest);
                return await WaitForEnvironmentUpdateCompletion(applicationName, environmentName, startingEventDate);
            }
            catch (Exception e)
            {
                throw new ElasticBeanstalkException(DeployToolErrorCode.FailedToUpdateElasticBeanstalkEnvironment, "An error occured while updating the Elastic Beanstalk environment", e);
            }
        }
        private async Task GetLatestEventDateAsync(string applicationName, string environmentName)
        {
            var request = new DescribeEventsRequest
            {
                ApplicationName = applicationName,
                EnvironmentName = environmentName
            };
            var ebClient = _awsClientFactory.GetAWSClient();
            var response = await ebClient.DescribeEventsAsync(request);
            if (response.Events.Count == 0)
                return DateTime.Now;
            return response.Events.First().EventDate;
        }
        private async Task WaitForEnvironmentUpdateCompletion(string applicationName, string environmentName, DateTime startingEventDate)
        {
            _interactiveService.LogInfoMessage("Waiting for environment update to complete");
            var success = true;
            var environment = new EnvironmentDescription();
            var lastPrintedEventDate = startingEventDate;
            var requestEvents = new DescribeEventsRequest
            {
                ApplicationName = applicationName,
                EnvironmentName = environmentName
            };
            var requestEnvironment = new DescribeEnvironmentsRequest
            {
                ApplicationName = applicationName,
                EnvironmentNames = new List { environmentName }
            };
            var ebClient = _awsClientFactory.GetAWSClient();
            do
            {
                Thread.Sleep(5000);
                var responseEnvironments = await ebClient.DescribeEnvironmentsAsync(requestEnvironment);
                if (responseEnvironments.Environments.Count == 0)
                    throw new AWSResourceNotFoundException(DeployToolErrorCode.BeanstalkEnvironmentDoesNotExist, $"Failed to find environment {environmentName} belonging to application {applicationName}");
                environment = responseEnvironments.Environments[0];
                requestEvents.StartTimeUtc = lastPrintedEventDate;
                var responseEvents = await ebClient.DescribeEventsAsync(requestEvents);
                if (responseEvents.Events.Any())
                {
                    for (var i = responseEvents.Events.Count - 1; i >= 0; i--)
                    {
                        var evnt = responseEvents.Events[i];
                        if (evnt.EventDate <= lastPrintedEventDate)
                            continue;
                        _interactiveService.LogInfoMessage(evnt.EventDate.ToLocalTime() + "    " + evnt.Severity + "    " + evnt.Message);
                        if (evnt.Severity == EventSeverity.ERROR || evnt.Severity == EventSeverity.FATAL)
                        {
                            success = false;
                        }
                    }
                    lastPrintedEventDate = responseEvents.Events[0].EventDate;
                }
            } while (environment.Status == EnvironmentStatus.Launching || environment.Status == EnvironmentStatus.Updating);
            return success;
        }
    }
}