using System.Diagnostics;
using System.Runtime.InteropServices;

namespace VoiceCat.App.Audio;

// ── Scope types (mirror macOS ScreenAudioScope / ScreenAudioSelection) ────────

public abstract record AppAudioScope;
// ExcludeSelf=true captures the whole system render mix minus VoiceCat's own process tree
// (kills the self-echo loop). Routed through the external-feed mixer as a single EXCLUDE
// capture; ExcludeSelf=false keeps the core's whole-device loopback path.
public sealed record EntireDesktop(bool ExcludeSelf = false) : AppAudioScope;
public sealed record OnlyApps(IReadOnlyList<int> Pids, IReadOnlyList<string> Names) : AppAudioScope;
public sealed record AllExceptApps(IReadOnlyList<int> Pids, IReadOnlyList<string> Names) : AppAudioScope;

public sealed record AudioAppInfo(int Pid, string DisplayName);

// ── Enumerator ─────────────────────────────────────────────────────────────────

public static class AudioSessionEnumerator
{
    // Returns all user-facing apps: visible-window processes (primary, like macOS
    // SCShareableContent.current) plus any background audio-session-only processes
    // (e.g. Spotify in mini-player). Excludes VoiceCat itself and system processes.
    public static IReadOnlyList<AudioAppInfo> GetAudioApps()
    {
        var seen   = new HashSet<int>();
        var result = new List<AudioAppInfo>();
        int selfPid = Environment.ProcessId;

        // ── 1. Visible-window processes (EnumWindows) ─────────────────────────
        // Same set macOS ScreenCaptureKit exposes: all apps with at least one
        // visible top-level window. Shows apps even when not currently producing audio.
        EnumWindows((hWnd, _) =>
        {
            if (!IsWindowVisible(hWnd)) return true;
            GetWindowThreadProcessId(hWnd, out uint pid);
            if (pid == 0 || pid == (uint)selfPid || !seen.Add((int)pid)) return true;

            try
            {
                var proc = Process.GetProcessById((int)pid);
                string name = proc.MainWindowTitle.Length > 0
                    ? $"{proc.ProcessName} — {proc.MainWindowTitle}"
                    : proc.ProcessName;
                if (!string.IsNullOrEmpty(proc.ProcessName))
                    result.Add(new AudioAppInfo((int)pid, name));
            }
            catch { /* process exited between EnumWindows and GetProcessById */ }

            return true; // continue enumeration
        }, IntPtr.Zero);

        // ── 2. Background audio-session processes (WASAPI, supplement) ────────
        // Catches apps that produce audio but have no visible window (screen reader,
        // background music player, etc.). Silently skipped if WASAPI is unavailable.
        AppendAudioSessionApps(seen, selfPid, result);

        return result.OrderBy(a => a.DisplayName, StringComparer.OrdinalIgnoreCase).ToList();
    }

    // ── EnumWindows P/Invoke ──────────────────────────────────────────────────

    private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

    [DllImport("user32.dll")]
    private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

    [DllImport("user32.dll")]
    private static extern bool IsWindowVisible(IntPtr hWnd);

    [DllImport("user32.dll")]
    private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

    // ── WASAPI audio session supplement ──────────────────────────────────────

    private static void AppendAudioSessionApps(HashSet<int> seen, int selfPid,
                                               List<AudioAppInfo> result)
    {
        IMMDeviceEnumerator? enumerator = null;
        IMMDevice? device    = null;
        IAudioSessionManager2? manager  = null;
        IAudioSessionEnumerator? sessions = null;

        try
        {
            enumerator = (IMMDeviceEnumerator)Activator.CreateInstance(
                Type.GetTypeFromCLSID(new Guid("BCDE0395-E52F-467C-8E3D-C4579291692E"))!)!;

            enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out device);

            var mgr2Iid = new Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F");
            device.Activate(ref mgr2Iid, 0x17 /*CLSCTX_ALL*/, IntPtr.Zero, out object mgr);
            manager = (IAudioSessionManager2)mgr;

            manager.GetSessionEnumerator(out sessions);
            sessions.GetCount(out int count);

            for (int i = 0; i < count; i++)
            {
                IAudioSessionControl? ctrl = null;
                try
                {
                    sessions.GetSession(i, out ctrl);
                    var ctrl2 = (IAudioSessionControl2)ctrl;
                    ctrl2.GetProcessId(out uint pid);

                    int ipid = (int)pid;
                    if (pid == 0 || ipid == selfPid || !seen.Add(ipid)) continue;

                    try
                    {
                        var proc = Process.GetProcessById(ipid);
                        if (!string.IsNullOrEmpty(proc.ProcessName))
                            result.Add(new AudioAppInfo(ipid, proc.ProcessName));
                    }
                    catch { /* exited */ }
                }
                catch { /* stale session */ }
                finally { if (ctrl != null) Marshal.ReleaseComObject(ctrl); }
            }
        }
        catch { /* no audio device or WASAPI unavailable — ignore */ }
        finally
        {
            if (sessions  != null) Marshal.ReleaseComObject(sessions);
            if (manager   != null) Marshal.ReleaseComObject(manager);
            if (device    != null) Marshal.ReleaseComObject(device);
            if (enumerator != null) Marshal.ReleaseComObject(enumerator);
        }
    }

    // ── COM interface declarations ─────────────────────────────────────────────

    [ComImport, Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMMDeviceEnumerator
    {
        [PreserveSig] int EnumAudioEndpoints(int dataFlow, int stateMask, out IntPtr devices);
        [PreserveSig] int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice endpoint);
        [PreserveSig] int GetDevice([MarshalAs(UnmanagedType.LPWStr)] string id, out IMMDevice device);
        [PreserveSig] int RegisterEndpointNotificationCallback(IntPtr client);
        [PreserveSig] int UnregisterEndpointNotificationCallback(IntPtr client);
    }

    [ComImport, Guid("D666063F-1587-4E43-81F1-B948E807363F"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IMMDevice
    {
        [PreserveSig] int Activate(ref Guid iid, int clsCtx, IntPtr activationParams,
            [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
        [PreserveSig] int OpenPropertyStore(int stgmAccess, out IntPtr propStore);
        [PreserveSig] int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id);
        [PreserveSig] int GetState(out int state);
    }

    [ComImport, Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioSessionManager2
    {
        [PreserveSig] int GetAudioSessionControl(ref Guid audioSessionGuid, int streamFlags,
            out IAudioSessionControl session);
        [PreserveSig] int GetSimpleAudioVolume(ref Guid audioSessionGuid, int streamFlags,
            out IntPtr audioVolume);
        [PreserveSig] int GetSessionEnumerator(out IAudioSessionEnumerator sessionEnum);
        [PreserveSig] int RegisterSessionNotification(IntPtr notification);
        [PreserveSig] int UnregisterSessionNotification(IntPtr notification);
        [PreserveSig] int RegisterDuckNotification([MarshalAs(UnmanagedType.LPWStr)] string sessionID,
            IntPtr notification);
        [PreserveSig] int UnregisterDuckNotification(IntPtr notification);
    }

    [ComImport, Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioSessionEnumerator
    {
        [PreserveSig] int GetCount(out int sessionCount);
        [PreserveSig] int GetSession(int sessionIndex, out IAudioSessionControl session);
    }

    [ComImport, Guid("F4B1A599-7266-4319-A8CA-E70ACB11E8CD"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioSessionControl
    {
        [PreserveSig] int GetState(out int state);
        [PreserveSig] int GetDisplayName([MarshalAs(UnmanagedType.LPWStr)] out string name);
        [PreserveSig] int SetDisplayName([MarshalAs(UnmanagedType.LPWStr)] string name,
            ref Guid eventContext);
        [PreserveSig] int GetIconPath([MarshalAs(UnmanagedType.LPWStr)] out string iconPath);
        [PreserveSig] int SetIconPath([MarshalAs(UnmanagedType.LPWStr)] string iconPath,
            ref Guid eventContext);
        [PreserveSig] int GetGroupingParam(out Guid groupingParam);
        [PreserveSig] int SetGroupingParam(ref Guid groupingParam, ref Guid eventContext);
        [PreserveSig] int RegisterAudioSessionNotification(IntPtr notification);
        [PreserveSig] int UnregisterAudioSessionNotification(IntPtr notification);
    }

    [ComImport, Guid("BFB7FF88-7239-4FC9-8FA2-07C950BE9C6D"),
     InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IAudioSessionControl2 : IAudioSessionControl
    {
        [PreserveSig] int GetSessionIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string id);
        [PreserveSig] int GetSessionInstanceIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string id);
        [PreserveSig] int GetProcessId(out uint pid);
        [PreserveSig] int IsSystemSoundsSession();
        [PreserveSig] int SetDuckingPreference(bool optOut);
    }
}
