namespace BrassAndSigil.Server.Services;

/// <summary>
/// Pushes player-visible messages and overlays into Minecraft via stdin
/// commands (/say, /title, /bossbar). The boss-bar countdown is the primary
/// primitive the updater uses for restart announcements.
/// </summary>
public sealed class Broadcaster
{
    private readonly ServerProcess _proc;
    private const string BossBarId = "brass:announce";

    public Broadcaster(ServerProcess proc) => _proc = proc;

    public Task SayAsync(string message, CancellationToken ct = default)
        => _proc.SendInputAsync($"say {SingleLine(message)}", ct);

    public Task ActionBarAsync(string message, CancellationToken ct = default)
        => _proc.SendInputAsync($"title @a actionbar {{\"text\":\"{EscapeJson(message)}\"}}", ct);

    /// <summary>
    /// Re-sends the action bar text once per second so it stays sticky for the
    /// full duration. Action bar fades after ~2-3 s of inactivity, so the
    /// re-send is mandatory. Doesn't conflict with boss-bar UI for actual
    /// boss fights -- preferred over BossBarCountdownAsync for restart warnings.
    /// </summary>
    public async Task ActionBarCountdownAsync(
        string title, int durationSeconds, CancellationToken ct = default)
    {
        if (durationSeconds <= 0) return;
        // Silence /title's "Showing new title for X" chat broadcast for the loop --
        // otherwise it spams chat once per second per online player. Restored in
        // the finally block. World save typically isn't quick enough to persist
        // the off state if we crash mid-flight, but worst case admins can flip
        // it back manually with /gamerule sendCommandFeedback true.
        await _proc.SendInputAsync("gamerule sendCommandFeedback false", ct);
        try
        {
            for (int sec = durationSeconds; sec > 0; sec--)
            {
                ct.ThrowIfCancellationRequested();
                var mins = sec / 60;
                var secs = sec % 60;
                var label = mins > 0 ? $"{title} -- {mins}m {secs:00}s" : $"{title} -- {secs}s";
                await _proc.SendInputAsync($"title @a actionbar {{\"text\":\"{EscapeJson(label)}\"}}", ct);
                try { await Task.Delay(1000, ct); }
                catch (OperationCanceledException) { throw; }
            }
        }
        finally
        {
            // Clear the action bar AND restore feedback. Both best-effort: if MC
            // is stopping these'll fail and that's fine.
            try { await _proc.SendInputAsync("title @a actionbar {\"text\":\"\"}"); } catch { }
            try { await _proc.SendInputAsync("gamerule sendCommandFeedback true"); } catch { }
        }
    }

    public Task TitleAsync(string message, CancellationToken ct = default)
        => _proc.SendInputAsync($"title @a title {{\"text\":\"{EscapeJson(message)}\"}}", ct);

    /// <summary>
    /// Show a draining boss bar at the top of every player's screen for
    /// <paramref name="durationSeconds"/>. Updates the bar's name with a
    /// "title -- Mm Ss" countdown each second. Returns when the bar is removed.
    /// Honours cancellation: bar is removed cleanly even on cancel.
    /// </summary>
    public async Task BossBarCountdownAsync(
        string title, int durationSeconds, string color = "yellow", CancellationToken ct = default)
    {
        if (durationSeconds <= 0) return;

        // Silence /bossbar feedback for the same reason as ActionBarCountdownAsync.
        await _proc.SendInputAsync("gamerule sendCommandFeedback false", ct);
        await _proc.SendInputAsync($"bossbar add {BossBarId} {{\"text\":\"{EscapeJson(title)}\"}}", ct);
        await _proc.SendInputAsync($"bossbar set {BossBarId} color {color}", ct);
        await _proc.SendInputAsync($"bossbar set {BossBarId} max {durationSeconds}", ct);
        await _proc.SendInputAsync($"bossbar set {BossBarId} value {durationSeconds}", ct);
        await _proc.SendInputAsync($"bossbar set {BossBarId} players @a", ct);

        try
        {
            for (int sec = durationSeconds; sec > 0; sec--)
            {
                ct.ThrowIfCancellationRequested();
                var mins = sec / 60;
                var secs = sec % 60;
                var label = mins > 0 ? $"{title} -- {mins}m {secs:00}s" : $"{title} -- {secs}s";
                await _proc.SendInputAsync($"bossbar set {BossBarId} name {{\"text\":\"{EscapeJson(label)}\"}}", ct);
                await _proc.SendInputAsync($"bossbar set {BossBarId} value {sec}", ct);
                try { await Task.Delay(1000, ct); }
                catch (OperationCanceledException) { throw; }
            }
        }
        finally
        {
            // Always remove the bar -- even on cancel, so a stuck bar isn't left
            // on every player's screen. Use CancellationToken.None for the cleanup.
            try { await _proc.SendInputAsync($"bossbar remove {BossBarId}"); } catch { }
            try { await _proc.SendInputAsync("gamerule sendCommandFeedback true"); } catch { }
        }
    }

    private static string EscapeJson(string s) =>
        s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", " ");

    private static string SingleLine(string s) =>
        s.Replace("\r", " ").Replace("\n", " ").Trim();
}
