using BrassAndSigil.Server.Models;
using System.Text.Json;

namespace BrassAndSigil.Server.Services;

/// <summary>
/// Drives a "fetch new manifest, drain players, swap mods, restart MC" workflow.
/// Single-flight: one update at a time, guarded by a semaphore. State is exposed
/// so the panel can poll progress; logs go through the existing OnLogLine event
/// (re-streamed via SSE) so they show up in the live console too.
/// </summary>
public sealed class UpdaterService
{
    private readonly ServerConfig _config;
    private readonly string _configPath;
    private readonly ServerProcess _proc;
    private readonly Broadcaster _broadcast;
    private readonly Action<string> _log;
    private readonly SemaphoreSlim _gate = new(1, 1);

    private CancellationTokenSource? _cts;
    public UpdateState State { get; private set; } = new();

    public sealed class UpdateState
    {
        public bool InProgress      { get; set; }
        public string Phase         { get; set; } = "idle";
            // "idle" | "countdown" | "stopping" | "syncing" | "installing_loader" | "starting" | "complete" | "failed" | "cancelled"
        public int CountdownTotal   { get; set; }
        public int CountdownRemaining { get; set; }
        public string? CurrentVersion   { get; set; }
        public string? AvailableVersion { get; set; }
        public string? Error            { get; set; }
        public DateTimeOffset? LastFinishedAt { get; set; }
    }

    public sealed record CheckResult(string? Current, string? Available, bool NeedsUpdate, string? Error);

    public UpdaterService(ServerConfig config, string configPath,
                          ServerProcess proc, Broadcaster broadcast,
                          Action<string> log)
    {
        _config = config;
        _configPath = configPath;
        _proc = proc;
        _broadcast = broadcast;
        _log = log;
    }

    /// <summary>Lightweight read: compare local pack-version.json to remote manifest.</summary>
    public async Task<CheckResult> CheckAsync(CancellationToken ct = default)
    {
        try
        {
            var sync = new ManifestSync();
            var manifest = await sync.FetchManifestAsync(_config.ManifestUrl, ct);
            var local = ReadLocalPackVersion(_config.ServerDir);
            var current = local;
            var available = manifest.Version;
            var needs = !string.Equals(current, available, StringComparison.Ordinal);
            State.CurrentVersion   = current;
            State.AvailableVersion = available;
            return new CheckResult(current, available, needs, null);
        }
        catch (Exception ex)
        {
            return new CheckResult(State.CurrentVersion, State.AvailableVersion, false, ex.Message);
        }
    }

    public bool TryCancel()
    {
        if (!State.InProgress || _cts is null) return false;
        // Only meaningful during countdown phase -- a sync mid-flight isn't safely abortable.
        if (State.Phase != "countdown") return false;
        _cts.Cancel();
        return true;
    }

    /// <summary>
    /// Run the full update flow. Single-flight -- returns false if one is already running.
    /// </summary>
    public async Task<bool> StartAsync(int delaySeconds)
    {
        if (!await _gate.WaitAsync(0)) return false;
        _cts = new CancellationTokenSource();
        var ct = _cts.Token;

        State = new UpdateState
        {
            InProgress = true,
            Phase = "countdown",
            CountdownTotal = delaySeconds,
            CountdownRemaining = delaySeconds,
            CurrentVersion = State.CurrentVersion,
            AvailableVersion = State.AvailableVersion,
        };

        try
        {
            // ── 1. Player-facing countdown ──
            if (delaySeconds > 0 && _proc.IsRunning)
            {
                _log($"[update] Announcing restart in {delaySeconds}s.");
                await _broadcast.SayAsync($"Server will restart in {FormatDuration(delaySeconds)} for an update to v{State.AvailableVersion}.", ct);

                // Run the action-bar countdown + periodic chat warnings + UI ticker
                // in parallel. Action bar (instead of boss bar) avoids stacking on
                // top of real boss fight UIs (Ender Dragon, raids, mod bosses).
                var actionBar = _broadcast.ActionBarCountdownAsync(
                    "Server restart for update", delaySeconds, ct);

                var warnings = WarnDuringCountdownAsync(delaySeconds, ct);

                // Drive State.CountdownRemaining for the UI poller.
                var ticker = TickCountdownStateAsync(delaySeconds, ct);

                await Task.WhenAll(actionBar, warnings, ticker);
            }

            // ── 2. Stop MC ──
            ct.ThrowIfCancellationRequested();
            State.Phase = "stopping";
            _log("[update] Stopping Minecraft for update...");
            if (_proc.IsRunning)
            {
                await _broadcast.SayAsync("Server is restarting now.");
                await _proc.StopAsync(TimeSpan.FromSeconds(30));
            }

            // ── 3. Sync mods from manifest ──
            ct.ThrowIfCancellationRequested();
            State.Phase = "syncing";
            _log("[update] Syncing mods from manifest...");
            var sync = new ManifestSync();
            var progress = new Progress<string>(msg => _log($"[update]   {msg}"));
            var result = await sync.SyncAsync(_config.ManifestUrl, _config.ServerDir, progress, ct);
            _log($"[update] Sync complete: {result.Downloaded} downloaded, {result.Removed} removed.");

            // ── 4. Update NeoForge if loader version changed ──
            ct.ThrowIfCancellationRequested();
            var manifest = await sync.FetchManifestAsync(_config.ManifestUrl, ct);
            if (manifest.Loader is { } loader &&
                loader.Type.Equals("neoforge", StringComparison.OrdinalIgnoreCase) &&
                LoaderVersionChanged(_config.ServerDir, loader.Version))
            {
                State.Phase = "installing_loader";
                _log($"[update] Reinstalling NeoForge {loader.Version}...");
                var nf = new NeoForgeInstaller();
                var ok = await nf.InstallAsync(loader.Version, _config.ServerDir, _config.JavaPath, progress, ct);
                if (!ok) throw new InvalidOperationException("NeoForge installer failed.");
            }

            // ── 5. Start MC ──
            ct.ThrowIfCancellationRequested();
            State.Phase = "starting";
            _log("[update] Starting Minecraft...");
            _proc.Start();
            State.CurrentVersion = manifest.Version;

            State.Phase = "complete";
            State.InProgress = false;
            State.LastFinishedAt = DateTimeOffset.UtcNow;
            _log("[update] Update complete.");
            return true;
        }
        catch (OperationCanceledException)
        {
            State.Phase = "cancelled";
            State.InProgress = false;
            State.LastFinishedAt = DateTimeOffset.UtcNow;
            _log("[update] Update cancelled.");
            // If we cancelled during countdown, MC is still running -- leave it alone.
            return false;
        }
        catch (Exception ex)
        {
            State.Phase = "failed";
            State.Error = ex.Message;
            State.InProgress = false;
            State.LastFinishedAt = DateTimeOffset.UtcNow;
            _log($"[update] Failed: {ex.Message}");
            // Try to bring MC back up if we stopped it but never restarted.
            if (!_proc.IsRunning)
            {
                try { _proc.Start(); _log("[update] Restored Minecraft after failure."); }
                catch (Exception startEx) { _log($"[update] Restore failed too: {startEx.Message}"); }
            }
            return false;
        }
        finally
        {
            _cts?.Dispose();
            _cts = null;
            _gate.Release();
        }
    }

    private async Task TickCountdownStateAsync(int total, CancellationToken ct)
    {
        for (int sec = total; sec > 0; sec--)
        {
            ct.ThrowIfCancellationRequested();
            State.CountdownRemaining = sec;
            try { await Task.Delay(1000, ct); }
            catch (OperationCanceledException) { throw; }
        }
        State.CountdownRemaining = 0;
    }

    private async Task WarnDuringCountdownAsync(int total, CancellationToken ct)
    {
        // Periodic chat warnings -- independent of the boss bar (visual-but-missable).
        // Each milestone fires at an absolute time computed from the start, so the
        // delays don't accumulate sequentially across the loop iterations.
        var startUtc = DateTime.UtcNow;
        var milestones = new[] { 300, 60, 30, 10 };
        foreach (var m in milestones)
        {
            if (m >= total) continue;
            var fireAt = startUtc.AddSeconds(total - m);
            var wait = fireAt - DateTime.UtcNow;
            if (wait > TimeSpan.Zero)
            {
                try { await Task.Delay(wait, ct); }
                catch (OperationCanceledException) { throw; }
            }
            try { await _broadcast.SayAsync($"Server restart in {FormatDuration(m)}."); }
            catch { /* don't bring down the whole update for one failed broadcast */ }
        }
    }

    private static bool LoaderVersionChanged(string serverDir, string newVersion)
    {
        // Look at the libraries dir for an existing neoforge-<version> path.
        // If absent or different version, we should re-install.
        var libsRoot = Path.Combine(serverDir, "libraries", "net", "neoforged", "neoforge");
        if (!Directory.Exists(libsRoot)) return true;
        var versions = Directory.EnumerateDirectories(libsRoot).Select(Path.GetFileName).ToList();
        return !versions.Contains(newVersion);
    }

    private static string? ReadLocalPackVersion(string serverDir)
    {
        var path = Path.Combine(serverDir, "pack-version.json");
        if (!File.Exists(path)) return null;
        try
        {
            using var doc = JsonDocument.Parse(File.ReadAllText(path));
            return doc.RootElement.TryGetProperty("version", out var v) ? v.GetString() : null;
        }
        catch { return null; }
    }

    private static string FormatDuration(int seconds)
    {
        if (seconds >= 60) return $"{seconds / 60} minute{(seconds / 60 == 1 ? "" : "s")}";
        return $"{seconds} second{(seconds == 1 ? "" : "s")}";
    }
}
