using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Reflection;

namespace ServerManagerTool.Updater
{
    public class Program
    {
        private static string[] ApplicationArgs
        {
            get;
            set;
        }

        private static string DownloadUrl
        {
            get;
            set;
        }

        private static string Prefix
        {
            get;
            set;
        }

        public static void Main(string[] args)
        {
            Console.Title = "Server Manager Updater";

            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

            var updaterArgs = args;
            ApplicationArgs = null;
            DownloadUrl = null;
            Prefix = null;

            try
            {
                // check if the updater is already running
                if (ProcessUtils.IsAlreadyRunning())
                    throw new Exception("The updater is already running.");

                var process = Validate(updaterArgs);
                CloseApplication(process);
                process = null;

                Update();
                RestartApplication();

                Environment.ExitCode = 0;
            }
            catch (Exception ex)
            {
                OutputError(ex.Message, ex.StackTrace);
                Environment.ExitCode = 1;

                OutputMessage("Press any key to continue.");
                Console.ReadKey(true);
            }
        }

        private static void CloseApplication(Process process)
        {
            OutputMessage("Closing application...");

            if (process == null || process.HasExited)
                throw new Exception("The process is not associated with a running application.");

            // send a close to the application
            process.CloseMainWindow();

            // wait until the application has been closed
            DateTime start = DateTime.Now;
            while (!process.HasExited)
            {
                if (start.AddMinutes(5) < DateTime.Now)
                {
                    process.Kill();
                    break;
                }
            }

            if (!process.HasExited)
                throw new Exception("The application could not be closed.");
        }

        private static void OutputError(string error, string stackTrace)
        {
            Console.WriteLine($"ERROR: {error}");
        }

        private static void OutputMessage(string message)
        {
            Console.WriteLine(message);
        }

        private static void RestartApplication()
        {
            OutputMessage("Restarting application...");

            var file = ApplicationArgs[0];
            var arguments = string.Empty;
            if (ApplicationArgs.Length > 1)
            {
                string[] argumentArray = new string[ApplicationArgs.Length - 1];
                Array.Copy(ApplicationArgs, 1, argumentArray, 0, argumentArray.Length);
                arguments = String.Join(" ", argumentArray);
            }

            ProcessStartInfo info = new ProcessStartInfo()
            {
                FileName = file.AsQuoted(),
                Arguments = arguments,
                UseShellExecute = true,
                CreateNoWindow = true,
            };

            var process = Process.Start(info);
            if (process == null)
                throw new Exception("Could not restart application.");
        }

        private static void Update()
        {
            if (ApplicationArgs == null || ApplicationArgs.Length == 0)
                throw new ArgumentNullException(nameof(ApplicationArgs), "The application args cannot be null or empty.");
            if (string.IsNullOrWhiteSpace(DownloadUrl))
                throw new ArgumentNullException(nameof(DownloadUrl), "The download url cannot be null or empty.");
            if (string.IsNullOrWhiteSpace(Prefix))
                throw new ArgumentNullException(nameof(Prefix), "The prefix cannot be null or empty.");

            OutputMessage("Starting upgrade process...");

            var currentInstallPath = Path.GetDirectoryName(ApplicationArgs[0]);
            var upgradeStagingPath = Path.GetTempPath();
            var applicationZip = Path.Combine(upgradeStagingPath, $"{Prefix}Latest.zip");
            var extractPath = Path.Combine(upgradeStagingPath, $"{Prefix}Latest");

            // Download the latest version
            using (var client = new WebClient())
            {
                OutputMessage("Downloading latest version file...");
#if DEBUG
                OutputMessage($"Url: {DownloadUrl}");
                OutputMessage($"Zip: {applicationZip}");
#endif
                client.DownloadFile(DownloadUrl, applicationZip);
            }

            // Unblock the downloaded file
            OutputMessage("Preparing downloaded file...");
#if DEBUG
            OutputMessage($"Zip: {applicationZip}");
#endif
            IOUtils.Unblock(applicationZip);

            try
            {
                // Delete the old extraction directory
                if (Directory.Exists(extractPath))
                {
#if DEBUG
                    OutputMessage("Deleting extract directory...");
                    OutputMessage($"Dir: {extractPath}");
#endif
                    Directory.Delete(extractPath, true);
                }
            }
            catch { }

            // Extract latest version to extraction directory
            OutputMessage("Extracting latest version to staging area...");
#if DEBUG
            OutputMessage($"Zip: {applicationZip}");
            OutputMessage($"Dir: {extractPath}");
#endif
            ZipFile.ExtractToDirectory(applicationZip, extractPath);

            // build a list of files to be updated
            var latestFiles = Directory.GetFiles(extractPath, "*.*", SearchOption.AllDirectories).ToList();

            // build a list of files to be removed
            var deleteFiles = Directory.GetFiles(currentInstallPath, "*.*", SearchOption.AllDirectories).ToList();
            foreach (var latestFile in latestFiles)
            {
                var file = Path.Combine(currentInstallPath, latestFile.Replace($"{extractPath}\\", ""));
                if (deleteFiles.Contains(file))
                    deleteFiles.Remove(file);
            }

            OutputMessage("Upgrading application...");

            // delete the obsolete files
            foreach (var deleteFile in deleteFiles)
            {
                if (File.Exists(deleteFile))
                {
#if DEBUG
                    OutputMessage($"Delete: {deleteFile}");
#endif
                    //File.Delete(deleteFile);
                }
            }

            // remove the updater from the files to be updated
            var assemblyFile = Assembly.GetEntryAssembly().Location;
            var assemblyPath = Path.GetDirectoryName(assemblyFile);

            var updaterFile = Path.Combine(extractPath, assemblyFile.Replace($"{assemblyPath}\\", ""));
#if DEBUG
                OutputMessage($"Checking: {updaterFile}");
#endif
            if (latestFiles.Contains(updaterFile))
                latestFiles.Remove(updaterFile);

            var updaterConfigFile = Path.Combine(updaterFile, ".config");
#if DEBUG
            OutputMessage($"Checking: {updaterConfigFile}");
#endif
            if (latestFiles.Contains(updaterConfigFile))
                latestFiles.Remove(updaterConfigFile);

            // Replace the current installation
            foreach (var latestFile in latestFiles)
            {
                var file = Path.Combine(currentInstallPath, latestFile.Replace($"{extractPath}\\", ""));
                if (File.Exists(latestFile))
                {
#if DEBUG
                    OutputMessage($"Copy: {latestFile} to {file}");
#endif
                    // check if the directory exists
                    var filePath = Path.GetDirectoryName(file);
                    if (!Directory.Exists(filePath))
                        Directory.CreateDirectory(filePath);

                    // copy over the file
                    File.Copy(latestFile, file, true);
                }
            }
        }

        private static Process Validate(string[] args)
        {
            OutputMessage("Validating update...");

            // argument format - PID, DownloadUrl, Prefix
            if (args == null)
                throw new ArgumentNullException(nameof(args), "The arguments cannot be null or empty");
            if (args == null || args.Length != 3)
                throw new ArgumentOutOfRangeException(nameof(args), "The arguments do not contain valid information.");

            // check if the passed pid is valid
            if (!int.TryParse(args[0], out int pid))
                throw new InvalidCastException("The process id value is not valid.");

            DownloadUrl = args[1];
            if (string.IsNullOrWhiteSpace(DownloadUrl))
                throw new ArgumentNullException(nameof(DownloadUrl), "The download url cannot be null or empty.");

            Prefix = args[2];
            if (string.IsNullOrWhiteSpace(Prefix))
                throw new ArgumentNullException(nameof(Prefix), "The prefix cannot be null or empty.");

            // get the process associated with the pid
            var process = ProcessUtils.GetProcess(pid);
            if (process == null)
                throw new Exception("The process id value is not associated with a running application.");

            // get a list of the running processes with the same name and file location
            var executablePath = ProcessUtils.GetMainModuleFilepath(pid);
            var processes = ProcessUtils.GetProcesses(process.ProcessName, executablePath);

            // check if there is more than one instance of the application running
            if (!processes.HasOne())
                throw new Exception("The application to be updated has more than one instance running.");

            // get the command line of the process
            var commandLine = ProcessUtils.GetCommandLineForProcess(pid);
            ApplicationArgs = ProcessUtils.CommandLineToArgs(commandLine);

            if (ApplicationArgs == null || ApplicationArgs.Length == 0)
                throw new ArgumentNullException(nameof(ApplicationArgs), "The application args cannot be null or empty.");

            return process;
        }
    }
}
