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

namespace DodoSSH.Client.App.Platform;

/// <summary>
/// The pieces of CoreFoundation and Security.framework <see cref="MacDeviceKeyStore"/> needs.
/// </summary>
/// <remarks>
/// <para>
/// Separated from the store itself because it is a different kind of code with a different kind of
/// review: nothing here makes a decision, and everything here is a translation of a C declaration that
/// is either right or wrong. Mixing the two would mean the security argument in
/// <see cref="MacDeviceKeyStore"/> had to be read past two hundred lines of marshalling to find.
/// </para>
/// <para>
/// <b>Every Create or Copy returns an object this process owns.</b> That is CoreFoundation's Create
/// Rule, and it is the thing here that goes wrong silently: the enclave key handle is small, so a leak
/// shows up as nothing at all until a long-running process has done a few thousand unlocks.
/// <see cref="CoreFoundationScope"/> exists so ownership is tracked by construction rather than by
/// remembering, and every function below that returns a handle says whether it is owned.
/// </para>
/// <para>
/// <b>The integer widths are the part worth checking against the headers rather than skimming.</b>
/// <c>CFIndex</c>, <c>CFOptionFlags</c> and <c>CFNumberType</c> are all pointer-width on a 64-bit Mac,
/// not 32-bit, and getting one wrong does not fail cleanly — it shifts every argument after it, so the
/// call receives plausible rubbish and returns a parameter error that names nothing.
/// </para>
/// </remarks>
[SupportedOSPlatform("macos")]
internal static partial class MacSecurity
{
    internal const string SecurityFramework =
        "/System/Library/Frameworks/Security.framework/Security";

    internal const string CoreFoundation =
        "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";

    /// <summary>
    /// The access control flags <c>SecAccessControlCreateWithFlags</c> takes.
    /// </summary>
    /// <remarks>
    /// <c>ulong</c> because the parameter is a <c>CFOptionFlags</c>, which is an <c>unsigned long</c>.
    /// Only the two flags that are used are listed; the full set is large, and copying it in would
    /// invite somebody to reach for one without reading what it does to the prompt — <c>Biometry</c>
    /// alone, for instance, leaves a Mac with no Touch ID unable to unlock at all rather than falling
    /// back to the login password.
    /// </remarks>
    [Flags]
    internal enum AccessControlFlags : ulong
    {
        /// <summary>
        /// Touch ID if the machine has it, the login password if not.
        /// </summary>
        /// <remarks>
        /// The forgiving one, deliberately. <c>BiometryCurrentSet</c> would additionally invalidate the
        /// key whenever a fingerprint is added or removed, which sounds stricter and here buys nothing:
        /// this key wraps a device key whose loss already means "ask for the passphrase", so the only
        /// effect would be users being sent back to their passphrase by an unrelated Settings change
        /// they would never connect to it.
        /// </remarks>
        UserPresence = 1ul << 0,

        /// <summary>Required for any key that lives in the Secure Enclave.</summary>
        PrivateKeyUsage = 1ul << 30,
    }

    /// <summary>The CFNumberType code for a 32-bit int, from CFNumber.h.</summary>
    internal const long CFNumberIntType = 9;

    /// <summary>errSecSuccess.</summary>
    internal const int Success = 0;

    /// <summary>errSecItemNotFound, which is an answer rather than a failure.</summary>
    internal const int ItemNotFound = -25300;

    // ---- CoreFoundation ------------------------------------------------------------------------------

    /// <summary>Releases an owned handle.</summary>
    [LibraryImport(CoreFoundation)]
    internal static partial void CFRelease(IntPtr handle);

    /// <summary>Copies bytes into a new CFData. Owned.</summary>
    [LibraryImport(CoreFoundation)]
    internal static partial IntPtr CFDataCreate(IntPtr allocator, IntPtr bytes, nint length);

    [LibraryImport(CoreFoundation)]
    internal static partial IntPtr CFDataGetBytePtr(IntPtr data);

    [LibraryImport(CoreFoundation)]
    internal static partial nint CFDataGetLength(IntPtr data);

    /// <summary>Boxes a value as a CFNumber. Owned.</summary>
    [LibraryImport(CoreFoundation)]
    internal static partial IntPtr CFNumberCreate(IntPtr allocator, nint theType, IntPtr valuePtr);

    /// <summary>Builds an immutable dictionary. Owned.</summary>
    /// <remarks>
    /// <para>
    /// The key and value arrays are passed as raw pointers to memory the caller pins, rather than as
    /// managed arrays. Source-generated interop wants an explicit element count for a marshalled array,
    /// and supplying one here would mean stating the length twice — once for the marshaller and once as
    /// <paramref name="numValues"/> — which is exactly the pair that drifts.
    /// </para>
    /// <para>
    /// The two callback tables are what make the dictionary retain its keys and values, which is why
    /// they are passed rather than left null: with null callbacks the dictionary stores raw pointers and
    /// keeps nothing alive, and the resulting use-after-free is intermittent by nature.
    /// </para>
    /// </remarks>
    [LibraryImport(CoreFoundation)]
    internal static partial IntPtr CFDictionaryCreate(
        IntPtr allocator,
        IntPtr keys,
        IntPtr values,
        nint numValues,
        IntPtr keyCallBacks,
        IntPtr valueCallBacks);

    // ---- Security.framework --------------------------------------------------------------------------

    /// <summary>Builds the access policy a Secure Enclave key is created under. Owned.</summary>
    [LibraryImport(SecurityFramework)]
    internal static partial IntPtr SecAccessControlCreateWithFlags(
        IntPtr allocator,
        IntPtr protection,
        AccessControlFlags flags,
        out IntPtr error);

    /// <summary>Creates a key pair from an attribute dictionary. Owned.</summary>
    [LibraryImport(SecurityFramework)]
    internal static partial IntPtr SecKeyCreateRandomKey(IntPtr parameters, out IntPtr error);

    /// <summary>The public half of a key. Owned.</summary>
    /// <remarks>
    /// Available even for an enclave key, and that asymmetry is the whole reason this design works: the
    /// public half is an ordinary key this process can hold and use, while the private half is a handle
    /// to something inside the enclave that never becomes bytes. So sealing is silent and unsealing is
    /// the thing the user is asked about.
    /// </remarks>
    [LibraryImport(SecurityFramework)]
    internal static partial IntPtr SecKeyCopyPublicKey(IntPtr key);

    /// <summary>Encrypts with a public key. Owned.</summary>
    [LibraryImport(SecurityFramework)]
    internal static partial IntPtr SecKeyCreateEncryptedData(
        IntPtr key,
        IntPtr algorithm,
        IntPtr plaintext,
        out IntPtr error);

    /// <summary>Decrypts with a private key, prompting for whatever guards it. Owned.</summary>
    [LibraryImport(SecurityFramework)]
    internal static partial IntPtr SecKeyCreateDecryptedData(
        IntPtr key,
        IntPtr algorithm,
        IntPtr ciphertext,
        out IntPtr error);

    /// <summary>Finds a keychain item. The out handle is owned when the result is <see cref="Success"/>.</summary>
    [LibraryImport(SecurityFramework)]
    internal static partial int SecItemCopyMatching(IntPtr query, out IntPtr result);

    /// <summary>Deletes every keychain item matching the query.</summary>
    [LibraryImport(SecurityFramework)]
    internal static partial int SecItemDelete(IntPtr query);

    // ---- The framework constants ---------------------------------------------------------------------

    /// <summary>
    /// Reads one of a framework's global CFString constants, or zero if it is not exported.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The keys these dictionaries take are not strings this code may spell for itself. They are
    /// pointer-comparable constants exported by the framework, and a CFString built here with the same
    /// characters is a different object — the lookups would miss and the call would fail with a
    /// parameter error naming nothing.
    /// </para>
    /// <para>
    /// <b>Dereferenced once, because the exported symbol is the variable rather than its value.</b>
    /// <c>TryGetExport</c> answers the address of the global; the CFStringRef is what that address
    /// holds. Missing the indirection produces a pointer that is stable, plausible and wrong, which is
    /// the worst of the three available outcomes.
    /// </para>
    /// <para>
    /// Zero on a missing symbol rather than an exception, because the caller's answer to every failure
    /// is the same one — report the store unavailable and let unlock ask for the passphrase — and a
    /// constant that has been renamed by a future macOS should reach that answer rather than a crash.
    /// </para>
    /// </remarks>
    internal static IntPtr Constant(IntPtr library, string symbol) =>
        NativeLibrary.TryGetExport(library, symbol, out var address)
            ? Marshal.ReadIntPtr(address)
            : IntPtr.Zero;
}

/// <summary>
/// Releases every CoreFoundation handle put into it, in reverse order, exactly once.
/// </summary>
/// <remarks>
/// <para>
/// The alternative is a try/finally per handle, and the operations here need six or seven at a time — a
/// dictionary holding a nested dictionary holding an access control object holding a CFData tag. Finallys
/// nested that deep stop being read, and a handle released twice is a crash rather than a leak.
/// </para>
/// <para>
/// <see cref="Keep"/> returns what it was given, so a handle can be tracked in the same expression that
/// produces it and the call sites read as ordinary code.
/// </para>
/// </remarks>
[SupportedOSPlatform("macos")]
internal sealed class CoreFoundationScope : IDisposable
{
    private readonly List<IntPtr> owned = [];

    private bool disposed;

    /// <summary>Takes ownership of a handle and hands it straight back.</summary>
    /// <remarks>
    /// Zero is ignored rather than rejected. Every CoreFoundation call here answers zero on failure, so
    /// accepting it lets a caller track the result in the expression that produces it and check it on
    /// the next line, instead of writing the check twice.
    /// </remarks>
    internal IntPtr Keep(IntPtr handle)
    {
        if (handle != IntPtr.Zero)
        {
            owned.Add(handle);
        }

        return handle;
    }

    public void Dispose()
    {
        if (disposed)
        {
            return;
        }

        disposed = true;

        // Reverse order, so a container is released before the things it retains. CoreFoundation does not
        // require it — retain counts make the order irrelevant — but it keeps the lifetimes readable in a
        // debugger, where a released container that still lists its contents is a confusing thing to meet.
        for (var i = owned.Count - 1; i >= 0; i--)
        {
            MacSecurity.CFRelease(owned[i]);
        }

        owned.Clear();
    }
}
