using CommunityToolkit.Mvvm.ComponentModel;
using DodoSSH.Client.Ssh;

namespace DodoSSH.Client.Shell.ViewModels;

/// <summary>
/// One named part of making a connection, in the order they happen.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="SshConnectionPhase"/> with one more at the front. The SSH assembly reports four phases and
/// knows about no others, which is correct for it — it has never heard of a renderer. But the first thing a
/// connection here waits on is the terminal page attaching its socket, and on the first connection after a
/// cold start that is a real wait with a real failure mode of its own: a missing WebView2 runtime. A step
/// list that began at "reaching the host" would leave the one wait most likely to hang unnamed.
/// </para>
/// <para>
/// Declared here rather than shared with the SSH layer for that reason, and the mapping between the two is
/// one <c>switch</c> in <c>VaultViewModel</c>. The numbering is the order and the order is load-bearing:
/// <see cref="TerminalTabViewModel.Advance"/> compares these values to decide what is already behind it.
/// </para>
/// </remarks>
internal enum ConnectionStep
{
    /// <summary>Waiting for the renderer to attach, before anything is dialled.</summary>
    PreparingTerminal = 0,

    /// <inheritdoc cref="SshConnectionPhase.Reaching" />
    Reaching = 1,

    /// <inheritdoc cref="SshConnectionPhase.CheckingHostKey" />
    CheckingHostKey = 2,

    /// <inheritdoc cref="SshConnectionPhase.Authenticating" />
    Authenticating = 3,

    /// <inheritdoc cref="SshConnectionPhase.OpeningShell" />
    OpeningShell = 4,
}

/// <summary>How one step of a connection is getting on.</summary>
/// <remarks>
/// Four states rather than a bool per row, because a step list is read as a sequence and the reader's
/// question at each row is which of the four this is: behind us, happening, not yet, or where it stopped.
/// <see cref="Stopped"/> exists only for the row a failure landed on — see
/// <see cref="TerminalTabViewModel.Failed"/> — and is what turns the list from a progress bar into an
/// account of how far the attempt got.
/// </remarks>
internal enum ConnectionStepState
{
    /// <summary>Not started. Nothing is known about it yet.</summary>
    Pending = 0,

    /// <summary>Happening now.</summary>
    Running = 1,

    /// <summary>Finished, because something after it started.</summary>
    Done = 2,

    /// <summary>Where the attempt stopped. There is no step after this one.</summary>
    Stopped = 3,
}

/// <summary>One row of the connecting card's step list.</summary>
/// <remarks>
/// A view model per step rather than an index the view compares against, because each row draws its own
/// state and an <c>ItemsControl</c> has no way to ask "am I before the current one?" — the alternative was a
/// converter taking two bindings, which is the same comparison written somewhere it cannot be tested.
/// </remarks>
internal sealed partial class ConnectionStepViewModel : ObservableObject
{
    internal ConnectionStepViewModel(ConnectionStep step, string caption)
    {
        Step = step;
        Caption = caption;
    }

    /// <summary>Which step this is.</summary>
    internal ConnectionStep Step { get; }

    /// <summary>What the row says, in the present tense of the thing being waited on.</summary>
    internal string Caption { get; }

    /// <inheritdoc cref="ConnectionStepState" />
    [ObservableProperty]
    private ConnectionStepState state;

    /// <summary>Whether this step is the one happening now.</summary>
    internal bool IsRunning => State is ConnectionStepState.Running;

    /// <summary>Whether this step finished.</summary>
    internal bool IsDone => State is ConnectionStepState.Done;

    /// <summary>Whether the attempt stopped on this step.</summary>
    internal bool IsStopped => State is ConnectionStepState.Stopped;

    /// <summary>
    /// The character drawn beside the caption for whichever state this is in.
    /// </summary>
    /// <remarks>
    /// Here rather than in a converter for the reason <c>TransferRowViewModel.StatusWord</c> is: the mapping
    /// is four cases with no arithmetic, and a converter would put it in a file the shell's tests cannot
    /// reach. The colours stay in the view, where the palette is.
    /// <para>
    /// Four distinguishable shapes rather than one recoloured, because the difference between a step that
    /// finished and a step still running has to survive somebody who cannot tell this design's green from
    /// its amber.
    /// </para>
    /// </remarks>
    internal string Mark => State switch
    {
        ConnectionStepState.Done => "✓",
        ConnectionStepState.Running => "●",
        ConnectionStepState.Stopped => "✕",
        _ => "○",
    };

    partial void OnStateChanged(ConnectionStepState value)
    {
        OnPropertyChanged(nameof(IsRunning));
        OnPropertyChanged(nameof(IsDone));
        OnPropertyChanged(nameof(IsStopped));
        OnPropertyChanged(nameof(Mark));
    }
}

/// <summary>
/// How far along a tab's connection is.
/// </summary>
/// <remarks>
/// A tab exists before its session does — see <see cref="TerminalTabViewModel"/> — so "is there a pane
/// behind this" is a question the strip and the window both have to be able to ask. Three states rather
/// than a nullable session id, because <see cref="Failed"/> and <see cref="Connecting"/> are both "no
/// session" and only one of them is still worth waiting for.
/// </remarks>
internal enum TerminalTabState
{
    /// <summary>The connection is being made. There is no pane yet.</summary>
    Connecting = 0,

    /// <summary>A session was opened, and the renderer has a pane for it.</summary>
    Open = 1,

    /// <summary>The connection did not happen. There is no pane, and there never will be for this tab.</summary>
    Failed = 2,
}

/// <summary>
/// One terminal, as a tab: from the moment connecting starts to the moment the tab is closed.
/// </summary>
/// <remarks>
/// <para>
/// A tab is a session id and a handful of strings — the address, and now the three connection facts the
/// status bar draws beside CONNECTED. It holds no terminal and owns nothing: the pane, its scrollback and the
/// shell behind it all live in the renderer and in <c>TerminalWorkspace</c>, and selecting a tab is one frame
/// telling the page which pane to show. That is what makes tabs cheap here — the expensive object is the
/// WebView, and there is one of those however many tabs are open.
/// </para>
/// <para>
/// <b>A tab starts before its session does.</b> Connecting is a network round trip that can take as long as
/// a DNS lookup and a handshake take, and the strip is where that is admitted to: the tab appears at the
/// moment the user asks for it, carrying <see cref="Status"/> instead of a pane, and becomes a real terminal
/// when <see cref="Opened"/> is called. Nothing about the rest of the application waits for that — which is
/// the point, because the alternative is a window that does nothing visible for ten seconds.
/// </para>
/// <para>
/// <b>Tabs belong to the shell, not to the vault.</b> Locking disposes the vault and every key it held, and
/// deliberately leaves shells running — so a tab list rebuilt per unlock would lose track of sessions that
/// are still connected, and the unlock screen's count of them would be the only place they appeared. The
/// shell outlives every lock, and so does this.
/// </para>
/// </remarks>
internal sealed partial class TerminalTabViewModel : ObservableObject
{
    /// <summary>A tab for a connection that is still being made.</summary>
    /// <param name="label">The host's name, as the vault has it.</param>
    /// <param name="address">Who this will be logged in as, and where.</param>
    internal TerminalTabViewModel(string label, string address)
    {
        Label = label;
        Address = address;
        isLive = false;

        Steps =
        [
            new ConnectionStepViewModel(ConnectionStep.PreparingTerminal, "Starting the terminal"),
            new ConnectionStepViewModel(ConnectionStep.Reaching, "Reaching the host"),
            new ConnectionStepViewModel(ConnectionStep.CheckingHostKey, "Checking the host key"),
            new ConnectionStepViewModel(ConnectionStep.Authenticating, "Signing in"),
            new ConnectionStepViewModel(ConnectionStep.OpeningShell, "Opening the shell"),
        ];

        // The first step is running before anything is awaited, because it is: the tab is created in the
        // same turn as the click and the renderer wait starts immediately after. A list that opened with
        // every row pending would show a connection that had not begun, which is one turn of the dispatcher
        // away from being untrue and is the turn the card is first drawn in.
        status = Steps[0].Caption;
        Steps[0].State = ConnectionStepState.Running;
    }

    /// <summary>A tab for a session that is already open.</summary>
    /// <param name="sessionId">Identifies this terminal to the renderer.</param>
    /// <param name="label">The host's name, as the vault has it.</param>
    /// <param name="address">Who this is logged in as, and where.</param>
    internal TerminalTabViewModel(uint sessionId, string label, string address)
        : this(label, address)
    {
        SessionId = sessionId;
        state = TerminalTabState.Open;
        status = string.Empty;
        isLive = true;

        // A session that already exists got through every step by definition, even though this tab watched
        // none of them happen — an adopted session is one whose connecting tab the user closed. The list is
        // never drawn for a tab in this state; it is filled in so that nothing downstream has to treat "open"
        // as a fourth answer to "how far did it get".
        CompleteSteps();
    }

    /// <summary>
    /// Identifies this terminal to the renderer, or zero while there is no session.
    /// </summary>
    /// <remarks>
    /// Zero is not a session id the workspace ever hands out — it counts from one — so it can stand for
    /// "not connected yet" without a nullable that every caller would have to unwrap. <see cref="HasSession"/>
    /// is what the window asks rather than this.
    /// </remarks>
    internal uint SessionId { get; private set; }

    internal string Label { get; }

    /// <summary>The account and endpoint, for the pane header and the status bar.</summary>
    internal string Address { get; }

    /// <summary>
    /// When the session behind this tab opened, for the status bar's elapsed timer — or null while there is
    /// none.
    /// </summary>
    /// <remarks>
    /// Set by <c>MainWindowViewModel</c> from its own clock at the same moment it calls <see cref="Opened"/>
    /// or constructs a tab that already carries a session id, rather than read here from
    /// <see cref="TimeProvider.System"/> directly — a view model with its own notion of "now" is a second
    /// clock the shell's tests would have no way to control. Left on a tab whose session has since ended:
    /// the pane still holds the scrollback, and "how long that shell ran" stays a true fact about it after it
    /// stops being live.
    /// </remarks>
    internal DateTimeOffset? StartedAt { get; set; }

    /// <summary>
    /// The negotiated server-to-client cipher, for the status bar — or null while there is no session.
    /// </summary>
    /// <remarks>
    /// Set alongside <see cref="StartedAt"/>, from the same event, and kept for the same reason: a dead tab's
    /// pane still holds the scrollback of a session that really did negotiate this cipher, and clearing the
    /// fact when the shell ends would not make it less true of what is on screen.
    /// </remarks>
    internal string? Cipher { get; set; }

    /// <summary>The accepted host key's algorithm, e.g. <c>ssh-ed25519</c> — or null while there is no session.</summary>
    /// <remarks>See <see cref="Cipher"/>; set and kept the same way, for the same reason.</remarks>
    internal string? HostKeyAlgorithm { get; set; }

    /// <summary>
    /// The display name of the key or credential that authenticated, or null when a typed password did, or
    /// null while there is no session.
    /// </summary>
    /// <remarks>
    /// Three different reasons collapse to the same null, and that is deliberate: nothing downstream needs to
    /// tell a session with no identity to name apart from a tab that has not opened one yet — both mean the
    /// status bar shows the host-key algorithm alone, with no ` · name` after it.
    /// </remarks>
    internal string? IdentityLabel { get; set; }

    /// <summary>
    /// How far this connection got, step by step, for the card that stands in for the pane.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Fixed at construction and never added to or removed from — the steps of a connection are known before
    /// it starts, and only their state changes — so a plain array is enough and the view needs no collection
    /// change notification for it.
    /// </para>
    /// <para>
    /// <b>Every row here is reported, not guessed.</b> The states come from
    /// <see cref="SshConnectionPhase"/>, raised by the handshake itself at the moment each part of it begins.
    /// Nothing on this list is a timer, a fraction, or a step this view model decided had probably finished
    /// by now. That is the whole reason it is worth showing: a card that invented plausible progress would be
    /// indistinguishable from one that had stopped receiving any.
    /// </para>
    /// </remarks>
    internal IReadOnlyList<ConnectionStepViewModel> Steps { get; }

    /// <summary>How many steps are behind the attempt, for the card's track.</summary>
    /// <remarks>
    /// Counted rather than stored, and it counts <see cref="ConnectionStepState.Done"/> alone: the running
    /// step is deliberately not half a step. The track fills to where the attempt has actually got to and
    /// stops there, which is the same promise the list itself makes.
    /// </remarks>
    internal int StepsDone => Steps.Count(step => step.IsDone);

    /// <summary>How many steps there are, for the card's track.</summary>
    internal int StepCount => Steps.Count;

    /// <inheritdoc cref="TerminalTabState" />
    [ObservableProperty]
    private TerminalTabState state;

    /// <summary>
    /// What this tab has to say for itself while it has no pane.
    /// </summary>
    /// <remarks>
    /// Empty once a session is open, because from then on the pane speaks for itself — anything written here
    /// would be a second, staler account of what the terminal is already showing.
    /// </remarks>
    [ObservableProperty]
    private string status;

    /// <summary>
    /// Whether the shell behind this tab is still running.
    /// </summary>
    /// <remarks>
    /// Cleared when the workspace says the session ended, never inferred from the tab being closed — closing
    /// a tab removes it, and a removed tab has nothing left to report. A dead tab is kept on purpose: its
    /// pane still holds the scrollback, and the last thing the remote said is usually why the shell ended.
    /// </remarks>
    [ObservableProperty]
    private bool isLive;

    /// <summary>
    /// Whether this is the tab whose pane is showing.
    /// </summary>
    /// <remarks>
    /// A flag on the tab as well as a selection on the shell, because the strip is an
    /// <c>ItemsControl</c> of buttons rather than a control that owns a selection — and a button has no
    /// <c>:selected</c> pseudo-class to style against. The shell writes it; nothing else does.
    /// </remarks>
    [ObservableProperty]
    private bool isSelected;

    /// <summary>
    /// Whether this tab is the thing the window is currently showing.
    /// </summary>
    /// <remarks>
    /// Not the same question as <see cref="IsSelected"/>, and the strip has to ask this one. The selection
    /// survives navigating away — that is what makes the strip a way back to a terminal rather than a way to
    /// lose it — so a tab that stayed lit while preferences filled the window would be a second "you are
    /// here" mark pointing at something nobody can see. The rail's own entries make exactly this distinction;
    /// see <c>MainWindowViewModel.IsHostsShowing</c>. The shell writes it, from the selection and the surface
    /// together.
    /// </remarks>
    [ObservableProperty]
    private bool isShowing;

    /// <summary>Whether there is a pane behind this tab.</summary>
    internal bool HasSession => State is TerminalTabState.Open;

    /// <summary>Whether this tab is still waiting on a connection.</summary>
    internal bool IsConnecting => State is TerminalTabState.Connecting;

    /// <summary>Whether this tab is a connection that never happened.</summary>
    internal bool IsFailed => State is TerminalTabState.Failed;

    /// <summary>
    /// Records that the connection has reached a named step.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Everything before <paramref name="step"/> is marked done, because a phase that has begun is proof the
    /// ones before it ended — the handshake is a sequence and there is no way to be at one point in it
    /// without having passed the earlier ones. That is also what covers a step too fast to observe: it is
    /// closed by its successor rather than needing a report of its own.
    /// </para>
    /// <para>
    /// Monotonic, and silently so. A report that has already been passed is ignored rather than rewinding
    /// the list, because the one thing that can produce one is a retry after the host-key question, and a
    /// card that jumped backwards would read as the connection having come undone.
    /// </para>
    /// </remarks>
    internal void Advance(ConnectionStep step)
    {
        if (State is not TerminalTabState.Connecting)
        {
            // Nothing to draw and nothing to correct. A late report from a handshake that has since
            // finished or been given up on is not worth reopening a settled tab for.
            return;
        }

        var reached = Steps.FirstOrDefault(row => row.Step == step);

        if (reached is null || reached.IsDone)
        {
            return;
        }

        foreach (var row in Steps)
        {
            if (row.Step < step)
            {
                row.State = ConnectionStepState.Done;
            }
            else if (row.Step == step)
            {
                row.State = ConnectionStepState.Running;
            }
        }

        Status = reached.Caption;
        OnPropertyChanged(nameof(StepsDone));
    }

    /// <summary>Takes ownership of the session that has just opened for this tab.</summary>
    internal void Opened(uint sessionId)
    {
        SessionId = sessionId;
        Status = string.Empty;
        IsLive = true;
        State = TerminalTabState.Open;

        CompleteSteps();
    }

    /// <summary>
    /// Records that the connection this tab was opened for did not happen.
    /// </summary>
    /// <remarks>
    /// The tab stays, and that is deliberate: connecting no longer blocks the window, so by the time a
    /// refusal arrives the user is quite likely looking at something else — and a tab that vanished would
    /// take the only account of what went wrong with it. It is closed the way every other tab is.
    /// </remarks>
    internal void Failed(string reason)
    {
        Status = reason;
        IsLive = false;

        // Before the state change, so the list is already correct the first time a view asks. The step that
        // was running is where it stopped, and the ones behind it stay done: how far a refused connection
        // got is the most useful thing the card still knows, and it is the difference between "that host is
        // not there" and "that host is there and would not have me".
        foreach (var row in Steps)
        {
            if (row.IsRunning)
            {
                row.State = ConnectionStepState.Stopped;
            }
        }

        State = TerminalTabState.Failed;
    }

    /// <summary>Marks every step done, for a connection that is no longer being waited on.</summary>
    private void CompleteSteps()
    {
        foreach (var row in Steps)
        {
            row.State = ConnectionStepState.Done;
        }

        OnPropertyChanged(nameof(StepsDone));
    }

    partial void OnStateChanged(TerminalTabState value)
    {
        OnPropertyChanged(nameof(HasSession));
        OnPropertyChanged(nameof(IsConnecting));
        OnPropertyChanged(nameof(IsFailed));
    }
}
