using Katteker.Exceptions;
using Katteker.Types;
using Katteker.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
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 readonly string _applicationName;
        private readonly string _packageDir;
        private readonly string _rootAppDirectory;
        private KattekerConfig _kattekerConfig = new();

        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 => _kattekerConfig.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)
        {
            var appName = applicationName ?? Utility.GetApplicationName();
            TryReadConfigFile(out var config);
            urlOrPath ??= config.Publish;
            var rootAppDirectory = Path.Combine(rootDirectory ?? Utility.GetLocalAppDataDirectory(),
                appName + config.Channel);
            return new UpdateManager(urlOrPath, appName, rootAppDirectory) {_kattekerConfig = config};
        }

        /// <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)
        {
            try
            {
                manager = Create(urlOrPath, applicationName, rootDirectory);
                return true;
            }
            catch (Exception)
            {
                manager = Create(null, applicationName, rootDirectory);
                return false;
            }
        }

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

        private async Task<IReadOnlyList<ReleaseEntry>> CheckForUpdateImplAsync()
        {
#if NETSTANDARD
            if (UrlOrPath is null) return Array.Empty<ReleaseEntry>();
#else
            if (UrlOrPath is null) return new ReleaseEntry[0];
#endif
            var releases = Utility.IsWebUrl(UrlOrPath)
                ? await DownloadIndexAsync(UrlOrPath).ConfigureAwait(false)
                : GetFromFilesystem(UrlOrPath);
            var updateInfo = new UpdateInfo(_applicationName + _kattekerConfig.Channel, releases);
            return updateInfo.ReleasesToApply;
        }

        /// <summary>
        /// Restart the application.
        /// </summary>
        /// <param name="exeToStart"></param>
        /// <param name="arguments"></param>
        /// <exception cref="KattekerUpdateException">Found no executable to start.</exception>
        /// <exception cref="FileNotFoundException">Program not found.</exception>
        public void RestartApp(string? exeToStart = null, string? arguments = null)
        {
            //exeToStart ??= Path.GetFileName(Assembly.GetEntryAssembly()?.Location)
            //               ?? throw new KattekerUpdateException("Found no executable to start.");
            //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 CheckForUpdatesAsync().ConfigureAwait(false)).ToArray();
                if (updateEntries.Length == 0) return false;
                var update = updateEntries.Last();
                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;
            string content;
            using (var wc = new WebClient())
            {
                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(urlOrPath);

        private static bool TryReadConfigFile(out KattekerConfig config)
        {
            var configPath = Path.Combine(BaseDir, Constants.KattekerConfig);
            if (!File.Exists(configPath) || !KattekerConfig.TryReadFromFile(configPath, out var configFromFile))
            {
                config = new KattekerConfig();
                return false;
            }

            config = configFromFile!;
            return true;

        }

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

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

        private static void DeleteAllFiles(string packageDir)
        {
            try
            {
                foreach (var enumerateFile in Directory.EnumerateFiles(packageDir))
                {
                    try
                    {
                        File.Delete(enumerateFile);
                    }
                    catch
                    {
                        // ignore
                    }
                }
            }
            catch
            {
                // ignore
            }
        }

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

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

                using var updater = new Process {StartInfo = new ProcessStartInfo(targetFile)};
                updater.Start();

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