using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using Windows.Management.Deployment;
using YMhut.Box.Core;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Net;
using YMhut.Box.Core.Updates;

namespace YMhut.Box.WinUI.Services;

public sealed class AppInstallerUpdateService(
    ILogService logService,
    IHttpService httpService) : IAppInstallerUpdateService
{
    private static readonly IReadOnlyList<Uri> DefaultUpdateInfoUris = UpdateInfoEndpointPolicy.BuildDefaultUris();
    private static readonly Uri DefaultAppInstallerUri = new("https://update.ymhut.cn/update-info/winui.appinstaller");
    private static readonly Uri DefaultUpdateHost = new("https://update.ymhut.cn/");
    private static readonly Guid DownloadsKnownFolderId = new("374DE290-123F-4565-9164-39C4925E467B");

    public async Task<RemoteUpdateInfo?> GetUpdateInfoAsync(CancellationToken cancellationToken = default)
    {
        var failures = new List<string>();
        foreach (var endpoint in DefaultUpdateInfoUris)
        {
            try
            {
                var response = await httpService.GetAsync(endpoint, cancellationToken).ConfigureAwait(false);
                var parsed = ParseUpdateInfo(response.Content);
                if (parsed is not null)
                {
                    await logService.WriteAsync("Information", "update", "读取远程更新配置", endpoint.AbsolutePath, cancellationToken);
                    return parsed;
                }

                failures.Add($"{endpoint.AbsolutePath}: invalid payload");
            }
            catch (Exception exception) when (exception is HttpRequestException or TaskCanceledException or JsonException or InvalidOperationException)
            {
                failures.Add($"{endpoint.AbsolutePath}: {SensitiveText.Sanitize(exception.Message)}");
            }
        }

        await logService.WriteAsync(
            "Warning",
            "update",
            "读取更新配置失败",
            string.Join(" | ", failures),
            cancellationToken);
        return null;
    }

    public async Task<string?> DownloadUpdateAsync(
        RemoteUpdateInfo info,
        IProgress<UpdateDownloadProgress>? progress = null,
        CancellationToken cancellationToken = default)
    {
        var url = FirstNonEmpty(info.DownloadUrl, info.AppInstallerUrl);
        if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out var uri))
        {
            return null;
        }

        var downloadDirectory = GetDownloadsDirectory();
        Directory.CreateDirectory(downloadDirectory);
        var fileName = Path.GetFileName(uri.LocalPath);
        if (string.IsNullOrWhiteSpace(fileName))
        {
            fileName = $"YMhutBox_Update_{info.DisplayVersion}.bin";
        }

        var targetPath = GetAvailableDownloadPath(downloadDirectory, SanitizeFileName(fileName));
        using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(30) };
        using var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
        response.EnsureSuccessStatusCode();
        var total = response.Content.Headers.ContentLength > 0 ? response.Content.Headers.ContentLength : info.SizeBytes > 0 ? info.SizeBytes : null;
        await using var source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
        await using var target = File.Create(targetPath);

        var buffer = new byte[128 * 1024];
        long received = 0;
        var started = Stopwatch.StartNew();
        int read;
        while ((read = await source.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
        {
            await target.WriteAsync(buffer.AsMemory(0, read), cancellationToken).ConfigureAwait(false);
            received += read;
            progress?.Report(new UpdateDownloadProgress(received, total, received / Math.Max(0.001, started.Elapsed.TotalSeconds)));
        }

        await logService.WriteAsync("Information", "update", "更新包下载完成", Path.GetFileName(targetPath), cancellationToken);
        return targetPath;
    }

    public async Task<string> CheckForUpdatesAsync(CancellationToken cancellationToken = default)
    {
        var info = await GetUpdateInfoAsync(cancellationToken).ConfigureAwait(false);
        var appInstallerUri = Uri.TryCreate(info?.AppInstallerUrl, UriKind.Absolute, out var manifestUri)
            ? manifestUri
            : DefaultAppInstallerUri;

        try
        {
            var manager = new PackageManager();
            await manager.AddPackageByAppInstallerFileAsync(
                appInstallerUri,
                AddPackageByAppInstallerOptions.ForceTargetAppShutdown,
                null).AsTask(cancellationToken);
            await logService.WriteAsync("Information", "update", "App Installer 更新检查已发起", cancellationToken: cancellationToken);
            return "更新检查已发起，请按系统提示继续完成安装。";
        }
        catch (Exception exception)
        {
            var error = SensitiveText.Sanitize(exception.Message);
            await logService.WriteAsync("Error", "update", "App Installer 更新检查失败", error, cancellationToken);
            return $"更新检查失败：{error}";
        }
    }

    private static RemoteUpdateInfo? ParseUpdateInfo(string content)
    {
        try
        {
            using var document = JsonDocument.Parse(content);
            var root = document.RootElement;
            var latest = GetObject(root, "latest") ?? GetObject(root, "winui") ?? GetObject(root, "release") ?? root;
            var installer = GetInstallerObject(root, latest);
            var package = GetLatestPackage(root);
            var version = FirstNonEmpty(
                GetString(root, "latestVersion"),
                GetString(latest, "version"),
                GetString(installer, "version"),
                GetString(latest, "app_version"),
                GetString(latest, "appVersion"),
                GetString(root, "version"),
                GetString(root, "app_version"));
            if (string.IsNullOrWhiteSpace(version))
            {
                return null;
            }

            var build = FirstNonEmpty(GetString(latest, "build"), GetString(latest, "build_number"), GetString(root, "build"));
            var download = FirstNonEmpty(
                GetString(installer, "url"),
                GetString(installer, "downloadUrl"),
                GetString(installer, "download_url"),
                GetMirrorDownloadUrl(installer),
                GetMirrorDownloadUrl(root),
                GetString(latest, "download_url"),
                GetString(latest, "downloadUrl"),
                GetString(latest, "installer_url"),
                GetString(latest, "msix_url"),
                GetString(root, "download_url"),
                GetString(root, "downloadUrl"),
                GetPackageDownloadUrl(package));
            var appInstaller = FirstNonEmpty(
                GetString(latest, "appinstaller_url"),
                GetString(latest, "appInstallerUrl"),
                GetString(latest, "app_installer_url"),
                GetString(root, "appinstaller_url"),
                DefaultAppInstallerUri.ToString());
            var releaseNotes = FirstNonEmpty(
                GetString(latest, "release_notes"),
                GetString(latest, "releaseNotes"),
                GetString(latest, "changelog"),
                GetString(root, "release_notes"),
                GetNotes(root, "update_notes", "last_update_notes"));
            var messageMarkdown = FirstNonEmpty(
                GetString(latest, "message_md"),
                GetString(latest, "messageMarkdown"),
                GetString(latest, "description_md"),
                GetString(latest, "home_notes_md"),
                GetString(root, "message_md"),
                GetString(root, "messageMarkdown"),
                GetString(root, "description_md"),
                GetString(root, "home_notes_md"));
            var releaseNotesMarkdown = FirstNonEmpty(
                GetString(latest, "release_notes_md"),
                GetString(latest, "releaseNotesMarkdown"),
                GetString(latest, "changelog_md"),
                GetString(latest, "latestNotesMarkdown"),
                GetString(latest, "latest_notes_md"),
                GetString(root, "release_notes_md"),
                GetString(root, "releaseNotesMarkdown"),
                GetString(root, "changelog_md"),
                GetString(root, "latestNotesMarkdown"),
                GetString(root, "latest_notes_md"));
            return new RemoteUpdateInfo(
                version,
                build,
                FirstNonEmpty(GetString(latest, "channel"), GetString(root, "channel"), "stable"),
                FirstNonEmpty(GetString(latest, "title"), GetString(root, "title"), "发现新版本"),
                FirstNonEmpty(GetString(latest, "message"), GetString(latest, "description"), GetString(root, "message"), GetString(root, "home_notes")),
                releaseNotes,
                NormalizeRemoteUrl(download),
                appInstaller,
                GetBoolean(latest, "mandatory") || GetBoolean(latest, "force_update"),
                GetInt64(installer, "size") is var installerSize and > 0
                    ? installerSize
                    : GetInt64(latest, "size") is var size and > 0
                        ? size
                        : FirstPositive(GetInt64(latest, "file_size"), GetInt64(package, "sizeBytes")),
                TryDate(FirstNonEmpty(
                    GetString(latest, "published_at"),
                    GetString(latest, "release_date"),
                    GetString(root, "published_at"),
                    GetString(root, "last_updated"),
                    GetString(package, "updateTime"),
                    GetString(package, "updateDate"))),
                messageMarkdown,
                releaseNotesMarkdown);
        }
        catch
        {
            return null;
        }
    }

    private static JsonElement GetInstallerObject(JsonElement root, JsonElement latest)
    {
        if (GetObject(latest, "fullInstaller") is { } latestFullInstaller)
        {
            return latestFullInstaller;
        }

        if (GetObject(latest, "files") is { } latestFiles &&
            GetObject(latestFiles, "fullInstaller") is { } latestFilesFullInstaller)
        {
            return latestFilesFullInstaller;
        }

        if (GetObject(root, "fullInstaller") is { } rootFullInstaller)
        {
            return rootFullInstaller;
        }

        if (GetObject(root, "files") is { } rootFiles &&
            GetObject(rootFiles, "fullInstaller") is { } rootFilesFullInstaller)
        {
            return rootFilesFullInstaller;
        }

        return default;
    }

    private static JsonElement? GetObject(JsonElement root, string name)
    {
        return root.ValueKind == JsonValueKind.Object &&
            root.TryGetProperty(name, out var value) &&
            value.ValueKind == JsonValueKind.Object
            ? value
            : null;
    }

    private static JsonElement GetLatestPackage(JsonElement root)
    {
        if (root.ValueKind != JsonValueKind.Object ||
            !root.TryGetProperty("detected_packages", out var packages) ||
            packages.ValueKind != JsonValueKind.Object)
        {
            return default;
        }

        var preferred = GetString(root, "detected_product");
        if (!string.IsNullOrWhiteSpace(preferred) &&
            packages.TryGetProperty(preferred, out var preferredList) &&
            preferredList.ValueKind == JsonValueKind.Array &&
            preferredList.GetArrayLength() > 0)
        {
            return preferredList[0];
        }

        foreach (var product in packages.EnumerateObject())
        {
            if (product.Value.ValueKind == JsonValueKind.Array && product.Value.GetArrayLength() > 0)
            {
                return product.Value[0];
            }
        }

        return default;
    }

    private static string GetMirrorDownloadUrl(JsonElement root)
    {
        if (root.ValueKind != JsonValueKind.Object ||
            !root.TryGetProperty("download_mirrors", out var mirrors) ||
            mirrors.ValueKind != JsonValueKind.Array)
        {
            return string.Empty;
        }

        foreach (var mirror in mirrors.EnumerateArray())
        {
            if (!GetBoolean(mirror, "enabled"))
            {
                continue;
            }

            var url = GetString(mirror, "url");
            if (!string.IsNullOrWhiteSpace(url))
            {
                return url;
            }
        }

        return string.Empty;
    }

    private static string GetPackageDownloadUrl(JsonElement package)
    {
        if (package.ValueKind != JsonValueKind.Object)
        {
            return string.Empty;
        }

        return FirstNonEmpty(GetString(package, "downloadUrl"), GetString(package, "download_url"), GetString(package, "downloadPath"));
    }

    private static string NormalizeRemoteUrl(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }

        return Uri.TryCreate(value, UriKind.Absolute, out var absolute)
            ? absolute.ToString()
            : new Uri(DefaultUpdateHost, value.TrimStart('/')).ToString();
    }

    private static string GetNotes(JsonElement root, params string[] names)
    {
        var lines = new List<string>();
        foreach (var name in names)
        {
            if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty(name, out var notes))
            {
                continue;
            }

            if (notes.ValueKind == JsonValueKind.Object)
            {
                lines.AddRange(notes.EnumerateObject()
                    .Select(item => $"{item.Name}: {ElementToText(item.Value)}")
                    .Where(line => !string.IsNullOrWhiteSpace(line)));
            }
            else if (notes.ValueKind == JsonValueKind.Array)
            {
                lines.AddRange(notes.EnumerateArray()
                    .Select(ElementToText)
                    .Where(line => !string.IsNullOrWhiteSpace(line)));
            }
            else
            {
                var text = ElementToText(notes);
                if (!string.IsNullOrWhiteSpace(text))
                {
                    lines.Add(text);
                }
            }
        }

        return string.Join(Environment.NewLine, lines.Distinct());
    }

    private static string ElementToText(JsonElement element)
    {
        return element.ValueKind switch
        {
            JsonValueKind.String => element.GetString() ?? string.Empty,
            JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => element.ToString(),
            _ => string.Empty
        };
    }

    private static string GetString(JsonElement root, string name)
    {
        return root.ValueKind == JsonValueKind.Object &&
            root.TryGetProperty(name, out var value)
            ? value.ValueKind switch
            {
                JsonValueKind.String => value.GetString() ?? string.Empty,
                JsonValueKind.Number => value.GetRawText(),
                JsonValueKind.True => "true",
                JsonValueKind.False => "false",
                _ => string.Empty
            }
            : string.Empty;
    }

    private static bool GetBoolean(JsonElement root, string name)
    {
        return root.ValueKind == JsonValueKind.Object &&
            root.TryGetProperty(name, out var value) &&
            value.ValueKind switch
            {
                JsonValueKind.True => true,
                JsonValueKind.False => false,
                JsonValueKind.String => bool.TryParse(value.GetString(), out var parsed) && parsed,
                _ => false
            };
    }

    private static long GetInt64(JsonElement root, string name)
    {
        if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty(name, out var value))
        {
            return 0;
        }

        if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var number))
        {
            return number;
        }

        return value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out var parsed)
            ? parsed
            : 0;
    }

    private static DateTimeOffset? TryDate(string value)
    {
        return DateTimeOffset.TryParse(value, out var date) ? date : null;
    }

    private static string FirstNonEmpty(params string[] values)
    {
        return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
    }

    private static long FirstPositive(params long[] values)
    {
        return values.FirstOrDefault(value => value > 0);
    }

    private static string GetDownloadsDirectory()
    {
        try
        {
            var result = SHGetKnownFolderPath(DownloadsKnownFolderId, 0, IntPtr.Zero, out var pathPointer);
            if (result == 0 && pathPointer != IntPtr.Zero)
            {
                try
                {
                    var path = Marshal.PtrToStringUni(pathPointer);
                    if (!string.IsNullOrWhiteSpace(path))
                    {
                        return path;
                    }
                }
                finally
                {
                    Marshal.FreeCoTaskMem(pathPointer);
                }
            }
        }
        catch
        {
        }

        return Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
            "Downloads");
    }

    private static string GetAvailableDownloadPath(string directory, string fileName)
    {
        var candidate = Path.Combine(directory, fileName);
        if (!File.Exists(candidate))
        {
            return candidate;
        }

        var name = Path.GetFileNameWithoutExtension(fileName);
        var extension = Path.GetExtension(fileName);
        for (var index = 1; index < 1000; index++)
        {
            candidate = Path.Combine(directory, $"{name} ({index}){extension}");
            if (!File.Exists(candidate))
            {
                return candidate;
            }
        }

        return Path.Combine(directory, $"{name}-{DateTimeOffset.Now:yyyyMMddHHmmss}{extension}");
    }

    private static string SanitizeFileName(string fileName)
    {
        var invalidCharacters = Path.GetInvalidFileNameChars();
        var sanitized = new string(fileName
            .Select(character => invalidCharacters.Contains(character) ? '_' : character)
            .ToArray());
        return string.IsNullOrWhiteSpace(sanitized) ? "YMhutBox_Update.bin" : sanitized;
    }

    [DllImport("shell32.dll")]
    private static extern int SHGetKnownFolderPath(
        [MarshalAs(UnmanagedType.LPStruct)] Guid rfid,
        uint dwFlags,
        IntPtr hToken,
        out IntPtr ppszPath);
}
