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

namespace TeamsISO.App.Services;

/// <summary>
/// Small Win32 wrapper that launches the Microsoft Teams desktop client as a
/// subprocess of TeamsISO. First step toward Phase E.1 of the embedded-Teams
/// roadmap (see docs/superpowers/specs/2026-05-08-embedded-teams-orchestration.md):
/// the operator can launch Teams from within TeamsISO so they don't have to
/// switch apps to start a meeting.
///
/// The launcher tries (in order):
///   1. ms-teams: URI  (works for both classic and new Teams)
///   2. MSTeams.exe in %LOCALAPPDATA%\Microsoft\WindowsApps\
///   3. Teams.exe in %LOCALAPPDATA%\Microsoft\Teams\Update.exe (legacy)
///
/// Group-routing automation (writing NDI Access Manager config so Teams
/// broadcasts on a private group) is deferred to a follow-up — for v1.0 we
/// document the manual steps in RELEASING.md and trust the operator to set
/// them once per machine.
/// </summary>
public static class TeamsLauncher
{
    /// <summary>
    /// Heuristic process-name candidates we'll consider as "the Teams process" when
    /// the rail toggle wants to find a running instance. New MSTeams comes first.
    /// </summary>
    private static readonly string[] TeamsProcessNames =
    {
        "ms-teams",   // new MSTeams binary basename
        "msteams",    // alternate basename observed on some installs
        "Teams",      // classic Teams desktop client
    };

    /// <summary>
    /// True if any process matching the known Teams binary basenames is running.
    /// Used by the rail to decide whether to show "Launch Teams" vs "Stop Teams".
    /// </summary>
    public static bool IsRunning() =>
        TeamsProcessNames.Any(n => Process.GetProcessesByName(n).Length > 0);

    /// <summary>
    /// Launches Teams. Returns true if a launch was started successfully (the
    /// process may take a few seconds to actually appear). False if every
    /// fallback path failed.
    /// </summary>
    public static bool TryLaunch(out string? errorMessage)
    {
        errorMessage = null;

        // Path 1: URI scheme. The shell handler picks whichever Teams client
        // is registered (new MSTeams.exe takes priority on modern Windows).
        if (TryStart("ms-teams:", useShell: true)) return true;

        // Path 2: new Teams' WindowsApps shim.
        var newTeams = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "Microsoft", "WindowsApps", "ms-teams.exe");
        if (File.Exists(newTeams) && TryStart(newTeams, useShell: false)) return true;

        // Path 3: classic Teams Update.exe with --processStart hands off to
        // the actual Teams.exe via Squirrel.
        var classicUpdater = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "Microsoft", "Teams", "Update.exe");
        if (File.Exists(classicUpdater))
        {
            try
            {
                Process.Start(new ProcessStartInfo
                {
                    FileName = classicUpdater,
                    Arguments = "--processStart \"Teams.exe\"",
                    UseShellExecute = false,
                    CreateNoWindow = true,
                });
                return true;
            }
            catch (Exception ex)
            {
                errorMessage = ex.Message;
            }
        }

        errorMessage ??= "No Microsoft Teams installation was found. Install Teams from https://www.microsoft.com/microsoft-teams and try again.";
        return false;
    }

    /// <summary>
    /// Asks every running Teams process to close gracefully via WM_CLOSE
    /// (CloseMainWindow). Returns the count of processes that exited cleanly within
    /// <paramref name="gracePeriod"/>. Stragglers are NOT force-killed — Teams' own
    /// "are you sure" prompt may legitimately keep a process alive briefly, and we
    /// don't want to nuke the user's call mid-transition.
    /// </summary>
    public static int StopAll(TimeSpan? gracePeriod = null)
    {
        var grace = gracePeriod ?? TimeSpan.FromSeconds(3);
        var deadline = DateTime.UtcNow + grace;
        var asked = 0;
        foreach (var name in TeamsProcessNames)
        {
            foreach (var p in Process.GetProcessesByName(name))
            {
                try
                {
                    if (p.HasExited) { p.Dispose(); continue; }
                    if (p.MainWindowHandle != IntPtr.Zero)
                    {
                        p.CloseMainWindow();
                        asked++;
                    }
                }
                catch { /* defensive: process may have died between enumeration and signal */ }
                finally { p.Dispose(); }
            }
        }
        // Best-effort wait so the rail can flip its icon promptly.
        while (DateTime.UtcNow < deadline && IsRunning())
            Thread.Sleep(150);
        return asked;
    }

    private static bool TryStart(string target, bool useShell)
    {
        try
        {
            var info = new ProcessStartInfo
            {
                FileName = target,
                UseShellExecute = useShell,
                CreateNoWindow = true,
            };
            Process.Start(info);
            return true;
        }
        catch
        {
            return false;
        }
    }

    // ════════════════════════════════════════════════════════════════════════
    // Phase E.2 — window orchestration
    //
    // Once Teams is running, we want to be able to hide its main window so the
    // operator only sees TeamsISO. We do this by enumerating top-level windows,
    // matching against each Teams process Id, and ShowWindow(SW_HIDE)-ing each
    // match. To restore we ShowWindow(SW_SHOW) and SetForegroundWindow.
    //
    // We deliberately don't use the Process.MainWindowHandle convenience because
    // new MSTeams (WebView2-hosted) creates several top-level windows per
    // process and Process picks an inconsistent one across launches; iterating
    // via EnumWindows + GetWindowThreadProcessId catches every visible window
    // owned by the process.
    // ════════════════════════════════════════════════════════════════════════

    private const int SW_HIDE = 0;
    private const int SW_SHOWNORMAL = 1;
    private const int SW_SHOW = 5;
    private const int SW_RESTORE = 9;

    private const uint GW_OWNER = 4;

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

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

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool IsWindowVisible(IntPtr hWnd);

    [DllImport("user32.dll")]
    private static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

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

    /// <summary>
    /// Enumerate every visible top-level window owned by any running Teams
    /// process. "Top-level" means GetWindow(GW_OWNER) is null (the window is
    /// not a tooltip or popup of another). Used by Hide/Show.
    /// </summary>
    private static List<IntPtr> FindTeamsTopLevelWindows()
    {
        var teamsPids = new HashSet<uint>(
            TeamsProcessNames
                .SelectMany(n => Process.GetProcessesByName(n))
                .Select(p => { try { return (uint)p.Id; } finally { p.Dispose(); } }));
        if (teamsPids.Count == 0) return new List<IntPtr>();

        var windows = new List<IntPtr>();
        EnumWindows((hWnd, _) =>
        {
            if (!IsWindowVisible(hWnd)) return true;
            if (GetWindow(hWnd, GW_OWNER) != IntPtr.Zero) return true; // not top-level
            GetWindowThreadProcessId(hWnd, out var pid);
            if (teamsPids.Contains(pid)) windows.Add(hWnd);
            return true;
        }, IntPtr.Zero);
        return windows;
    }

    /// <summary>
    /// Hides every visible top-level Teams window. Returns the count hidden;
    /// 0 means Teams isn't running or has no visible windows yet (it can take
    /// a couple seconds after launch for the splash to materialize).
    /// </summary>
    public static int HideWindows()
    {
        var windows = FindTeamsTopLevelWindows();
        foreach (var w in windows) ShowWindow(w, SW_HIDE);
        return windows.Count;
    }

    // ────────────────────────────────────────────────────────────────────
    // Keyboard-shortcut forwarding (PostMessage path).
    //
    // UIAutomation (TeamsControlBridge) is our preferred way to drive Teams
    // because it works regardless of foreground/visibility state. PostMessage
    // is a fallback for shortcuts that don't have a stable UIA-discoverable
    // button — chat scroll, custom keymap actions, etc. Note: WebView2-hosted
    // Teams (the modern client) frequently ignores PostMessage(WM_KEYDOWN) at
    // its app-shortcut layer because shortcut routing happens after focus
    // changes, not on raw key messages. Treat this as best-effort.
    // ────────────────────────────────────────────────────────────────────

    private const uint WM_KEYDOWN = 0x0100;
    private const uint WM_KEYUP = 0x0101;
    private const uint WM_CHAR = 0x0102;
    private const uint WM_SYSKEYDOWN = 0x0104;
    private const uint WM_SYSKEYUP = 0x0105;

    [Flags]
    public enum ShortcutModifiers
    {
        None = 0,
        Ctrl = 1,
        Shift = 2,
        Alt = 4,
    }

    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool PostMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

    /// <summary>
    /// Sends a synthesized key press (modifier-down, key-down, key-up,
    /// modifier-up) to the most recently used top-level Teams window via
    /// PostMessage. Returns true if a window was found to send to. Note that
    /// returning true doesn't guarantee Teams reacted — modern WebView2-based
    /// Teams sometimes ignores synthesized key messages at the app-shortcut
    /// layer. Prefer UIA (<see cref="TeamsControlBridge"/>) when an equivalent
    /// button exists.
    /// </summary>
    public static bool SendShortcut(ShortcutModifiers modifiers, int virtualKey)
    {
        var windows = FindTeamsTopLevelWindows();
        if (windows.Count == 0) return false;
        var hwnd = windows[^1];

        // Modifier key downs
        if ((modifiers & ShortcutModifiers.Ctrl) != 0)
            PostMessage(hwnd, WM_KEYDOWN, (IntPtr)0x11, IntPtr.Zero); // VK_CONTROL
        if ((modifiers & ShortcutModifiers.Shift) != 0)
            PostMessage(hwnd, WM_KEYDOWN, (IntPtr)0x10, IntPtr.Zero); // VK_SHIFT
        if ((modifiers & ShortcutModifiers.Alt) != 0)
            PostMessage(hwnd, WM_SYSKEYDOWN, (IntPtr)0x12, IntPtr.Zero); // VK_MENU

        // Main key down + up
        PostMessage(hwnd, WM_KEYDOWN, (IntPtr)virtualKey, IntPtr.Zero);
        PostMessage(hwnd, WM_KEYUP, (IntPtr)virtualKey, IntPtr.Zero);

        // Modifier key ups (reverse order)
        if ((modifiers & ShortcutModifiers.Alt) != 0)
            PostMessage(hwnd, WM_SYSKEYUP, (IntPtr)0x12, IntPtr.Zero);
        if ((modifiers & ShortcutModifiers.Shift) != 0)
            PostMessage(hwnd, WM_KEYUP, (IntPtr)0x10, IntPtr.Zero);
        if ((modifiers & ShortcutModifiers.Ctrl) != 0)
            PostMessage(hwnd, WM_KEYUP, (IntPtr)0x11, IntPtr.Zero);

        return true;
    }

    /// <summary>
    /// Restores every Teams top-level window from hidden state and brings the
    /// most recently used one to the foreground. Returns the count shown.
    /// </summary>
    public static int ShowWindows()
    {
        // To find hidden windows too we still enumerate, but our IsWindowVisible
        // filter would skip them. Re-implement here with the visible check off.
        var teamsPids = new HashSet<uint>(
            TeamsProcessNames
                .SelectMany(n => Process.GetProcessesByName(n))
                .Select(p => { try { return (uint)p.Id; } finally { p.Dispose(); } }));
        var windows = new List<IntPtr>();
        EnumWindows((hWnd, _) =>
        {
            if (GetWindow(hWnd, GW_OWNER) != IntPtr.Zero) return true;
            GetWindowThreadProcessId(hWnd, out var pid);
            if (teamsPids.Contains(pid)) windows.Add(hWnd);
            return true;
        }, IntPtr.Zero);

        foreach (var w in windows) ShowWindow(w, SW_SHOW);
        if (windows.Count > 0) SetForegroundWindow(windows[^1]);
        return windows.Count;
    }
}
