using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace DodoSSH.Client.App.Platform;

/// <summary>
/// The framework constants <see cref="MacDeviceKeyStore"/> passes to CoreFoundation and Security.
/// </summary>
/// <remarks>
/// <para>
/// Every field here is a pointer read out of a loaded framework rather than a value this code could
/// write down. The dictionaries these go into are matched by pointer identity, so a CFString built with
/// the same characters is a different key and the lookup misses — see <see cref="MacSecurity.Constant"/>
/// for the indirection that trips people.
/// </para>
/// <para>
/// <b>Resolved once and cached, and the caching is what makes the failure survivable.</b> Two frameworks
/// and nineteen symbols is a lot of things to be wrong about, and the useful property is that being
/// wrong about any one of them shows up here — as <see cref="Complete"/> being false — rather than
/// three calls later as a parameter error. A store that reports itself unavailable sends the user back
/// to their passphrase; a store that half works corrupts the moment somebody registers a device.
/// </para>
/// <para>
/// <see cref="Lazy{T}"/> rather than a static constructor, because a type initialiser that throws
/// poisons the type for the life of the process and turns a missing symbol into a
/// <c>TypeInitializationException</c> at every later call site. The load is done inside a try instead,
/// and its failure is a value.
/// </para>
/// </remarks>
[SupportedOSPlatform("macos")]
internal sealed class MacSymbols
{
    private static readonly Lazy<MacSymbols> Cached = new(Load, LazyThreadSafetyMode.ExecutionAndPublication);

    private MacSymbols()
    {
    }

    /// <summary>Whether every symbol resolved.</summary>
    /// <remarks>
    /// Checked by every caller before any of the pointers are used. It is one check rather than
    /// nineteen, which is the only reason the call sites in <see cref="MacDeviceKeyStore"/> are
    /// readable.
    ///
    /// Computed rather than stored, so that the instance returned when a framework will not load at all
    /// — every field left at zero — answers false without that having to be set anywhere. One rule,
    /// applied to the only state there is.
    /// </remarks>
    internal bool Complete => AllResolved();

    // CoreFoundation.
    internal IntPtr True { get; private init; }

    internal IntPtr TypeDictionaryKeyCallBacks { get; private init; }

    internal IntPtr TypeDictionaryValueCallBacks { get; private init; }

    // Security: item classes and query keys.
    internal IntPtr Class { get; private init; }

    internal IntPtr ClassKey { get; private init; }

    internal IntPtr ReturnRef { get; private init; }

    internal IntPtr UseDataProtectionKeychain { get; private init; }

    internal IntPtr UseOperationPrompt { get; private init; }

    // Security: key attributes.
    internal IntPtr AttrKeyType { get; private init; }

    internal IntPtr AttrKeySizeInBits { get; private init; }

    internal IntPtr AttrTokenId { get; private init; }

    internal IntPtr AttrIsPermanent { get; private init; }

    internal IntPtr AttrApplicationTag { get; private init; }

    internal IntPtr AttrAccessControl { get; private init; }

    internal IntPtr PrivateKeyAttrs { get; private init; }

    // Security: attribute values.
    internal IntPtr KeyTypeEcSecPrimeRandom { get; private init; }

    internal IntPtr TokenIdSecureEnclave { get; private init; }

    internal IntPtr AccessibleWhenUnlockedThisDeviceOnly { get; private init; }

    /// <summary>
    /// <c>kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM</c>.
    /// </summary>
    /// <remarks>
    /// The one algorithm the Secure Enclave's P-256 keys support for encryption, and the reason this
    /// store wraps rather than signs. The long name spells out the whole construction: an ephemeral
    /// key agreed against the enclave's public half with cofactor ECDH, run through the X9.63 KDF with
    /// SHA-256, used as an AES-GCM key. The ephemeral public key travels in the output, which is why the
    /// ciphertext is larger than the 32 bytes going in and why nothing else has to be stored beside it.
    /// </remarks>
    internal IntPtr EciesAlgorithm { get; private init; }

    /// <summary>The resolved symbols, loaded once.</summary>
    internal static MacSymbols Resolve() => Cached.Value;

    private static MacSymbols Load()
    {
        try
        {
            if (!NativeLibrary.TryLoad(MacSecurity.CoreFoundation, out var cf)
                || !NativeLibrary.TryLoad(MacSecurity.SecurityFramework, out var sec))
            {
                // Every pointer left at zero, which AllResolved reads as incomplete.
                return new MacSymbols();
            }

            // The two callback tables are structs rather than object pointers, so what is wanted is the
            // address of the export itself and not what it holds. Every other symbol here is a CFTypeRef
            // global and needs the dereference; these two do not, and mixing them up produces a
            // dictionary that does not retain its contents.
            var keyCallBacks = NativeLibrary.TryGetExport(cf, "kCFTypeDictionaryKeyCallBacks", out var k)
                ? k
                : IntPtr.Zero;

            var valueCallBacks = NativeLibrary.TryGetExport(cf, "kCFTypeDictionaryValueCallBacks", out var v)
                ? v
                : IntPtr.Zero;

            return new MacSymbols
            {
                True = MacSecurity.Constant(cf, "kCFBooleanTrue"),
                TypeDictionaryKeyCallBacks = keyCallBacks,
                TypeDictionaryValueCallBacks = valueCallBacks,

                Class = MacSecurity.Constant(sec, "kSecClass"),
                ClassKey = MacSecurity.Constant(sec, "kSecClassKey"),
                ReturnRef = MacSecurity.Constant(sec, "kSecReturnRef"),
                UseDataProtectionKeychain = MacSecurity.Constant(sec, "kSecUseDataProtectionKeychain"),
                UseOperationPrompt = MacSecurity.Constant(sec, "kSecUseOperationPrompt"),

                AttrKeyType = MacSecurity.Constant(sec, "kSecAttrKeyType"),
                AttrKeySizeInBits = MacSecurity.Constant(sec, "kSecAttrKeySizeInBits"),
                AttrTokenId = MacSecurity.Constant(sec, "kSecAttrTokenID"),
                AttrIsPermanent = MacSecurity.Constant(sec, "kSecAttrIsPermanent"),
                AttrApplicationTag = MacSecurity.Constant(sec, "kSecAttrApplicationTag"),
                AttrAccessControl = MacSecurity.Constant(sec, "kSecAttrAccessControl"),
                PrivateKeyAttrs = MacSecurity.Constant(sec, "kSecPrivateKeyAttrs"),

                KeyTypeEcSecPrimeRandom = MacSecurity.Constant(sec, "kSecAttrKeyTypeECSECPrimeRandom"),
                TokenIdSecureEnclave = MacSecurity.Constant(sec, "kSecAttrTokenIDSecureEnclave"),
                AccessibleWhenUnlockedThisDeviceOnly =
                    MacSecurity.Constant(sec, "kSecAttrAccessibleWhenUnlockedThisDeviceOnly"),

                EciesAlgorithm = MacSecurity.Constant(
                    sec,
                    "kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM"),
            };
        }
        catch (Exception exception) when (exception is DllNotFoundException or BadImageFormatException)
        {
            return new MacSymbols();
        }
    }

    private bool AllResolved() =>
        True != IntPtr.Zero
        && TypeDictionaryKeyCallBacks != IntPtr.Zero
        && TypeDictionaryValueCallBacks != IntPtr.Zero
        && Class != IntPtr.Zero
        && ClassKey != IntPtr.Zero
        && ReturnRef != IntPtr.Zero
        && UseDataProtectionKeychain != IntPtr.Zero
        && UseOperationPrompt != IntPtr.Zero
        && AttrKeyType != IntPtr.Zero
        && AttrKeySizeInBits != IntPtr.Zero
        && AttrTokenId != IntPtr.Zero
        && AttrIsPermanent != IntPtr.Zero
        && AttrApplicationTag != IntPtr.Zero
        && AttrAccessControl != IntPtr.Zero
        && PrivateKeyAttrs != IntPtr.Zero
        && KeyTypeEcSecPrimeRandom != IntPtr.Zero
        && TokenIdSecureEnclave != IntPtr.Zero
        && AccessibleWhenUnlockedThisDeviceOnly != IntPtr.Zero
        && EciesAlgorithm != IntPtr.Zero;
}
