namespace DodoSSH.Client.Session;

/// <summary>
/// How far withdrawing a device key got.
/// </summary>
/// <remarks>
/// Three outcomes rather than a boolean, because the middle one is a state the user has to be told about
/// and can act on. The local half always happens; only the server half can be out of reach.
/// </remarks>
public enum DeviceRevocation
{
    /// <summary>There was no device key here to withdraw.</summary>
    NothingRegistered,

    /// <summary>
    /// This machine can no longer unlock itself, but the account still lists the device.
    /// </summary>
    /// <remarks>
    /// Offline. The wrap on the server is the user's bundle sealed to a key this machine has now destroyed,
    /// so nothing can open it — but it is still a row that should not be there, and a reinstall would pull
    /// it down again and offer a device unlock that cannot work.
    /// </remarks>
    LocalOnly,

    /// <summary>Gone from this machine and from the account.</summary>
    Complete,
}

/// <summary>
/// Where this machine keeps the private half of its device key.
/// </summary>
/// <remarks>
/// <para>
/// An interface because the answer is a platform decision and a security decision, recorded in
/// ADR 0007: on Windows a gesture guards it, and the gesture is the whole value — it is what stops a
/// process running as the user from unlocking the vault silently. Nothing in this assembly should know
/// which gesture, and nothing above it should be able to skip one.
/// </para>
/// <para>
/// <b>Exactly 32 bytes, and only because the cache key moved.</b> This holds the X25519 device private
/// key and nothing else. It would have had to hold the local cache key too — a second live secret at
/// rest, going stale on every passphrase change — had that key not been re-keyed to the identity bundle
/// first. See docs/crypto.md §3.2.
/// </para>
/// <para>
/// Every member may fail or refuse, and refusal is ordinary rather than exceptional: a user can cancel a
/// fingerprint prompt, a Hello key is invalidated when the PIN is reset, and a machine may have no
/// keystore at all. <see cref="TryLoadAsync"/> therefore returns null rather than throwing, and the
/// caller's answer is always the same — ask for the passphrase.
/// </para>
/// </remarks>
public interface IDeviceKeyStore
{
    /// <summary>Whether this machine can keep a device key at all.</summary>
    /// <remarks>
    /// Asked before offering to register one. Registering a device whose private half does not survive
    /// the process would put a wrap on the server that nothing can ever open, and make the account's
    /// device list claim a capability this machine does not have.
    /// </remarks>
    ValueTask<bool> IsAvailableAsync(CancellationToken cancellationToken);

    /// <summary>Stores the device private key, replacing any already held.</summary>
    /// <param name="devicePrivateKey">The raw 32-byte X25519 scalar.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    ValueTask SaveAsync(ReadOnlyMemory<byte> devicePrivateKey, CancellationToken cancellationToken);

    /// <summary>
    /// Retrieves the device private key, prompting for whatever guards it.
    /// </summary>
    /// <returns>
    /// The raw scalar, or <see langword="null"/> if there is none, the user declined, or the platform
    /// has invalidated it. The three are deliberately not distinguished: the caller does the same thing
    /// in each case, and a message naming which one would be describing the keystore rather than telling
    /// the user anything they can act on.
    /// </returns>
    ValueTask<byte[]?> TryLoadAsync(CancellationToken cancellationToken);

    /// <summary>Discards the stored key.</summary>
    /// <remarks>
    /// Local only. The server's wrap row outlives this and has to be deleted separately, or the account
    /// will go on listing a device that can no longer unlock anything.
    /// </remarks>
    ValueTask ForgetAsync(CancellationToken cancellationToken);
}

/// <summary>
/// A machine with nowhere to keep a device key.
/// </summary>
/// <remarks>
/// What the application composes until a real keystore is wired up, and the honest answer for a platform
/// that has none. Reports unavailable and holds nothing, so unlock asks for the passphrase exactly as it
/// did before any of this existed — a placeholder that changes no behaviour rather than one that pretends.
/// </remarks>
public sealed class UnavailableDeviceKeyStore : IDeviceKeyStore
{
    /// <inheritdoc />
    public ValueTask<bool> IsAvailableAsync(CancellationToken cancellationToken) =>
        ValueTask.FromResult(false);

    /// <inheritdoc />
    public ValueTask SaveAsync(ReadOnlyMemory<byte> devicePrivateKey, CancellationToken cancellationToken) =>
        throw new NotSupportedException(
            "This machine has no device key store. Check IsAvailableAsync before offering to register one.");

    /// <inheritdoc />
    public ValueTask<byte[]?> TryLoadAsync(CancellationToken cancellationToken) =>
        ValueTask.FromResult<byte[]?>(null);

    /// <inheritdoc />
    public ValueTask ForgetAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask;
}
