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>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>
public sealed record SshConnectionRequest(
    string Host,
    int Port,
    string Username,
    SshCredential Credential,
    TimeSpan? ConnectTimeout = 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>Opens an interactive shell with a pseudo-terminal.</summary>
    Task<ISshShellSession> OpenShellAsync(TerminalSize size, CancellationToken cancellationToken);
}

/// <summary>Opens connections, enforcing host key trust before authenticating.</summary>
public interface ISshConnectionFactory
{
    /// <summary>
    /// Connects and authenticates.
    /// </summary>
    /// <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, CancellationToken cancellationToken);
}
