using DodoSSH.Client.Terminal;

namespace DodoSSH.Client.Android.Platform;

/// <summary>
/// Keeps <see cref="SessionForegroundService"/> in step with what is actually running.
/// </summary>
/// <remarks>
/// <para>
/// The service is started and stopped from one place, and that place is a count rather than a lifecycle.
/// Anything else drifts: a service started when a shell opens and stopped when a tab closes would leave
/// the notification up after the last shell died on its own, and a phone showing "1 shell connected" over
/// nothing is the same dishonesty the unlock screen's shell count exists to avoid.
/// </para>
/// <para>
/// <see cref="TerminalWorkspace.LiveSessionCount"/> is deliberately the source of truth rather than a
/// tally kept here. It already knows that a session whose shell exited half an hour ago is not live, which
/// a counter incremented on open and decremented on close would not.
/// </para>
/// <para>
/// Three facts feed <see cref="SessionForegroundService.Reconcile"/>, not one: live shells, moving
/// transfers, and an idle-but-connected Files session. The last of those used to be missing entirely —
/// a shell survives backgrounding because somebody opened it, but a Files connection with nothing moving
/// looked, to this class, exactly like nothing being open at all. <c>holdsFileSession</c> below is that
/// gap closed, read the same way the other two facts are: asked, not cached.
/// </para>
/// </remarks>
internal sealed class SessionKeepAlive : IDisposable
{
    private readonly TerminalWorkspace workspace;
    private readonly Func<int> activeTransfers;
    private readonly Func<bool> holdsFileSession;

    /// <param name="workspace">The live shells.</param>
    /// <param name="activeTransfers">
    /// How many transfers are moving bytes. A delegate rather than a queue, because ownership of the
    /// transfer queue stays with <c>TransfersViewModel</c> — this class only ever asks it a question.
    /// </param>
    /// <param name="holdsFileSession">
    /// Whether the Files screen holds a live SFTP connection with nothing moving on it — the idle-but-
    /// connected case a transfer count alone would miss. See <c>TransfersViewModel.HasLiveFileSession</c>.
    /// </param>
    public SessionKeepAlive(TerminalWorkspace workspace, Func<int> activeTransfers, Func<bool> holdsFileSession)
    {
        this.workspace = workspace;
        this.activeTransfers = activeTransfers;
        this.holdsFileSession = holdsFileSession;

        // Raised on whatever thread the workspace announced from — a continuation of the ended run, or the
        // closer's own — which is fine: starting and stopping a service is a binder call and needs no
        // particular thread. Nothing here touches the interface. That the announcement waits for the run to
        // actually complete, and comes for deliberate closes too, is what makes reading LiveSessionCount
        // from it honest — the event's own remark carries the stuck notification that taught us both.
        workspace.SessionEnded += OnSessionEnded;
    }

    /// <summary>Re-reads the counts and starts or stops the service to match.</summary>
    /// <remarks>
    /// Called after anything that could change any of the three facts — opening a shell, closing a tab, a
    /// transfer finishing, a Files connection opening or closing. Calling it when nothing changed is free:
    /// reconciling to the state it is already in is either a redundant <c>startForegroundService</c> — or,
    /// now, a redundant notification post — on a running service, or a <c>stopService</c> on a stopped one,
    /// and Android treats all of those as no-ops.
    /// </remarks>
    public void Refresh() =>
        SessionForegroundService.Reconcile(workspace.LiveSessionCount, activeTransfers(), holdsFileSession());

    /// <inheritdoc />
    public void Dispose()
    {
        workspace.SessionEnded -= OnSessionEnded;

        // The notification goes with the composition root. Leaving it up over a process that is shutting
        // down is how an SSH client acquires a reputation for a notification you cannot get rid of.
        SessionForegroundService.Reconcile(0, 0, false);
    }

    private void OnSessionEnded(object? sender, TerminalSessionEndedEventArgs e) => Refresh();
}
