using System.Data;
using System.Runtime.InteropServices;
using System.Security;
using Microsoft.Data.SqlClient;

namespace e_suite.Database.Migrator.SqlWrapper;

public static class SecureSqlParameterExtensions
{
    [DllImport("kernel32.dll", EntryPoint = "CopyMemory")]
    private static extern void CopyMemory(IntPtr dest, IntPtr src, IntPtr count);
    [DllImport("kernel32.dll", EntryPoint = "RtlZeroMemory")]
    private static extern void ZeroMemory(IntPtr ptr, IntPtr count);

    /// <summary>
    /// You must dispose the return value as soon as SqlCommand.Execute* is called.
    /// </summary>
    public static IDisposable AddSecure(this SqlParameterCollection collection, string name, SecureString secureString)
    {
        var value = new SecureStringParameterValue(secureString);
        collection.Add(name, SqlDbType.NVarChar).Value = value;
        return value;
    }

    private sealed class SecureStringParameterValue : IConvertible, IDisposable
    {
        private readonly SecureString? _secureString;
        private int _length;
        private string? _insecureManagedCopy;
        private GCHandle _insecureManagedCopyGcHandle;

        public SecureStringParameterValue(SecureString secureString)
        {
            _secureString = secureString;
        }

        #region IConvertible

        public TypeCode GetTypeCode()
        {
            return TypeCode.String;
        }

        public string ToString(IFormatProvider? provider)
        {
            if (_insecureManagedCopy != null) return _insecureManagedCopy;
            if (_secureString == null || _secureString.Length == 0) return string.Empty;

            // We waited till the last possible minute.

            // Here's the plan:
            //  1. Create a new managed string initialized to zero
            //  2. Pin the managed string so the GC leaves it alone
            //  3. Copy the contents of the SecureString into the managed string
            //  4. Use the string as a SqlParameter
            //  5. Zero the managed string after Execute* is called and free the GC handle

            _length = _secureString.Length;
            _insecureManagedCopy = new string('\0', _length);
            _insecureManagedCopyGcHandle = GCHandle.Alloc(_insecureManagedCopy, GCHandleType.Pinned); // Do not allow the GC to move this around and leave copies behind

            try
            {
                // This is the only way to read the contents, sadly.
                // SecureStringToBSTR picks where to put it, so we have to copy it from there and zerofree the unmanaged copy as fast as possible.
                IntPtr insecureUnmanagedCopy = Marshal.SecureStringToBSTR(_secureString);
                try
                {
                    CopyMemory(_insecureManagedCopyGcHandle.AddrOfPinnedObject(), insecureUnmanagedCopy, (IntPtr)(_length * 2));
                }
                finally
                {
                    if (insecureUnmanagedCopy != IntPtr.Zero) Marshal.ZeroFreeBSTR(insecureUnmanagedCopy);
                }

                // Now the string managed string has the contents in the clear.
                return _insecureManagedCopy;
            }
            catch
            {
                Dispose();
                throw;
            }
        }

        public void Dispose()
        {
            if (_insecureManagedCopy == null) return;
            _insecureManagedCopy = null;
            ZeroMemory(_insecureManagedCopyGcHandle.AddrOfPinnedObject(), (IntPtr)(_length * 2));
            _insecureManagedCopyGcHandle.Free();
        }

        public bool ToBoolean(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public char ToChar(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public sbyte ToSByte(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public byte ToByte(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public short ToInt16(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public ushort ToUInt16(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public int ToInt32(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public uint ToUInt32(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public long ToInt64(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public ulong ToUInt64(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public float ToSingle(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public double ToDouble(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public decimal ToDecimal(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public DateTime ToDateTime(IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        public object ToType(Type conversionType, IFormatProvider? provider)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}