using Microsoft.Win32;
using MBN_STOCK_WEBVIEW.Playout.Interop;

namespace MBN_STOCK_WEBVIEW.Playout.Registration;

[Flags]
public enum K3dRegistrationIssue
{
    None = 0,
    ProcessIsNot64Bit = 1 << 0,
    TypeLibraryMissing = 1 << 1,
    Win64BinaryMissing = 1 << 2,
    Win64BinaryIsNotAmd64 = 1 << 3,
    EngineClassMissing = 1 << 4,
    EngineProgIdMismatch = 1 << 5,
    EngineThreadingModelMismatch = 1 << 6,
    EngineTypeLibraryMismatch = 1 << 7,
    EventHandlerClassMissing = 1 << 8,
    EventHandlerProgIdMismatch = 1 << 9,
    EventHandlerThreadingModelMismatch = 1 << 10,
    EventHandlerTypeLibraryMismatch = 1 << 11,
    EngineProgIdClassIdMismatch = 1 << 12,
    EngineServerIsNotAmd64 = 1 << 13,
    EventHandlerProgIdClassIdMismatch = 1 << 14,
    EventHandlerServerIsNotAmd64 = 1 << 15,
    CurrentUserOverridePresent = 1 << 16,
    RegistryReadFailed = 1 << 17
}

public sealed record K3dRegistrationReport(
    K3dRegistrationIssue Issues,
    bool Is64BitProcess,
    bool HasWin64TypeLibrary,
    bool IsWin64BinaryAmd64,
    bool IsEngineClassReady,
    bool IsEventHandlerClassReady,
    string Message)
{
    public bool IsReady => Issues == K3dRegistrationIssue.None;
}

internal interface IK3dRegistrationProbe
{
    K3dRegistrationReport Probe();
}

internal sealed record K3dClassRegistrationSnapshot(
    bool ClassKeyPresent,
    string? ClassProgId,
    string? ProgIdClassId,
    string? InprocServerPath,
    string? ThreadingModel,
    string? TypeLibraryId);

internal sealed record K3dRegistrySnapshot(
    bool Is64BitProcess,
    string? Win64TypeLibraryPath,
    K3dClassRegistrationSnapshot Engine,
    K3dClassRegistrationSnapshot EventHandler,
    bool CurrentUserOverridePresent);

internal interface IK3dRegistrySnapshotSource
{
    K3dRegistrySnapshot Capture();
}

internal readonly record struct PeBinaryInspection(bool Exists, bool IsAmd64);

internal interface IPeBinaryInspector
{
    PeBinaryInspection Inspect(string? path);
}

public sealed class K3dRegistrationProbe : IK3dRegistrationProbe
{
    private readonly IK3dRegistrySnapshotSource _snapshotSource;
    private readonly IPeBinaryInspector _binaryInspector;

    public K3dRegistrationProbe()
        : this(new WindowsK3dRegistrySnapshotSource(), new FilePeBinaryInspector())
    {
    }

    internal K3dRegistrationProbe(
        IK3dRegistrySnapshotSource snapshotSource,
        IPeBinaryInspector binaryInspector)
    {
        _snapshotSource = snapshotSource;
        _binaryInspector = binaryInspector;
    }

    public K3dRegistrationReport Probe()
    {
        try
        {
            return ProbeCore(_snapshotSource.Capture());
        }
        catch (Exception)
        {
            return new K3dRegistrationReport(
                K3dRegistrationIssue.RegistryReadFailed,
                Environment.Is64BitProcess,
                false,
                false,
                false,
                false,
                SafeMessage(K3dRegistrationIssue.RegistryReadFailed));
        }
    }

    private K3dRegistrationReport ProbeCore(K3dRegistrySnapshot snapshot)
    {
        var issues = K3dRegistrationIssue.None;
        if (!snapshot.Is64BitProcess)
        {
            issues |= K3dRegistrationIssue.ProcessIsNot64Bit;
        }

        var typeLibraryBinary = _binaryInspector.Inspect(snapshot.Win64TypeLibraryPath);
        var hasWin64TypeLibrary = !string.IsNullOrWhiteSpace(snapshot.Win64TypeLibraryPath);
        if (!hasWin64TypeLibrary)
        {
            issues |= K3dRegistrationIssue.TypeLibraryMissing;
        }

        if (!typeLibraryBinary.Exists)
        {
            issues |= K3dRegistrationIssue.Win64BinaryMissing;
        }
        else if (!typeLibraryBinary.IsAmd64)
        {
            issues |= K3dRegistrationIssue.Win64BinaryIsNotAmd64;
        }

        var engineReady = CheckClass(
            snapshot.Engine,
            K3dComConstants.KaEngineClassGuid,
            K3dComConstants.KaEngineProgId,
            K3dRegistrationIssue.EngineClassMissing,
            K3dRegistrationIssue.EngineProgIdMismatch,
            K3dRegistrationIssue.EngineProgIdClassIdMismatch,
            K3dRegistrationIssue.EngineServerIsNotAmd64,
            K3dRegistrationIssue.EngineThreadingModelMismatch,
            K3dRegistrationIssue.EngineTypeLibraryMismatch,
            ref issues);

        var eventHandlerReady = CheckClass(
            snapshot.EventHandler,
            K3dComConstants.KaEventHandlerClassGuid,
            K3dComConstants.KaEventHandlerProgId,
            K3dRegistrationIssue.EventHandlerClassMissing,
            K3dRegistrationIssue.EventHandlerProgIdMismatch,
            K3dRegistrationIssue.EventHandlerProgIdClassIdMismatch,
            K3dRegistrationIssue.EventHandlerServerIsNotAmd64,
            K3dRegistrationIssue.EventHandlerThreadingModelMismatch,
            K3dRegistrationIssue.EventHandlerTypeLibraryMismatch,
            ref issues);

        if (snapshot.CurrentUserOverridePresent)
        {
            issues |= K3dRegistrationIssue.CurrentUserOverridePresent;
            engineReady = false;
            eventHandlerReady = false;
        }

        return new K3dRegistrationReport(
            issues,
            snapshot.Is64BitProcess,
            hasWin64TypeLibrary,
            typeLibraryBinary.Exists && typeLibraryBinary.IsAmd64,
            engineReady,
            eventHandlerReady,
            SafeMessage(issues));
    }

    private bool CheckClass(
        K3dClassRegistrationSnapshot registration,
        Guid expectedClassId,
        string expectedProgId,
        K3dRegistrationIssue missingIssue,
        K3dRegistrationIssue classProgIdIssue,
        K3dRegistrationIssue progIdClassIdIssue,
        K3dRegistrationIssue architectureIssue,
        K3dRegistrationIssue threadingIssue,
        K3dRegistrationIssue typeLibraryIssue,
        ref K3dRegistrationIssue issues)
    {
        if (!registration.ClassKeyPresent)
        {
            issues |= missingIssue;
        }

        if (!string.Equals(
                registration.ClassProgId,
                expectedProgId,
                StringComparison.OrdinalIgnoreCase))
        {
            issues |= classProgIdIssue;
        }

        if (!MatchesGuid(registration.ProgIdClassId, expectedClassId))
        {
            issues |= progIdClassIdIssue;
        }

        var classBinary = _binaryInspector.Inspect(registration.InprocServerPath);
        if (!classBinary.Exists)
        {
            issues |= missingIssue;
        }
        else if (!classBinary.IsAmd64)
        {
            issues |= architectureIssue;
        }

        if (!string.Equals(
                registration.ThreadingModel,
                K3dComConstants.ApartmentThreadingModel,
                StringComparison.OrdinalIgnoreCase))
        {
            issues |= threadingIssue;
        }

        if (!MatchesGuid(registration.TypeLibraryId, Guid.Parse(K3dComConstants.TypeLibraryId)))
        {
            issues |= typeLibraryIssue;
        }

        var classIssues =
            missingIssue |
            classProgIdIssue |
            progIdClassIdIssue |
            architectureIssue |
            threadingIssue |
            typeLibraryIssue;
        return (issues & classIssues) == 0;
    }

    private static bool MatchesGuid(string? value, Guid expected) =>
        Guid.TryParse(value, out var actual) && actual == expected;

    private static string SafeMessage(K3dRegistrationIssue issues) => issues switch
    {
        K3dRegistrationIssue.None => "x64 K3D 송출 구성 요소가 준비되었습니다.",
        _ when issues.HasFlag(K3dRegistrationIssue.CurrentUserOverridePresent) =>
            "사용자별 K3D 등록 재정의가 감지되어 송출 연결을 차단했습니다.",
        _ when issues.HasFlag(K3dRegistrationIssue.RegistryReadFailed) =>
            "K3D 송출 구성 요소 등록을 확인할 수 없습니다.",
        _ when issues.HasFlag(K3dRegistrationIssue.ProcessIsNot64Bit) =>
            "송출 어댑터를 x64 프로세스로 실행해야 합니다.",
        _ when issues.HasFlag(K3dRegistrationIssue.TypeLibraryMissing) ||
               issues.HasFlag(K3dRegistrationIssue.Win64BinaryMissing) ||
               issues.HasFlag(K3dRegistrationIssue.Win64BinaryIsNotAmd64) ||
               issues.HasFlag(K3dRegistrationIssue.EngineServerIsNotAmd64) ||
               issues.HasFlag(K3dRegistrationIssue.EventHandlerServerIsNotAmd64) =>
            "x64 K3D 송출 구성 요소를 확인할 수 없습니다.",
        _ => "K3D 송출 구성 요소 등록이 올바르지 않습니다."
    };
}

internal sealed class WindowsK3dRegistrySnapshotSource : IK3dRegistrySnapshotSource
{
    internal static IReadOnlyList<string> CurrentUserOverridePaths { get; } = Array.AsReadOnly(
    [
        $"CLSID\\{K3dComConstants.KaEngineClassId}",
        $"CLSID\\{K3dComConstants.KaEventHandlerClassId}",
        K3dComConstants.KaEngineProgId,
        K3dComConstants.KaEventHandlerProgId,
        $"TypeLib\\{K3dComConstants.TypeLibraryId}"
    ]);

    public K3dRegistrySnapshot Capture()
    {
        using var localMachineBase = RegistryKey.OpenBaseKey(
            RegistryHive.LocalMachine,
            RegistryView.Registry64);
        using var localMachineClasses = localMachineBase.OpenSubKey("SOFTWARE\\Classes");
        using var currentUserBase = RegistryKey.OpenBaseKey(
            RegistryHive.CurrentUser,
            RegistryView.Registry64);
        using var currentUserClasses = currentUserBase.OpenSubKey("Software\\Classes");

        return new K3dRegistrySnapshot(
            Environment.Is64BitProcess,
            ReadDefault(
                localMachineClasses,
                $"TypeLib\\{K3dComConstants.TypeLibraryId}\\{K3dComConstants.TypeLibraryVersion}\\0\\win64"),
            ReadClass(
                localMachineClasses,
                K3dComConstants.KaEngineClassId,
                K3dComConstants.KaEngineProgId),
            ReadClass(
                localMachineClasses,
                K3dComConstants.KaEventHandlerClassId,
                K3dComConstants.KaEventHandlerProgId),
            HasCurrentUserOverride(currentUserClasses));
    }

    private static K3dClassRegistrationSnapshot ReadClass(
        RegistryKey? classes,
        string classId,
        string progId)
    {
        using var classKey = classes?.OpenSubKey($"CLSID\\{classId}");
        using var classProgId = classKey?.OpenSubKey("ProgID");
        using var inprocServer = classKey?.OpenSubKey("InprocServer32");
        using var typeLibrary = classKey?.OpenSubKey("TypeLib");
        using var progIdClass = classes?.OpenSubKey($"{progId}\\CLSID");

        return new K3dClassRegistrationSnapshot(
            classKey is not null,
            classProgId?.GetValue(null) as string,
            progIdClass?.GetValue(null) as string,
            inprocServer?.GetValue(null) as string,
            inprocServer?.GetValue("ThreadingModel") as string,
            typeLibrary?.GetValue(null) as string);
    }

    private static string? ReadDefault(RegistryKey? root, string relativePath)
    {
        using var key = root?.OpenSubKey(relativePath);
        return key?.GetValue(null) as string;
    }

    private static bool HasCurrentUserOverride(RegistryKey? currentUserClasses)
    {
        foreach (var path in CurrentUserOverridePaths)
        {
            using var key = currentUserClasses?.OpenSubKey(path);
            if (key is not null)
            {
                return true;
            }
        }

        return false;
    }
}

internal sealed class FilePeBinaryInspector : IPeBinaryInspector
{
    private const ushort ImageFileMachineAmd64 = 0x8664;
    private const uint PortableExecutableSignature = 0x00004550;

    public PeBinaryInspection Inspect(string? path)
    {
        if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
        {
            return new PeBinaryInspection(false, false);
        }

        try
        {
            using var stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.ReadWrite | FileShare.Delete);
            using var reader = new BinaryReader(stream);

            if (stream.Length < 64)
            {
                return new PeBinaryInspection(true, false);
            }

            stream.Position = 0x3c;
            var headerOffset = reader.ReadInt32();
            if (headerOffset < 0 || headerOffset > stream.Length - 6)
            {
                return new PeBinaryInspection(true, false);
            }

            stream.Position = headerOffset;
            var isAmd64 = reader.ReadUInt32() == PortableExecutableSignature &&
                          reader.ReadUInt16() == ImageFileMachineAmd64;
            return new PeBinaryInspection(true, isAmd64);
        }
        catch (IOException)
        {
            return new PeBinaryInspection(true, false);
        }
        catch (UnauthorizedAccessException)
        {
            return new PeBinaryInspection(true, false);
        }
    }
}
