using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;

namespace DragonISO.App.Services;

/// <summary>
/// Asks Forgejo's REST API whether a newer release tag exists than the one
/// we're running. Manual-only for v1 â€” there's no background polling. The
/// operator can click "Check for updates" in the About dialog whenever they
/// want, and a positive result opens the release page in their browser
/// (rather than auto-downloading; we don't want a long-running show
/// interrupted by a surprise installer).
///
/// We use the public release endpoint so no auth is needed:
///   GET /api/v1/repos/zgaetano/Dragon-ISO/releases?limit=1
///
/// On any error (offline, DNS failure, repo private, malformed response),
/// the caller gets <see cref="UpdateCheckResult.Failed"/> with a short
/// human-readable message rather than an exception.
/// </summary>
public static class UpdateChecker
{
    private const string ReleasesApi =
        "https://forge.wilddragon.net/api/v1/repos/zgaetano/Dragon-ISO/releases?limit=1";

    private const string ReleasesPage =
        "https://forge.wilddragon.net/zgaetano/Dragon-ISO/releases";

    /// <summary>Outcome of a single check.</summary>
    public sealed record UpdateCheckResult(
        UpdateStatus Status,
        string? LatestTag,
        string? CurrentVersion,
        string? Message)
    {
        public static UpdateCheckResult Failed(string message) =>
            new(UpdateStatus.Error, null, null, message);
    }

    public enum UpdateStatus
    {
        UpToDate,
        UpdateAvailable,
        Error,
    }

    public static async Task<UpdateCheckResult> CheckAsync(CancellationToken ct = default)
    {
        var current = GetCurrentVersion();
        try
        {
            using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(8) };
            client.DefaultRequestHeaders.Add("User-Agent", "Dragon-ISO/" + current);

            using var res = await client.GetAsync(ReleasesApi, ct);
            if (!res.IsSuccessStatusCode)
                return UpdateCheckResult.Failed($"Server returned {(int)res.StatusCode}.");

            var json = await res.Content.ReadAsStringAsync(ct);
            using var doc = JsonDocument.Parse(json);
            if (doc.RootElement.ValueKind != JsonValueKind.Array || doc.RootElement.GetArrayLength() == 0)
                return new UpdateCheckResult(UpdateStatus.UpToDate, null, current,
                    "No releases published yet.");

            var first = doc.RootElement[0];
            if (!first.TryGetProperty("tag_name", out var tagProp))
                return UpdateCheckResult.Failed("Release record missing tag_name.");

            var latestTag = tagProp.GetString();
            if (string.IsNullOrWhiteSpace(latestTag))
                return UpdateCheckResult.Failed("Latest tag was empty.");

            var latestVersion = TryParseSemVer(latestTag);
            var currentVersion = TryParseSemVer("v" + current);

            if (latestVersion is null || currentVersion is null)
                return new UpdateCheckResult(UpdateStatus.UpToDate, latestTag, current,
                    "Couldn't compare versions; latest tag is " + latestTag);

            return latestVersion > currentVersion
                ? new UpdateCheckResult(UpdateStatus.UpdateAvailable, latestTag, current,
                    $"A newer version ({latestTag}) is available.")
                : new UpdateCheckResult(UpdateStatus.UpToDate, latestTag, current,
                    "You're on the latest release.");
        }
        catch (TaskCanceledException)
        {
            return UpdateCheckResult.Failed("Update check timed out.");
        }
        catch (HttpRequestException ex)
        {
            return UpdateCheckResult.Failed("Couldn't reach the update server: " + ex.Message);
        }
        catch (Exception ex)
        {
            return UpdateCheckResult.Failed("Unexpected error: " + ex.Message);
        }
    }

    /// <summary>
    /// Open the releases page in the user's default browser. Used by the
    /// "Update available" dialog button â€” we deliberately don't download/run
    /// the MSI ourselves, so the operator decides when to install.
    /// </summary>
    public static void OpenReleasesPage()
    {
        try
        {
            Process.Start(new ProcessStartInfo
            {
                FileName = ReleasesPage,
                UseShellExecute = true,
            });
        }
        catch
        {
            // best-effort; the dialog already shows the URL as text fallback
        }
    }

    /// <summary>
    /// Silent throttled launch-time check. Returns the result if a check actually
    /// happened, or null if the cooldown window suppressed it. The cooldown lives
    /// in <c>%LOCALAPPDATA%\Dragon-ISO\last-update-check.txt</c> as an ISO 8601
    /// timestamp; a missing file means "never checked, do it now."
    /// </summary>
    public static async Task<UpdateCheckResult?> CheckIfDueAsync(TimeSpan cooldown, CancellationToken ct = default)
    {
        try
        {
            var path = CooldownPath;
            if (File.Exists(path))
            {
                var raw = File.ReadAllText(path).Trim();
                if (DateTimeOffset.TryParse(raw, out var last) &&
                    DateTimeOffset.UtcNow - last < cooldown)
                {
                    return null;
                }
            }
        }
        catch
        {
            // Throttle check is best-effort; on read failures we just check now.
        }

        var result = await CheckAsync(ct);

        try
        {
            var dir = Path.GetDirectoryName(CooldownPath);
            if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
            File.WriteAllText(CooldownPath, DateTimeOffset.UtcNow.ToString("o"));
        }
        catch
        {
            // If we can't write the cooldown stamp, future launches will check
            // again immediately. Annoying but not broken.
        }

        return result;
    }

    /// <summary>
    /// Test-only seam â€” when set, overrides the default
    /// %LOCALAPPDATA%\Dragon-ISO path that holds the cooldown stamp +
    /// the opt-out flag. Tests use this to write to a tempdir so
    /// CheckIfDueAsync's throttle path can be exercised without
    /// hitting real disk paths or the real network (the throttle
    /// short-circuits before the HTTP call).
    /// </summary>
    internal static string? StateDirectoryOverride { get; set; }

    private static string StateDirectory => StateDirectoryOverride ??
        Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "Dragon-ISO");

    private static string CooldownPath =>
        Path.Combine(StateDirectory, "last-update-check.txt");

    private static string OptOutPath =>
        Path.Combine(StateDirectory, "no-update-check.flag");

    /// <summary>
    /// Whether launch-time update checks are enabled. Inverted-flag-file storage:
    /// the absence of the file means "checks on" (default), the presence means
    /// "checks off". Operators can ship the flag file via group policy / config-
    /// management to suppress checks across a fleet.
    /// </summary>
    public static bool LaunchCheckEnabled
    {
        get => !File.Exists(OptOutPath);
        set
        {
            try
            {
                if (value)
                {
                    if (File.Exists(OptOutPath)) File.Delete(OptOutPath);
                }
                else
                {
                    var dir = Path.GetDirectoryName(OptOutPath);
                    if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
                    File.WriteAllText(OptOutPath, "Update checks suppressed by user. Delete this file to re-enable.");
                }
            }
            catch
            {
                // Best-effort; toggle won't persist if disk is read-only, but
                // the in-memory checkbox state still reflects the user's intent
                // for this session.
            }
        }
    }

    private static string GetCurrentVersion()
    {
        var asm = typeof(UpdateChecker).Assembly;
        return asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
            ?? asm.GetName().Version?.ToString()
            ?? "0.0.0";
    }

    /// <summary>
    /// Parse a "vX.Y.Z" or "X.Y.Z(.N)" string into a Version. Strips any
    /// pre-release suffix ("-alpha", "-beta") so the comparison is on
    /// numeric components only â€” pre-release vs. release ordering is a
    /// follow-up if we need it. Internal so tests can pin parsing
    /// behaviour without HTTP.
    /// </summary>
    internal static Version? TryParseSemVer(string s)
    {
        var trimmed = s.TrimStart('v', 'V');
        var dash = trimmed.IndexOf('-');
        if (dash >= 0) trimmed = trimmed[..dash];
        return Version.TryParse(trimmed, out var v) ? v : null;
    }
}
