using DodoSSH.Client.Session;
using global::Android.App;
using global::Android.Security.Keystore;
using global::Java.Security;
using global::Javax.Crypto;
using global::Javax.Crypto.Spec;

namespace DodoSSH.Client.Android.Platform;

/// <summary>
/// This phone's device key, wrapped by a hardware-held key that a fingerprint releases.
/// </summary>
/// <remarks>
/// <para>
/// The Android counterpart of the desktop head's <c>WindowsDeviceKeyStore</c>, and a closer match to what
/// the unlock screen wants than that one is: the Windows store leans on DPAPI plus a TPM-held key and a
/// Hello gesture, whereas here the gesture is a property of the key itself and the platform will not
/// release it without one. See ADR 0007 and docs/android-port.md §4.
/// </para>
/// <para>
/// <b>The X25519 scalar is not stored in the keystore</b>, and cannot be: Android's keystore holds keys it
/// generates and refuses to export them, while what the vault needs back is the raw 32 bytes. So the
/// keystore holds an AES-GCM key that never leaves the secure hardware, and that key encrypts the scalar
/// into an ordinary file beside the cache. The file is useless on its own — on this phone as much as on
/// any other — which is the same shape the Windows store already has.
/// </para>
/// <para>
/// <b>StrongBox is asked for and not required.</b> Where the phone has a separate security chip the key
/// lives there; where it does not, generation throws
/// <see cref="StrongBoxUnavailableException"/> and the key is made in the TEE instead. Refusing to fall
/// back would mean a mid-range phone reporting no device key at all, which costs a real user a real
/// feature to buy a distinction the threat model does not draw.
/// </para>
/// <para>
/// <b>Every failure here is answered by returning null rather than throwing</b>, exactly as the interface
/// asks. A user can cancel the prompt, a re-enrolled fingerprint invalidates the key permanently, and a
/// phone can have no enrolled biometric at all — and the caller's answer to all three is the same one:
/// ask for the passphrase.
/// </para>
/// </remarks>
internal sealed class AndroidDeviceKeyStore(ClientPaths paths) : IDeviceKeyStore
{
    private const string KeystoreName = "AndroidKeyStore";
    private const string KeyAlias = "dodossh.device";
    private const string Transformation = "AES/GCM/NoPadding";

    /// <summary>
    /// The nonce is stored ahead of the ciphertext rather than derived.
    /// </summary>
    /// <remarks>
    /// The cipher picks it: a GCM key that is asked to encrypt twice under a caller-chosen nonce is one
    /// misuse away from losing the key, and Android's keystore refuses a caller-supplied IV for exactly
    /// that reason. Twelve bytes is what it generates.
    /// </remarks>
    private const int NonceBytes = 12;

    /// <inheritdoc />
    /// <remarks>
    /// Three things have to be true, and the third is the one that is easy to forget: the platform has a
    /// keystore, the phone has a screen lock, and something is actually enrolled to satisfy it. A phone
    /// with no lock screen can still generate a key that requires authentication — and then no gesture can
    /// ever release it.
    /// </remarks>
    public ValueTask<bool> IsAvailableAsync(CancellationToken cancellationToken)
    {
        try
        {
            var keyguard = PhoneEnvironment.Require()
                .GetSystemService(global::Android.Content.Context.KeyguardService) as KeyguardManager;

            return ValueTask.FromResult(keyguard?.IsDeviceSecure == true);
        }
        catch (Exception exception) when (exception is not OutOfMemoryException)
        {
            return ValueTask.FromResult(false);
        }
    }

    /// <inheritdoc />
    public async ValueTask SaveAsync(ReadOnlyMemory<byte> devicePrivateKey, CancellationToken cancellationToken)
    {
        var key = GenerateWrappingKey();

        var cipher = Cipher.GetInstance(Transformation)
            ?? throw new InvalidOperationException("This phone has no AES/GCM/NoPadding cipher.");

        cipher.Init(CipherMode.EncryptMode, key);

        // Authenticated before the key is usable, exactly as loading is. Registering a device is itself a
        // decision worth a gesture — it is the moment this phone gains the ability to open the vault
        // without the passphrase.
        await BiometricGate
            .AuthenticateAsync(cipher, "Register this phone", cancellationToken)
            .ConfigureAwait(false);

        var sealed_ = cipher.DoFinal(devicePrivateKey.ToArray())
            ?? throw new InvalidOperationException("The keystore cipher returned nothing.");

        var nonce = cipher.GetIV() ?? throw new InvalidOperationException("The keystore cipher chose no IV.");

        var blob = new byte[nonce.Length + sealed_.Length];
        nonce.CopyTo(blob, 0);
        sealed_.CopyTo(blob, nonce.Length);

        paths.EnsureCreated();

        await File.WriteAllBytesAsync(paths.DeviceKeyFile, blob, cancellationToken).ConfigureAwait(false);
    }

    /// <inheritdoc />
    public async ValueTask<byte[]?> TryLoadAsync(CancellationToken cancellationToken)
    {
        try
        {
            if (!File.Exists(paths.DeviceKeyFile))
            {
                return null;
            }

            var blob = await File.ReadAllBytesAsync(paths.DeviceKeyFile, cancellationToken).ConfigureAwait(false);

            if (blob.Length <= NonceBytes)
            {
                return null;
            }

            var store = KeyStore.GetInstance(KeystoreName)
                ?? throw new InvalidOperationException("This phone has no AndroidKeyStore.");

            store.Load(null);

            // Null when the key was invalidated — a re-enrolled fingerprint or a reset screen lock does
            // this, and it is permanent by design. The wrapped file is unopenable from here on, so the
            // honest answer is the same as having no key: ask for the passphrase.
            if (store.GetKey(KeyAlias, null) is not IKey key)
            {
                return null;
            }

            var cipher = Cipher.GetInstance(Transformation)
                ?? throw new InvalidOperationException("This phone has no AES/GCM/NoPadding cipher.");

            cipher.Init(CipherMode.DecryptMode, key, new GCMParameterSpec(128, blob, 0, NonceBytes));

            await BiometricGate
                .AuthenticateAsync(cipher, "Unlock DodoSSH", cancellationToken)
                .ConfigureAwait(false);

            return cipher.DoFinal(blob, NonceBytes, blob.Length - NonceBytes);
        }
        catch (Exception exception) when (exception is not OutOfMemoryException)
        {
            // Deliberately flat. KeyPermanentlyInvalidatedException, UserNotAuthenticatedException, a
            // cancelled prompt and a truncated file are four different stories with one ending, and the
            // interface says so: distinguishing them would describe the keystore rather than tell the user
            // anything they can act on.
            return null;
        }
    }

    /// <inheritdoc />
    public ValueTask ForgetAsync(CancellationToken cancellationToken)
    {
        try
        {
            var store = KeyStore.GetInstance(KeystoreName);
            store?.Load(null);
            store?.DeleteEntry(KeyAlias);
        }
        catch (Exception exception) when (exception is not OutOfMemoryException)
        {
            // Best effort: the file going is what actually withdraws this phone's ability to unlock, and
            // an orphaned keystore entry opens nothing.
        }

        // After the keystore entry, not before. A file left behind with its key already deleted is merely
        // unopenable; a key left behind with its file already gone is the same. Order costs nothing here,
        // but the file is the one that matters, so it goes last and unconditionally.
        if (File.Exists(paths.DeviceKeyFile))
        {
            File.Delete(paths.DeviceKeyFile);
        }

        return ValueTask.CompletedTask;
    }

    /// <remarks>
    /// <c>SetInvalidatedByBiometricEnrollment</c> is on, which is the setting that makes this worth having:
    /// without it, somebody who can add their own fingerprint to an unlocked phone inherits the ability to
    /// unlock the vault. With it, enrolling a new fingerprint destroys the key and the phone falls back to
    /// the passphrase — which is the correct outcome and the reason the load path treats invalidation as
    /// ordinary rather than exceptional.
    /// </remarks>
    private static IKey GenerateWrappingKey()
    {
        static KeyGenParameterSpec.Builder Spec() =>
            new KeyGenParameterSpec.Builder(KeyAlias, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
                .SetBlockModes(KeyProperties.BlockModeGcm)!
                .SetEncryptionPaddings(KeyProperties.EncryptionPaddingNone)!
                .SetKeySize(256)!
                .SetUserAuthenticationRequired(true)!
                .SetInvalidatedByBiometricEnrollment(true)!;

        var generator = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, KeystoreName)
            ?? throw new InvalidOperationException("This phone has no AES key generator in its keystore.");

        try
        {
            generator.Init(Spec().SetIsStrongBoxBacked(true)!.Build());

            return generator.GenerateKey()!;
        }
        catch (StrongBoxUnavailableException)
        {
            // No separate security chip. The TEE-backed key is still hardware-held and still gated by the
            // same gesture; see the class remarks for why this is a fallback rather than a refusal.
            generator.Init(Spec().Build());

            return generator.GenerateKey()!;
        }
    }
}
