using Insight.Domain.Models;
using Insight.Updater.Constants;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.IO.Compression;
using System.Net.Http.Json;
using System.Runtime.Versioning;
using System.ServiceProcess;
using System.Text.Json;

namespace Insight.Updater.Services
{
    public class UpdateService : BackgroundService
    {
        private readonly Uri _uri;

        private readonly HttpClient _httpClient;
        private readonly ILogger<UpdateService> _logger;

        public UpdateService(HttpClient httpClient, IConfiguration configuration, ILogger<UpdateService> logger)
        {
            _httpClient = httpClient;
            _uri = configuration.GetValue<Uri?>("api") ?? throw new Exception($"api value not set (appsettings)");
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken cancellationToken)
        {
            _logger.LogTrace("ExecuteAsync");

            while (cancellationToken.IsCancellationRequested is false)
            {
                try
                {
                    UpdateResult? result = null;

                    if (OperatingSystem.IsWindows()) result = await WindowsUpdateAsync(cancellationToken);
                    if (OperatingSystem.IsLinux()) result = await LinuxUpdateAsync(cancellationToken);

                    _logger.LogInformation("Update Result: {result}", result?.Success);
                    if (result?.UpdateErrors is not null)
                    {
                        _logger.LogError("Update Errors: {errors}", string.Concat(result?.UpdateErrors));
                    }
                }
                catch (OperationCanceledException) { }
                catch (Exception ex) // may inform via client / api about errors
                {
                    _logger.LogError("{ex}", ex.Message);
                }
                finally
                {
                    await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
                }
            }
        }

        [SupportedOSPlatform("windows")]
        private async ValueTask<UpdateResult> WindowsUpdateAsync(CancellationToken cancellationToken)
        {
            return await Windows.Service.UpdateAsync(
                _httpClient,
                Deploy.GetUpdateHref(_uri, Deploy.Agent.Name),
                Deploy.GetAppExecutable(Deploy.Agent.Name),
                Deploy.Agent.ServiceName,
                cancellationToken);
        }

        [SupportedOSPlatform("linux")]
        private ValueTask<UpdateResult> LinuxUpdateAsync(CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        [SupportedOSPlatform("windows")]
        private static class Windows
        {
            public static class Service
            {
                private static bool ServiceExistence(string serviceName)
                {
                    try
                    {
                        if (ServiceController.GetServices().Any(s => s.ServiceName.Equals(serviceName, StringComparison.InvariantCultureIgnoreCase))) return true;
                        return false;
                    }
                    catch (Exception) { }

                    return false;
                }

                private static bool SetServiceState(string app, ServiceControllerStatus status, TimeSpan timeout)
                {
                    try
                    {
                        using var sc = ServiceController.GetServices().FirstOrDefault(s => s.ServiceName.Equals(app, StringComparison.InvariantCultureIgnoreCase));
                        if (sc is null) return false;

                        if (sc.Status != status)
                        {
                            switch (status)
                            {
                                case ServiceControllerStatus.Running:
                                    sc.Start();
                                    break;

                                case ServiceControllerStatus.Stopped:
                                    sc.Stop();
                                    break;
                            }

                            sc.WaitForStatus(status, timeout);
                        }

                        return true;
                    }
                    catch (Exception) { }

                    return false;
                }

                public static async ValueTask<UpdateResult> UpdateAsync(HttpClient httpClient, Uri api, FileInfo bin, string serviceName, CancellationToken cancellationToken)
                {
                    var result = new UpdateResult
                    {
                        Api = api?.ToString(),
                        SourceDirectory = bin.Directory?.FullName,
                        App = bin.Name,
                        ServiceName = serviceName
                    };

                    try
                    {
                        // check if service exists
                        if (ServiceExistence(serviceName) is false)
                        {
                            result.UpdateErrors.Add("service not found");
                            return result;
                        }

                        // get service update details
                        var response = await httpClient.GetFromJsonAsync<UpdateResponse>(api, cancellationToken);
                        if (response is null)
                        {
                            result.ApiErrors.Add("not available / response null");
                            return result;
                        }

                        result.ApiAvailable = true;

                        // check if local binary exists
                        if (bin is null)
                        {
                            result.UpdateErrors.Add("source binary not found");
                            return result;
                        }

                        // get local file binary version
                        if (FileVersionInfo.GetVersionInfo(bin.FullName).FileVersion is not string binVersionString)
                        {
                            result.UpdateErrors.Add("source binary fileversion not valid");
                            return result;
                        }

                        // compare local against update version, skip lower or equal update version
                        var actualVersion = Version.Parse(binVersionString);
                        if (actualVersion >= response.Version)
                        {
                            result.Success = true;
                            return result;
                        }
                        else
                        {
                            result.UpdateAvailable = true;
                        }

                        // get update file (bytes) to memory
                        using var update = await httpClient.GetAsync(response.Uri, cancellationToken);
                        if (update is null)
                        {
                            result.ApiErrors.Add("update source not available");
                            return result;
                        }

                        // stop service
                        if (SetServiceState(serviceName, ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(10)) is false)
                        {
                            result.UpdateErrors.Add("service control failed / failed to stop service");
                            return result;
                        }

                        // read update archive to temp (overwrite)
                        var temp = Directory.CreateTempSubdirectory();
                        var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip");

                        await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken);

                        // extract update archive from temp to app dir (overwrite)
                        ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true);

                        // delete temp folder
                        if (temp.Exists) temp.Delete(true);

                        // start updateds service
                        if (SetServiceState(serviceName, ServiceControllerStatus.Running, TimeSpan.FromSeconds(10)) is false)
                        {
                            result.UpdateErrors.Add("service control failed / failed to start service");
                            return result;
                        }

                        result.Success = true;
                    }
                    catch (Exception ex)
                    {
                        result.UpdateErrors.Add(ex.Message);
                    }

                    return result;
                }
            }

            public static class Process
            {
                public static bool IsRunning(FileInfo bin)
                {
                    if (bin.Exists is false) return false;

                    var matched = System.Diagnostics.Process.GetProcessesByName(bin.FullName);

                    if (matched is null || matched.Any() is false) return false;

                    if (matched.Any(p =>
                        p.MainModule is not null &&
                        p.MainModule.FileName is not null &&
                        p.MainModule.FileName.Equals(bin.FullName,
                        StringComparison.InvariantCultureIgnoreCase))) return true;

                    return false;
                }

                public static bool Start(FileInfo binary)
                {
                    try
                    {
                        if (IsRunning(binary) is false) return false;

                        using var process = System.Diagnostics.Process.Start(binary.FullName);
                        return true;
                    }
                    catch (Exception) { }

                    return false;
                }

                public static bool Stop(FileInfo bin, TimeSpan timeout)
                {
                    try
                    {
                        if (IsRunning(bin) is false) return false;

                        var matched = System.Diagnostics.Process.GetProcessesByName(bin.FullName);

                        if (matched is null || matched.Any() is false) return true;

                        foreach (var procsInfo in matched.Where(p =>
                            p.MainModule is not null &&
                            p.MainModule.FileName is not null &&
                            p.MainModule.FileName.Equals(bin.FullName, StringComparison.InvariantCultureIgnoreCase)))
                        {
                            if (procsInfo.CloseMainWindow()) procsInfo.WaitForExit((int)timeout.TotalMilliseconds);
                            if (procsInfo.HasExited is false) procsInfo.Kill(true);
                        }

                        return true;
                    }
                    catch (Exception) { }

                    return false;
                }

                public static async ValueTask<bool> UpdateAsync(HttpClient httpClient, Uri api, FileInfo bin, CancellationToken cancellationToken)
                {
                    if (IsRunning(bin) is false) return false;

                    var response = await httpClient.GetFromJsonAsync<UpdateResponse>(api.AbsoluteUri, new JsonSerializerOptions
                    {
                        IncludeFields = true
                    }, cancellationToken);

                    if (response is null) return false;

                    Version actualVersion;

                    if (FileVersionInfo.GetVersionInfo(bin.FullName).FileVersion is not string binVersionString) return false;

                    try
                    {
                        actualVersion = Version.Parse(binVersionString);
                        if (actualVersion >= response.Version) return false;

                        using var update = await httpClient.GetAsync(response.Uri, cancellationToken);
                        if (update is null) return false;

                        Stop(bin, TimeSpan.FromSeconds(60));

                        // read update archive to temp (overwrite)
                        var temp = Directory.CreateTempSubdirectory();
                        var updateFile = new FileInfo($@"{temp.FullName}/{bin.Name}.zip");

                        await File.WriteAllBytesAsync(updateFile.FullName, await update.Content.ReadAsByteArrayAsync(cancellationToken), cancellationToken);

                        // extract update archive from temp to app dir (overwrite)
                        ZipFile.ExtractToDirectory(updateFile.FullName, bin.Directory?.FullName, true);

                        // delete temp folder
                        if (temp.Exists) temp.Delete(true);

                        // rewrite with options to start user session process
                        //Start(app, directory, TimeSpan.FromSeconds(60));

                        return true;
                    }
                    catch (Exception) { }

                    return false;
                }

                public static bool Delete(FileInfo bin, TimeSpan timeout)
                {
                    try
                    {
                        Stop(bin, timeout);
                        bin.Delete();

                        return true;
                    }
                    catch (Exception) { }

                    return false;
                }
            }
        }

        public class UpdateResult
        {
            public string? Api { get; set; }
            public string? SourceDirectory { get; set; }
            public string? App { get; set; }
            public string? ServiceName { get; set; }

            public bool ApiAvailable { get; set; } = false;
            public bool UpdateAvailable { get; set; } = false;
            public bool Success { get; set; } = false;
            public List<string> ApiErrors { get; } = new();
            public List<string> UpdateErrors { get; } = new();
        }
    }
}