using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace Katteker
{
    /// <summary>
    /// Update manager, which handles the updates.
    /// </summary>
    public class UpdateManager
    {
        private static readonly string BaseDir = AppDomain.CurrentDomain.BaseDirectory;
        private static KattekerConfig _config;
        private readonly string _applicationName;
        private readonly string _packageDir;
        private readonly string _rootAppDirectory;
        private Releases _releases;

        private UpdateManager(string urlOrPath, string applicationName, string rootDirectory)
        {
            UrlOrPath = urlOrPath;
            _applicationName = applicationName;
            _rootAppDirectory = rootDirectory;
            var packageDir = Path.Combine(rootDirectory, Constants.PackageFolder);
            if (!Directory.Exists(packageDir)) Directory.CreateDirectory(packageDir);
            _packageDir = packageDir;
        }

        /// <summary>
        /// Filename of the changelog.
        /// </summary>
        public string ChangelogFilename => _config.Changelog;

        /// <summary>
        /// Url or path where the update files are located.
        /// </summary>
        public string UrlOrPath { get; }

        /// <summary>
        /// Create the update manager.
        /// </summary>
        /// <param name="urlOrPath">path to the publishing directory</param>
        /// <param name="applicationName">name of the application to update.</param>
        /// <param name="rootDirectory">root directory.</param>
        /// <returns>the update manager.</returns>
        public static UpdateManager Create(string urlOrPath = null, string applicationName = null,
            string rootDirectory = null)
        {
            _config = ReadConfigFile();
            urlOrPath = urlOrPath ?? _config.Publish;
            var appName = applicationName ?? Utility.GetApplicationName();
            var channel = string.IsNullOrWhiteSpace(_config.Channel) ? string.Empty : $"_{_config.Channel}";
            var rootAppDirectory = Path.Combine(rootDirectory ?? Utility.GetLocalAppDataDirectory(), appName + channel);
            return new UpdateManager(urlOrPath, appName, rootAppDirectory);
        }

        /// <summary>
        /// Try to create the update manager.
        /// </summary>
        /// <param name="manager">update manager</param>
        /// <param name="urlOrPath">path to the publishing directory</param>
        /// <param name="applicationName">name of the application to update.</param>
        /// <param name="rootDirectory">root directory.</param>
        /// <returns>true if the creation success, false otherwise.</returns>
        public static bool TryCreate(out UpdateManager manager, string urlOrPath = null, string applicationName = null,
            string rootDirectory = null)
        {
            manager = default;
            try
            {
                manager = Create(urlOrPath, applicationName, rootDirectory);
                return true;
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Check for updates
        /// </summary>
        /// <returns></returns>
        public async Task<IEnumerable<ReleaseEntry>> CheckForUpdateAsync()
        {
            try
            {
                return await CheckForUpdateImplAsync().ConfigureAwait(false);
            }
            catch (Exception e)
            {
                throw new KattekerUpdateException("Error at checking for available update.", e);
            }
        }

        private async Task<IEnumerable<ReleaseEntry>> CheckForUpdateImplAsync()
        {
            _releases = Utility.IsWebUrl(UrlOrPath)
                ? await DownloadIndexAsync(UrlOrPath).ConfigureAwait(false)
                : GetFromFilesystem(UrlOrPath);
            var updateInfo = new UpdateInfo(_applicationName, _releases);
            return updateInfo.ReleasesToApply;
        }

        /// <summary>
        /// Restart the application.
        /// </summary>
        /// <param name="exeToStart"></param>
        /// <param name="arguments"></param>
        public void RestartApp(string exeToStart = null, string arguments = null)
        {
            //TODO DOTNET Core compatibility

            exeToStart = exeToStart ?? Path.GetFileName(Assembly.GetEntryAssembly()?.Location);
            exeToStart = Path.ChangeExtension(exeToStart, "exe");
            var program = Path.Combine(_rootAppDirectory, exeToStart);
            if (!File.Exists(program)) throw new FileNotFoundException(program);
            Process.Start(program, arguments);
            Thread.Sleep(500);
            Environment.Exit(0);
        }

        /// <summary>
        /// Update application.
        /// </summary>
        /// <param name="progress">The updating process.</param>
        /// <returns></returns>
        public async Task<bool> UpdateAppAsync(IProgress<int> progress = null)
        {
            try
            {
                progress?.Report(0);
                var updateEntries = (await CheckForUpdateAsync().ConfigureAwait(false)).ToArray();
                var update = updateEntries.LastOrDefault();
                if (update == null) return false;
                progress?.Report(30);
                return await UpdateAppImplAsync(update, progress).ConfigureAwait(false);
            }
            catch (Exception e)
            {
                throw new KattekerUpdateException("Error at updating application.", e);
            }
        }

        private static async Task<Releases> DownloadIndexAsync(string urlOrPath)
        {
            var url = urlOrPath.TrimEnd('/');
            url += "/" + Constants.Release;
            using (var wc = new WebClient())
            {
                var content = await wc.DownloadStringTaskAsync(url).ConfigureAwait(false);
                var lines = content.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries);
                return new Releases(lines);
            }
        }

        private static Releases GetFromFilesystem(string urlOrPath) => new Releases(urlOrPath);

        private static KattekerConfig ReadConfigFile()
        {
            var configPath = Path.Combine(BaseDir, Constants.KattekerConfig);
            if (!File.Exists(configPath)) throw new FileNotFoundException("Configuration file not found.", configPath);
            return KattekerConfig.ReadFromFile(configPath);
        }

        private static bool VerifyFileChecksum(string targetFile, string lastEntrySha1)
        {
            var hash = Utility.ComputeFileHash(targetFile);
            return lastEntrySha1.Equals(hash);
        }

        private Task KillAppStubAsync()
        {
            foreach (var process in Process.GetProcessesByName(_applicationName))
            {
                var path = Path.GetDirectoryName(process.MainModule?.FileName);
                if (!_rootAppDirectory.Equals(path)) continue;
                process.Kill();
                return Task.Delay(100);
            }

            return Task.FromResult(true);
        }

        private async Task PutFileInPackageFolderAsync(string filename)
        {
            var targetFile = Path.Combine(_packageDir, filename);
            File.Delete(targetFile);
            if (Utility.IsWebUrl(UrlOrPath))
            {
                var url = UrlOrPath.TrimEnd('/');
                url += "/" + filename;
                using (var wc = new WebClient())
                {
                   await wc.DownloadFileTaskAsync(new Uri(url), targetFile).ConfigureAwait(false);
                }
            }
            else
            {
                File.Copy(Path.Combine(UrlOrPath, filename), targetFile);
            }
        }

        private async Task<bool> UpdateAppImplAsync(ReleaseEntry lastEntry, IProgress<int> progress = default)
        {
            try
            {
                if (lastEntry == null) throw new ArgumentNullException(nameof(lastEntry));
                var targetFile = Path.Combine(_packageDir, lastEntry.Filename);
                //download file.
                await PutFileInPackageFolderAsync(lastEntry.Filename).ConfigureAwait(false);
                progress?.Report(60);

                if (!VerifyFileChecksum(targetFile, lastEntry.SHA1)) throw new FileLoadException("Checksum missmatch.");
                progress?.Report(70);

                await KillAppStubAsync().ConfigureAwait(false);
                progress?.Report(80);

                using (var updater = new Process())
                {
                    updater.StartInfo = new ProcessStartInfo(targetFile, "/S");
                    updater.Start();
                    updater.WaitForExit();
                }

                progress?.Report(100);
                return true;
            }
            catch
            {
                return false;
            }
        }
    }
}