using System.Reflection;
using System.Reflection.PortableExecutable;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Security.Cryptography;
using MBN_STOCK_WEBVIEW.Playout.Interop;
using MBN_STOCK_WEBVIEW.Playout.Registration;

namespace MBN_STOCK_WEBVIEW.Playout.Diagnostics;

internal interface IK3dConnectOnlyComBinding
{
    object CreateEventHandler();

    object CreateEngine();

    int KtapConnect(object engine, string host, int port, object eventHandler);

    void Disconnect(object engine);

    void Release(object value);
}

internal interface IK3dConnectOnlyComBindingFactory
{
    IK3dConnectOnlyComBinding Create();
}

internal sealed class InstalledK3dInteropConnectBindingFactory : IK3dConnectOnlyComBindingFactory
{
    public IK3dConnectOnlyComBinding Create() =>
        new InstalledK3dInteropConnectBinding(
            InstalledK3dInteropMetadata.Resolve(),
            new RuntimeComObjectReleaser());
}

internal static class InstalledK3dInteropMetadata
{
    internal const string ApprovedSha256EnvironmentVariable =
        "MBN_STOCK_K3D_INTEROP_SHA256";
    internal const string ApprovedNativeSha256EnvironmentVariable =
        "MBN_STOCK_K3D_NATIVE_SHA256";

    private static readonly Lazy<InstalledK3dInteropTypes> Types = new(
        ResolveCore,
        LazyThreadSafetyMode.ExecutionAndPublication);

    public static InstalledK3dInteropTypes Resolve() => Types.Value;

    private static InstalledK3dInteropTypes ResolveCore()
    {
        try
        {
            return ResolveAndHoldTrustedFiles();
        }
        catch (InstalledK3dInteropMetadataUnavailableException)
        {
            throw;
        }
        catch (Exception)
        {
            // Do not put the approved hash, discovered path, or registry values in reports.
            throw new InstalledK3dInteropMetadataUnavailableException();
        }
    }

    private static InstalledK3dInteropTypes ResolveAndHoldTrustedFiles()
    {
        FileStream? nativeStream = null;
        FileStream? interopStream = null;
        try
        {
            var snapshot = new WindowsK3dRegistrySnapshotSource().Capture();
            var paths = ResolveRegisteredPaths(snapshot);
            EnsureFixedDrive(paths.NativeBinaryPath);
            EnsureFixedDrive(paths.InteropAssemblyPath);
            EnsureNoReparsePointInAncestry(paths.NativeBinaryPath);
            EnsureNoReparsePointInAncestry(paths.InteropAssemblyPath);

            nativeStream = OpenReadLocked(paths.NativeBinaryPath);
            ValidateAmd64Pe(nativeStream, requireManagedMetadata: false);
            ValidateApprovedSha256(
                nativeStream,
                Environment.GetEnvironmentVariable(
                    ApprovedNativeSha256EnvironmentVariable,
                    EnvironmentVariableTarget.Process));

            interopStream = OpenReadLocked(paths.InteropAssemblyPath);
            ValidateAmd64Pe(interopStream, requireManagedMetadata: true);
            ValidateApprovedSha256(
                interopStream,
                Environment.GetEnvironmentVariable(
                    ApprovedSha256EnvironmentVariable,
                    EnvironmentVariableTarget.Process));

            // Both non-delete/non-write shared handles remain rooted by the Lazy metadata
            // for the process lifetime, including COM activation and subsequent use.
            EnsureNoReparsePointInAncestry(paths.NativeBinaryPath);
            EnsureNoReparsePointInAncestry(paths.InteropAssemblyPath);
            var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
                paths.InteropAssemblyPath);
            var lease = new InstalledK3dInteropFileLease(nativeStream, interopStream);
            var result = ValidateLoadedAssembly(assembly, lease);
            nativeStream = null;
            interopStream = null;
            return result;
        }
        finally
        {
            interopStream?.Dispose();
            nativeStream?.Dispose();
        }
    }

    internal static InstalledK3dInteropPaths ResolveRegisteredPaths(
        K3dRegistrySnapshot snapshot)
    {
        if (!snapshot.Is64BitProcess ||
            snapshot.CurrentUserOverridePresent ||
            !IsExpectedClassRegistration(
                snapshot.Engine,
                K3dComConstants.KaEngineProgId,
                K3dComConstants.KaEngineClassGuid) ||
            !IsExpectedClassRegistration(
                snapshot.EventHandler,
                K3dComConstants.KaEventHandlerProgId,
                K3dComConstants.KaEventHandlerClassGuid))
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        var typeLibraryPath = NormalizeRegisteredPath(snapshot.Win64TypeLibraryPath);
        var enginePath = NormalizeRegisteredPath(snapshot.Engine.InprocServerPath);
        var eventHandlerPath = NormalizeRegisteredPath(snapshot.EventHandler.InprocServerPath);
        if (!string.Equals(typeLibraryPath, enginePath, StringComparison.OrdinalIgnoreCase) ||
            !string.Equals(typeLibraryPath, eventHandlerPath, StringComparison.OrdinalIgnoreCase))
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        var nativeFile = new FileInfo(typeLibraryPath);
        var releaseDirectory = nativeFile.Directory;
        var x64Directory = releaseDirectory?.Parent;
        var dllDirectory = x64Directory?.Parent;
        var vendorRoot = dllDirectory?.Parent;
        if (!string.Equals(
                nativeFile.Name,
                "K3DAsyncEngine.dll",
                StringComparison.OrdinalIgnoreCase) ||
            !string.Equals(releaseDirectory?.Name, "Release", StringComparison.OrdinalIgnoreCase) ||
            !string.Equals(x64Directory?.Name, "x64", StringComparison.OrdinalIgnoreCase) ||
            !string.Equals(dllDirectory?.Name, "DLL", StringComparison.OrdinalIgnoreCase) ||
            vendorRoot is null)
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        var interopPath = Path.GetFullPath(
            Path.Combine(
                vendorRoot.FullName,
                "Bin",
                "x64",
                "C#",
                "Interop.K3DAsyncEngineLib.dll"));
        return new InstalledK3dInteropPaths(typeLibraryPath, interopPath);
    }

    internal static byte[] ParseApprovedSha256(string? value)
    {
        if (value is null || value.Length != 64 || value.Any(character => !IsHex(character)))
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        return Convert.FromHexString(value);
    }

    internal static void ValidateApprovedSha256(Stream stream, string? approvedValue)
    {
        var approvedHash = ParseApprovedSha256(approvedValue);
        stream.Position = 0;
        var actualHash = SHA256.HashData(stream);
        try
        {
            if (!CryptographicOperations.FixedTimeEquals(actualHash, approvedHash))
            {
                throw new InstalledK3dInteropMetadataUnavailableException();
            }
        }
        finally
        {
            CryptographicOperations.ZeroMemory(actualHash);
            CryptographicOperations.ZeroMemory(approvedHash);
        }
    }

    private static bool IsExpectedClassRegistration(
        K3dClassRegistrationSnapshot registration,
        string expectedProgId,
        Guid expectedClassId) =>
        registration.ClassKeyPresent &&
        string.Equals(registration.ClassProgId, expectedProgId, StringComparison.OrdinalIgnoreCase) &&
        Guid.TryParse(registration.ProgIdClassId, out var classId) &&
        classId == expectedClassId &&
        string.Equals(
            registration.ThreadingModel,
            K3dComConstants.ApartmentThreadingModel,
            StringComparison.OrdinalIgnoreCase) &&
        Guid.TryParse(registration.TypeLibraryId, out var typeLibraryId) &&
        typeLibraryId == Guid.Parse(K3dComConstants.TypeLibraryId);

    private static string NormalizeRegisteredPath(string? path)
    {
        if (string.IsNullOrEmpty(path) ||
            !string.Equals(path, path.Trim(), StringComparison.Ordinal) ||
            !Path.IsPathFullyQualified(path))
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        return Path.GetFullPath(path);
    }

    private static void EnsureFixedDrive(string path)
    {
        var root = Path.GetPathRoot(path);
        if (string.IsNullOrEmpty(root) || new DriveInfo(root).DriveType != DriveType.Fixed)
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }
    }

    private static void EnsureNoReparsePointInAncestry(string path)
    {
        if (!File.Exists(path))
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        if ((File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0)
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        var current = Path.GetDirectoryName(path);
        while (!string.IsNullOrEmpty(current))
        {
            if ((File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0)
            {
                throw new InstalledK3dInteropMetadataUnavailableException();
            }

            current = Directory.GetParent(current)?.FullName;
        }
    }

    private static FileStream OpenReadLocked(string path) => new(
        path,
        FileMode.Open,
        FileAccess.Read,
        FileShare.Read);

    private static void ValidateAmd64Pe(Stream stream, bool requireManagedMetadata)
    {
        stream.Position = 0;
        using var reader = new PEReader(stream, PEStreamOptions.LeaveOpen);
        if (reader.PEHeaders.CoffHeader.Machine != Machine.Amd64 ||
            reader.PEHeaders.PEHeader?.Magic != PEMagic.PE32Plus ||
            (requireManagedMetadata && !reader.HasMetadata))
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }
    }

    private static InstalledK3dInteropTypes ValidateLoadedAssembly(
        Assembly assembly,
        InstalledK3dInteropFileLease fileLease)
    {
        var assemblyName = assembly.GetName();
        if (!string.Equals(
                assemblyName.Name,
                "Interop.K3DAsyncEngineLib",
                StringComparison.Ordinal) ||
            assemblyName.Version != new Version(1, 0, 0, 0) ||
            !Guid.TryParse(assembly.GetCustomAttribute<GuidAttribute>()?.Value, out var typeLibraryId) ||
            typeLibraryId != Guid.Parse(K3dComConstants.TypeLibraryId) ||
            !string.Equals(
                assembly.GetCustomAttribute<ImportedFromTypeLibAttribute>()?.Value,
                "K3DAsyncEngineLib",
                StringComparison.Ordinal) ||
            assembly.GetCustomAttribute<TypeLibVersionAttribute>() is not { MajorVersion: 1, MinorVersion: 0 })
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        var engineType = RequireComType(
            assembly,
            "K3DAsyncEngineLib.KAEngineClass",
            K3dComConstants.KaEngineClassGuid);
        var eventHandlerType = RequireComType(
            assembly,
            "K3DAsyncEngineLib.KAEventHandlerClass",
            K3dComConstants.KaEventHandlerClassGuid);
        var eventHandlerInterfaceType = RequireComType(
            assembly,
            "K3DAsyncEngineLib.KAEventHandler",
            Guid.Parse("C21817DB-C0D3-4647-8D85-027338DCF832"));
        var scenePlayerType = RequireComType(
            assembly,
            "K3DAsyncEngineLib.IKAScenePlayer",
            Guid.Parse("14083C84-4B17-41BF-AE1A-206F13C0E5C5"));
        var sceneType = RequireComType(
            assembly,
            "K3DAsyncEngineLib.IKAScene",
            Guid.Parse("BFD73274-AC76-437B-AF85-35682543F44C"));
        var objectType = RequireComType(
            assembly,
            "K3DAsyncEngineLib.IKAObject",
            Guid.Parse("B138B515-505B-4C7B-A3E1-F82782005ECF"));

        var callbackMethods = eventHandlerInterfaceType.GetInterfaces()
            .Append(eventHandlerInterfaceType)
            .SelectMany(type => type.GetMethods(
                BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
            .Distinct()
            .ToArray();
        if (callbackMethods.Length != 282 ||
            callbackMethods.Any(method => method.ReturnType != typeof(void)) ||
            !HasCallback(callbackMethods, "OnHello") ||
            !HasCallback(callbackMethods, "OnScenePlayed", typeof(int), typeof(int), typeof(int)) ||
            !HasCallback(callbackMethods, "OnCutOut", typeof(int), typeof(int), typeof(int)) ||
            !HasCallback(callbackMethods, "OnStopAll", typeof(int)))
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        var connectMethod = engineType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
            .SingleOrDefault(method =>
                method.Name == "KTAPConnect" &&
                method.ReturnType == typeof(int) &&
                method.GetParameters() is { Length: 5 } parameters &&
                parameters[0].ParameterType == typeof(int) &&
                parameters[1].ParameterType == typeof(string) &&
                parameters[2].ParameterType == typeof(int) &&
                parameters[3].ParameterType == typeof(int) &&
                parameters[4].ParameterType.GUID ==
                    Guid.Parse("C21817DB-C0D3-4647-8D85-027338DCF832") &&
                parameters[4].ParameterType == eventHandlerInterfaceType &&
                parameters[4].ParameterType.IsAssignableFrom(eventHandlerType));
        var disconnectMethod = engineType.GetMethod(
            "Disconnect",
            BindingFlags.Public | BindingFlags.Instance,
            binder: null,
            types: Type.EmptyTypes,
            modifiers: null);
        var setOutputChannelMethod = sceneType
            .GetMethods(BindingFlags.Public | BindingFlags.Instance)
            .SingleOrDefault(method =>
                method.Name == "SetOutputChannelIndex" &&
                method.ReturnType == typeof(void) &&
                method.GetParameters() is { Length: 1 } parameters &&
                parameters[0].ParameterType == typeof(int));
        var unloadMethod = sceneType.GetMethod(
            "Unload",
            BindingFlags.Public | BindingFlags.Instance,
            binder: null,
            types: Type.EmptyTypes,
            modifiers: null);
        var endTransactionOnChannelMethod = engineType
            .GetMethods(BindingFlags.Public | BindingFlags.Instance)
            .SingleOrDefault(method =>
                method.Name == "EndTransactionOnChannel" &&
                method.ReturnType == typeof(void) &&
                method.GetParameters() is { Length: 1 } parameters &&
                parameters[0].ParameterType == typeof(int));
        if (connectMethod is null ||
            disconnectMethod?.ReturnType != typeof(void) ||
            setOutputChannelMethod is null ||
            unloadMethod?.ReturnType != typeof(void) ||
            endTransactionOnChannelMethod is null)
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        return new InstalledK3dInteropTypes(
            engineType,
            eventHandlerType,
            eventHandlerInterfaceType,
            scenePlayerType,
            sceneType,
            objectType,
            connectMethod,
            disconnectMethod,
            fileLease);
    }

    private static Type RequireComType(Assembly assembly, string name, Guid expectedGuid)
    {
        var type = assembly.GetType(name, throwOnError: false, ignoreCase: false);
        if (type is null || !type.IsImport || type.GUID != expectedGuid)
        {
            throw new InstalledK3dInteropMetadataUnavailableException();
        }

        return type;
    }

    private static bool HasCallback(
        IEnumerable<MethodInfo> methods,
        string name,
        params Type[] parameterTypes) => methods.Any(method =>
            method.Name == name &&
            method.GetParameters().Select(parameter => parameter.ParameterType)
                .SequenceEqual(parameterTypes));

    private static bool IsHex(char value) =>
        value is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F';
}

internal sealed class InstalledK3dInteropMetadataUnavailableException : Exception
{
    public InstalledK3dInteropMetadataUnavailableException()
        : base("The installed K3D interop metadata is unavailable or not approved.")
    {
    }
}

internal readonly record struct InstalledK3dInteropPaths(
    string NativeBinaryPath,
    string InteropAssemblyPath);

internal sealed class InstalledK3dInteropFileLease
{
    internal InstalledK3dInteropFileLease(FileStream nativeStream, FileStream interopStream)
    {
        NativeStream = nativeStream;
        InteropStream = interopStream;
    }

    internal FileStream NativeStream { get; }

    internal FileStream InteropStream { get; }
}

internal sealed record InstalledK3dInteropTypes(
    Type EngineType,
    Type EventHandlerType,
    Type EventHandlerInterfaceType,
    Type ScenePlayerType,
    Type SceneType,
    Type ObjectType,
    MethodInfo ConnectMethod,
    MethodInfo DisconnectMethod,
    InstalledK3dInteropFileLease FileLease);

internal sealed class InstalledK3dInteropComActivator : ILateBoundComActivator
{
    public object Create(Guid classId)
    {
        var types = InstalledK3dInteropMetadata.Resolve();
        var type = classId switch
        {
            _ when classId == K3dComConstants.KaEngineClassGuid => types.EngineType,
            _ when classId == K3dComConstants.KaEventHandlerClassGuid => types.EventHandlerType,
            _ => throw new InvalidOperationException("The K3D COM class is not allowlisted.")
        };

        return Activator.CreateInstance(type)
            ?? throw new InvalidOperationException("The installed K3D COM class could not be created.");
    }
}

internal sealed class InstalledK3dInteropMethodInvoker : ILateBoundComMethodInvoker
{
    private static readonly IReadOnlySet<string> EngineMethods = new HashSet<string>(
    [
        "KTAPConnect",
        "Disconnect",
        "GetScenePlayer",
        "GetScenePlayerOnChannel",
        "LoadScene",
        "BeginTransaction",
        "EndTransaction",
        "EndTransactionOnChannel",
        "RollbackTransaction"
    ], StringComparer.Ordinal);

    private static readonly IReadOnlySet<string> PlayerMethods = new HashSet<string>(
    [
        "Prepare",
        "Play",
        "StopAll",
        "CutOut",
        "GetPlayingScene"
    ], StringComparer.Ordinal);

    private static readonly IReadOnlySet<string> SceneMethods = new HashSet<string>(
    [
        "SetSceneEffectType",
        "SetOutputChannelIndex",
        "SetBackgroundTexture",
        "SetBackgroundVideo",
        "UseBackground",
        "QueryVariables",
        "GetObject",
        "Unload"
    ], StringComparer.Ordinal);

    private static readonly IReadOnlySet<string> ObjectMethods = new HashSet<string>(
    [
        "SetValue",
        "SetVisible",
        "SetFaceColor",
        "SetPosition",
        "SetPositionKey",
        "SetScale",
        "SetCropKey",
        "SetCircleAngleKey",
        "BeginPathPoint",
        "ClearPathPoints",
        "AddPathPoint",
        "EndPathPoint",
        "BeginPathShapePoint",
        "ClearPathShapePoints",
        "AddPathShapePoint",
        "EndPathShapePoint"
    ], StringComparer.Ordinal);

    public object? Invoke(object target, string method, params object?[] arguments)
    {
        ArgumentNullException.ThrowIfNull(target);
        if (!Marshal.IsComObject(target))
        {
            return InvokeMember(target.GetType(), target, method, arguments);
        }

        var types = InstalledK3dInteropMetadata.Resolve();
        var invocationType = EngineMethods.Contains(method)
            ? types.EngineType
            : PlayerMethods.Contains(method)
                ? types.ScenePlayerType
                : SceneMethods.Contains(method)
                    ? types.SceneType
                    : ObjectMethods.Contains(method)
                        ? types.ObjectType
                        : throw new MissingMethodException("The K3D method is not allowlisted.");
        var candidates = invocationType
            .GetMethods(BindingFlags.Public | BindingFlags.Instance)
            .Where(candidate =>
                candidate.Name == method &&
                candidate.GetParameters().Length == arguments.Length)
            .ToArray();
        if (candidates.Length != 1)
        {
            throw new MissingMethodException("The installed K3D method signature is ambiguous.");
        }

        var invokeArguments = NormalizeArguments(candidates[0], arguments);
        return InvokeMethod(candidates[0], target, invokeArguments);
    }

    private static object?[] NormalizeArguments(MethodInfo method, object?[] arguments)
    {
        var parameters = method.GetParameters();
        var result = new object?[arguments.Length];
        for (var index = 0; index < arguments.Length; index++)
        {
            var value = arguments[index];
            var expectedType = parameters[index].ParameterType;
            if (value is null || expectedType.IsInstanceOfType(value))
            {
                result[index] = value;
            }
            else if (expectedType.IsEnum && value is IConvertible)
            {
                result[index] = Enum.ToObject(expectedType, value);
            }
            else if (value is IConvertible && typeof(IConvertible).IsAssignableFrom(expectedType))
            {
                result[index] = Convert.ChangeType(
                    value,
                    expectedType,
                    System.Globalization.CultureInfo.InvariantCulture);
            }
            else
            {
                result[index] = value;
            }
        }

        return result;
    }

    private static object? InvokeMember(
        Type type,
        object target,
        string method,
        object?[] arguments)
    {
        try
        {
            return type.InvokeMember(
                method,
                BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance,
                binder: null,
                target,
                arguments,
                System.Globalization.CultureInfo.InvariantCulture);
        }
        catch (TargetInvocationException exception) when (exception.InnerException is not null)
        {
            ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
            throw;
        }
    }

    private static object? InvokeMethod(
        MethodInfo method,
        object target,
        object?[] arguments)
    {
        try
        {
            return method.Invoke(target, arguments);
        }
        catch (TargetInvocationException exception) when (exception.InnerException is not null)
        {
            ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
            throw;
        }
    }
}

internal sealed class InstalledK3dInteropConnectBinding : IK3dConnectOnlyComBinding
{
    private readonly InstalledK3dInteropTypes _types;
    private readonly ILateBoundComObjectReleaser _releaser;

    public InstalledK3dInteropConnectBinding(
        InstalledK3dInteropTypes types,
        ILateBoundComObjectReleaser releaser)
    {
        _types = types;
        _releaser = releaser;
    }

    public object CreateEventHandler() =>
        Activator.CreateInstance(_types.EventHandlerType)
        ?? throw new InvalidOperationException("The K3D event handler could not be created.");

    public object CreateEngine() =>
        Activator.CreateInstance(_types.EngineType)
        ?? throw new InvalidOperationException("The K3D engine could not be created.");

    public int KtapConnect(object engine, string host, int port, object eventHandler) =>
        Convert.ToInt32(
            Invoke(_types.ConnectMethod, engine, [1, host, port, 0, eventHandler]),
            System.Globalization.CultureInfo.InvariantCulture);

    public void Disconnect(object engine) =>
        _ = Invoke(_types.DisconnectMethod, engine, null);

    public void Release(object value) => _releaser.Release(value);

    private static object? Invoke(MethodInfo method, object target, object?[]? arguments)
    {
        try
        {
            return method.Invoke(target, arguments);
        }
        catch (TargetInvocationException exception) when (exception.InnerException is not null)
        {
            ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
            throw;
        }
    }
}
