using System.Diagnostics;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace MBN_STOCK_WEBVIEW.Playout.Runtime;

internal sealed record TornadoProcessSnapshot(
    int TotalProcessCount,
    int EligibleProcessCount,
    int ProgramProcessCount)
{
    public TornadoProcessGeneration EligibleProcessGeneration { get; init; } =
        TornadoProcessGeneration.Synthetic("eligible", EligibleProcessCount);

    public TornadoProcessGeneration ProgramProcessGeneration { get; init; } =
        TornadoProcessGeneration.Synthetic("program", ProgramProcessCount);

    public bool IdentityInspectionSucceeded { get; init; } = true;

    public bool AnyTornadoProcess => TotalProcessCount > 0;

    public bool HasSingleEligibleProcess => EligibleProcessCount == 1;

    public bool UnsafeProgramWindowMatched => ProgramProcessCount > 0;

    public bool HasSameProcessGeneration(TornadoProcessSnapshot other) =>
        IdentityInspectionSucceeded &&
        other.IdentityInspectionSucceeded &&
        EligibleProcessGeneration == other.EligibleProcessGeneration &&
        ProgramProcessGeneration == other.ProgramProcessGeneration;
}

internal readonly record struct TornadoProcessGeneration(string OpaqueValue)
{
    internal static TornadoProcessGeneration FromIdentities(
        string category,
        IEnumerable<TornadoProcessIdentity> identities)
    {
        var canonical = string.Join(
            ";",
            identities
                .OrderBy(identity => identity.ProcessId)
                .ThenBy(identity => identity.StartTimeUtcTicks)
                .Select(identity => string.Concat(
                    identity.ProcessId.ToString(CultureInfo.InvariantCulture),
                    ":",
                    identity.StartTimeUtcTicks.ToString(CultureInfo.InvariantCulture))));
        var digest = SHA256.HashData(Encoding.UTF8.GetBytes($"{category}|{canonical}"));
        return new TornadoProcessGeneration(Convert.ToHexString(digest));
    }

    internal static TornadoProcessGeneration Synthetic(string category, int count) =>
        FromIdentities(
            category,
            Enumerable.Range(0, Math.Max(count, 0))
                .Select(index => new TornadoProcessIdentity(index, 0)));
}

internal readonly record struct TornadoProcessIdentity(int ProcessId, long StartTimeUtcTicks);

internal interface ITornadoProcessProbe
{
    TornadoProcessSnapshot Capture(Regex? testWindowPattern);
}

internal sealed record TornadoWindowTitleSnapshot(
    IReadOnlyList<string> Titles,
    bool InspectionSucceeded);

internal readonly record struct TornadoWindowClassification(
    bool IsProgram,
    bool IsEligible);

internal interface ITornadoWindowTitleProbe
{
    TornadoWindowTitleSnapshot Capture(int processId);
}

internal sealed class TornadoProcessProbe : ITornadoProcessProbe
{
    internal const string ProcessNamePrefix = "Tornado2";
    private readonly ITornadoWindowTitleProbe _windowTitleProbe;

    internal TornadoProcessProbe()
        : this(new Win32TornadoWindowTitleProbe())
    {
    }

    internal TornadoProcessProbe(ITornadoWindowTitleProbe windowTitleProbe)
    {
        ArgumentNullException.ThrowIfNull(windowTitleProbe);
        _windowTitleProbe = windowTitleProbe;
    }

    public TornadoProcessSnapshot Capture(Regex? testWindowPattern)
    {
        var totalCount = 0;
        var eligibleCount = 0;
        var programCount = 0;
        var identityInspectionSucceeded = true;
        var eligibleIdentities = new List<TornadoProcessIdentity>();
        var programIdentities = new List<TornadoProcessIdentity>();
        Process[] processes;

        try
        {
            processes = Process.GetProcesses();
        }
        catch (Exception exception) when (IsProcessInspectionException(exception))
        {
            return new TornadoProcessSnapshot(0, 0, 0)
            {
                IdentityInspectionSucceeded = false
            };
        }

        foreach (var process in processes)
        {
            try
            {
                string processName;
                try
                {
                    processName = process.ProcessName;
                }
                catch (Exception exception) when (IsExitedProcessInspectionException(exception))
                {
                    // The process vanished after enumeration and can no longer own an output.
                    continue;
                }
                catch (Exception exception) when (IsProcessInspectionException(exception))
                {
                    // An unreadable process could be Tornado/PROGRAM; never undercount it as safe.
                    identityInspectionSucceeded = false;
                    continue;
                }

                if (!processName.StartsWith(ProcessNamePrefix, StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                totalCount++;
                TornadoWindowTitleSnapshot windowSnapshot;
                try
                {
                    windowSnapshot = _windowTitleProbe.Capture(process.Id);
                }
                catch (Exception exception) when (IsProcessInspectionException(exception))
                {
                    identityInspectionSucceeded = false;
                    continue;
                }

                if (!TryClassifyWindowSnapshot(
                        windowSnapshot,
                        testWindowPattern,
                        out var classification))
                {
                    identityInspectionSucceeded = false;
                    continue;
                }

                if (classification.IsProgram)
                {
                    programCount++;
                }

                if (classification.IsEligible)
                {
                    eligibleCount++;
                }

                if (!classification.IsEligible && !classification.IsProgram)
                {
                    continue;
                }

                if (!TryGetIdentity(process, out var identity))
                {
                    identityInspectionSucceeded = false;
                    continue;
                }

                if (classification.IsEligible)
                {
                    eligibleIdentities.Add(identity);
                }

                if (classification.IsProgram)
                {
                    programIdentities.Add(identity);
                }
            }
            finally
            {
                process.Dispose();
            }
        }

        return new TornadoProcessSnapshot(totalCount, eligibleCount, programCount)
        {
            EligibleProcessGeneration = TornadoProcessGeneration.FromIdentities(
                "eligible",
                eligibleIdentities),
            ProgramProcessGeneration = TornadoProcessGeneration.FromIdentities(
                "program",
                programIdentities),
            IdentityInspectionSucceeded = identityInspectionSucceeded
        };
    }

    internal static bool IsTornadoProcessName(string? processName) =>
        processName?.StartsWith(ProcessNamePrefix, StringComparison.OrdinalIgnoreCase) == true;

    internal static bool TryClassifyWindowSnapshot(
        TornadoWindowTitleSnapshot snapshot,
        Regex? testWindowPattern,
        out TornadoWindowClassification classification)
    {
        if (!snapshot.InspectionSucceeded ||
            !snapshot.Titles.Any(title => !string.IsNullOrWhiteSpace(title)))
        {
            classification = default;
            return false;
        }

        classification = ClassifyWindowTitles(snapshot.Titles, testWindowPattern);
        return true;
    }

    internal static TornadoWindowClassification ClassifyWindowTitles(
        IEnumerable<string?> titles,
        Regex? testWindowPattern)
    {
        ArgumentNullException.ThrowIfNull(titles);

        var meaningfulTitles = titles
            .Where(title => !string.IsNullOrWhiteSpace(title))
            .Select(title => title!)
            .ToArray();
        var isProgram = meaningfulTitles.Length == 0 ||
            meaningfulTitles.Any(IsProgramTitle);
        var isEligible = testWindowPattern is null;

        if (testWindowPattern is not null && !isProgram)
        {
            try
            {
                isEligible = meaningfulTitles.Any(title =>
                    HasExplicitTestMarker(title) &&
                    testWindowPattern.IsMatch(title));
            }
            catch (RegexMatchTimeoutException)
            {
                isEligible = false;
            }
        }

        return new TornadoWindowClassification(isProgram, isEligible);
    }

    internal static bool IsProgramTitle(string? title) =>
        string.IsNullOrWhiteSpace(title) ||
        title.Equals(ProcessNamePrefix, StringComparison.OrdinalIgnoreCase) ||
        title.Contains("PGM", StringComparison.OrdinalIgnoreCase) ||
        title.Contains("PROGRAM", StringComparison.OrdinalIgnoreCase);

    internal static bool HasExplicitTestMarker(string? title)
    {
        if (string.IsNullOrWhiteSpace(title))
        {
            return false;
        }

        const string marker = "TEST";
        for (var start = 0; start <= title.Length - marker.Length; start++)
        {
            if (!title.AsSpan(start, marker.Length).Equals(
                    marker.AsSpan(),
                    StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var beforeIsBoundary = start == 0 || !char.IsLetterOrDigit(title[start - 1]);
            var after = start + marker.Length;
            var afterIsBoundary = after == title.Length || !char.IsLetterOrDigit(title[after]);
            if (beforeIsBoundary && afterIsBoundary)
            {
                return true;
            }
        }

        return false;
    }

    private static bool TryGetIdentity(Process process, out TornadoProcessIdentity identity)
    {
        try
        {
            identity = new TornadoProcessIdentity(
                process.Id,
                process.StartTime.ToUniversalTime().Ticks);
            return true;
        }
        catch (Exception exception) when (IsProcessInspectionException(exception))
        {
            identity = default;
            return false;
        }
    }

    private static bool IsProcessInspectionException(Exception exception) =>
        exception is InvalidOperationException or
            ArgumentException or
            Win32Exception or
            NotSupportedException or
            UnauthorizedAccessException;

    private static bool IsExitedProcessInspectionException(Exception exception) =>
        exception is InvalidOperationException or ArgumentException;
}

internal sealed class Win32TornadoWindowTitleProbe : ITornadoWindowTitleProbe
{
    private const int ErrorInvalidWindowHandle = 1400;

    public TornadoWindowTitleSnapshot Capture(int processId)
    {
        if (processId <= 0)
        {
            return FailedSnapshot();
        }

        var titles = new List<string>();
        var inspectionSucceeded = true;
        EnumWindowsCallback callback = (windowHandle, _) =>
        {
            try
            {
                Marshal.SetLastPInvokeError(0);
                var threadId = GetWindowThreadProcessId(windowHandle, out var windowProcessId);
                if (threadId == 0)
                {
                    if (Marshal.GetLastPInvokeError() == ErrorInvalidWindowHandle)
                    {
                        // A top-level window can disappear while EnumWindows is walking the list.
                        return true;
                    }

                    inspectionSucceeded = false;
                    return false;
                }

                if (windowProcessId != unchecked((uint)processId))
                {
                    return true;
                }

                Marshal.SetLastPInvokeError(0);
                var titleLength = GetWindowTextLength(windowHandle);
                if (titleLength == 0)
                {
                    if (Marshal.GetLastPInvokeError() != 0)
                    {
                        inspectionSucceeded = false;
                        return false;
                    }

                    return true;
                }

                var titleBuilder = new StringBuilder(titleLength + 1);
                if (GetWindowText(windowHandle, titleBuilder, titleBuilder.Capacity) == 0)
                {
                    inspectionSucceeded = false;
                    return false;
                }

                var title = titleBuilder.ToString();
                if (!string.IsNullOrWhiteSpace(title))
                {
                    titles.Add(title);
                }

                return true;
            }
            catch (Exception)
            {
                inspectionSucceeded = false;
                return false;
            }
        };

        try
        {
            if (!EnumWindows(callback, nint.Zero) || !inspectionSucceeded)
            {
                return FailedSnapshot();
            }
        }
        catch (Exception)
        {
            return FailedSnapshot();
        }

        return new TornadoWindowTitleSnapshot(titles, InspectionSucceeded: true);
    }

    private static TornadoWindowTitleSnapshot FailedSnapshot() =>
        new(Array.Empty<string>(), InspectionSucceeded: false);

    [UnmanagedFunctionPointer(CallingConvention.Winapi)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private delegate bool EnumWindowsCallback(nint windowHandle, nint parameter);

    [DllImport("user32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool EnumWindows(
        EnumWindowsCallback callback,
        nint parameter);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern uint GetWindowThreadProcessId(
        nint windowHandle,
        out uint processId);

    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern int GetWindowTextLength(nint windowHandle);

    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern int GetWindowText(
        nint windowHandle,
        StringBuilder title,
        int maximumCharacterCount);
}
