namespace DodoSSH.Client.Ssh;

/// <summary>How to authenticate to a host.</summary>
/// <remarks>
/// Always decrypted from the vault immediately before use and never persisted outside it. Because
/// SSH terminates on the client, using a credential requires its plaintext here — which is exactly
/// why the <c>Connect</c> permission is a UI hint and not an enforceable boundary. See ADR 0001.
/// </remarks>
public abstract record SshCredential;

/// <summary>Password authentication, and keyboard-interactive where the server prefers it.</summary>
public sealed record SshPasswordCredential(string Password) : SshCredential;

/// <summary>Public-key authentication.</summary>
/// <param name="PrivateKeyPem">The private key in PEM form, as stored in the vault.</param>
/// <param name="Passphrase">Passphrase protecting the key, when it has one.</param>
public sealed record SshPrivateKeyCredential(byte[] PrivateKeyPem, string? Passphrase) : SshCredential;

/// <summary>
/// A SOCKS5 proxy on this machine's loopback interface, through which a host is reached.
/// </summary>
/// <param name="Port">The port it is listening on.</param>
/// <remarks>
/// <para>
/// ◆ <b>A port and nothing else, so a proxy anywhere but loopback cannot be expressed.</b> Both things
/// that will produce one of these listen on <c>127.0.0.1</c> — SSH.NET's own dynamic forward over a
/// bastion, and the bridge that will front the server relay — and a SOCKS proxy bound to any other
/// interface is an open proxy into whatever network the machine is on, for as long as the shell is up.
/// Leaving the host out of this type is what makes that unrepresentable rather than merely unlikely; it is
/// the same reason <c>AuthenticationChoice</c> carries a kind beside its id.
/// </para>
/// <para>
/// <b>SOCKS5 rather than a plain pipe, and that is what keeps host key pinning honest.</b> The target's
/// real name and port stay in <see cref="SshConnectionRequest.Host"/> and <see cref="SshConnectionRequest.Port"/>
/// and travel to the proxy in the CONNECT request, so the connection is *about* the target throughout —
/// nothing downstream has to be told that the address dialled is not the address being spoken to. A dumb
/// pipe would have meant handing SSH.NET <c>127.0.0.1</c> and remembering, everywhere else, that it was a
/// stand-in. See the gate in <c>SshNetConnectionFactory</c>, which pins what this request names.
/// </para>
/// <para>
/// Nothing in this assembly opens one. The proxy is somebody else's — a forward on a bastion connection,
/// or the relay bridge — and its lifetime belongs to whoever opened it, which must outlast the connection
/// made through it. See <c>docs/reaching-a-host-you-cannot-dial.md</c>.
/// </para>
/// </remarks>
public sealed record SshLoopbackProxy(int Port);

/// <summary>Everything needed to reach one host.</summary>
/// <param name="Host">Hostname or address.</param>
/// <param name="Port">Port.</param>
/// <param name="Username">Remote account.</param>
/// <param name="Credential">How to authenticate.</param>
/// <param name="ConnectTimeout">How long to wait for the transport and handshake.</param>
/// <param name="Proxy">
/// A loopback SOCKS5 proxy to reach <paramref name="Host"/> through, or null to dial it directly.
/// <para>
/// Last and optional, so that every existing caller — which is every connection this product makes today —
/// keeps meaning exactly what it did. A host that can be dialled is still dialled.
/// </para>
/// </param>
public sealed record SshConnectionRequest(
    string Host,
    int Port,
    string Username,
    SshCredential Credential,
    TimeSpan? ConnectTimeout = null,
    SshLoopbackProxy? Proxy = null);

/// <summary>An interactive shell over a pseudo-terminal.</summary>
public interface ISshShellSession : IAsyncDisposable
{
    /// <summary>Whether the channel is still usable.</summary>
    bool IsOpen { get; }

    /// <summary>Reads whatever output is available, blocking until at least one byte arrives.</summary>
    /// <returns>Bytes read, or 0 once the remote closes the channel.</returns>
    ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken);

    /// <summary>Sends keystrokes to the remote.</summary>
    ValueTask WriteAsync(ReadOnlyMemory<byte> data, CancellationToken cancellationToken);

    /// <summary>
    /// Tells the remote the terminal has been resized.
    /// </summary>
    /// <remarks>
    /// Verified to reach the remote against a real sshd; see <c>PtyAndResizeSpikeTests</c>. Sizes
    /// that are not <see cref="TerminalSize.IsUsable"/> are dropped rather than forwarded.
    /// </remarks>
    void Resize(TerminalSize size);
}

/// <summary>An authenticated connection to one host.</summary>
public interface ISshConnection : IAsyncDisposable
{
    /// <summary>Whether the transport is still up.</summary>
    bool IsConnected { get; }

    /// <summary>The host key that was accepted for this connection.</summary>
    HostKeyPresentation HostKey { get; }

    /// <summary>
    /// The negotiated server-to-client encryption algorithm, e.g. <c>aes256-gcm@openssh.com</c>.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Read once, immediately after the handshake, off SSH.NET's own <c>ConnectionInfo.CurrentServerEncryption</c>.
    /// The only event that could make this stale is a rekey, and SSH.NET raises no event for one and exposes no
    /// way to ask again — there is nothing behind this property to go and re-read. A captured value is therefore
    /// not a snapshot that might drift; it is the only value there has ever been a moment to observe.
    /// </para>
    /// <para>
    /// <b>Server-to-client, not client-to-server.</b> SSH negotiates the two directions independently and a
    /// server is free to choose differently for each, so the two can in principle disagree. This is the
    /// direction the bytes drawn on a terminal pane travelled in, which is the fact a status bar showing what
    /// the screen is made of should be naming.
    /// </para>
    /// </remarks>
    string Cipher { get; }

    /// <summary>Opens an interactive shell with a pseudo-terminal.</summary>
    Task<ISshShellSession> OpenShellAsync(TerminalSize size, CancellationToken cancellationToken);
}

/// <summary>
/// How far a connection being made has got.
/// </summary>
/// <remarks>
/// <para>
/// These are the boundaries a client can actually observe, and there are deliberately no others. SSH.NET
/// runs the whole handshake inside one <c>ConnectAsync</c> and raises exactly one event from the middle of
/// it — <c>HostKeyReceived</c>, once the key exchange has produced a key to show. That event is the only
/// interior moment there is, so it is the only interior phase named here: everything before it is
/// <see cref="Reaching"/> and everything after it is <see cref="Authenticating"/>.
/// </para>
/// <para>
/// ◆ <b>Nothing here is a guess about elapsed time or a fraction of the way through.</b> Each value is
/// reported at the instant the thing it names actually starts, which is what makes it safe for a screen to
/// draw as fact. A phase that took no measurable time is reported anyway and simply passes at once — that
/// is a true account of a fast handshake, not a step that was skipped. See the transfer strip's own remark
/// in TransfersScreen.axaml for why this design does not invent furniture for states it cannot measure.
/// </para>
/// </remarks>
public enum SshConnectionPhase
{
    /// <summary>Resolving the name, opening the socket, and exchanging keys. Before any key is known.</summary>
    Reaching = 0,

    /// <summary>The server has offered a host key, and its trust is being decided.</summary>
    CheckingHostKey = 1,

    /// <summary>The key was accepted. The credential is being offered.</summary>
    Authenticating = 2,

    /// <summary>Authenticated. A pseudo-terminal and a shell channel are being opened.</summary>
    OpeningShell = 3,
}

/// <summary>Opens connections, enforcing host key trust before authenticating.</summary>
public interface ISshConnectionFactory
{
    /// <summary>
    /// Connects and authenticates.
    /// </summary>
    /// <param name="request">What to connect to, as whom, and with what.</param>
    /// <param name="progress">
    /// Told each phase as it begins, or null to report nothing. Called from whichever thread the handshake
    /// is on — SSH.NET raises host key verification on its own — so an implementation that touches a UI must
    /// marshal for itself.
    /// </param>
    /// <param name="cancellationToken">Abandons the attempt.</param>
    /// <exception cref="SshHostKeyUnknownException">
    /// The host has no pinned key. The caller must show the fingerprint, and only on explicit
    /// confirmation record it via <see cref="IKnownHostStore.TrustAsync"/> and retry.
    /// </exception>
    /// <exception cref="SshHostKeyMismatchException">
    /// The presented key differs from the pin. There is no retry path: this is a hard block.
    /// </exception>
    Task<ISshConnection> ConnectAsync(
        SshConnectionRequest request,
        IProgress<SshConnectionPhase>? progress,
        CancellationToken cancellationToken);
}
