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

namespace DragonISO.App.Services;

/// <summary>
/// Small Win32 wrapper that launches the Microsoft Teams desktop client as a
/// subprocess of DragonISO. 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 Dragon-ISO 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; <paramref name="errorMessage"/> includes the
    /// reasons each attempt was rejected so the operator can see why.
    ///
    /// Path order matters:
    ///   1. <c>ms-teams:</c> URI â€” new Teams (MSTeams AppX) registers this
    ///      handler at install. Activates through the AppX shell so the
    ///      stub <c>ms-teams.exe</c> in WindowsApps gets the right context.
    ///   2. AppsFolder shell verb â€” direct AppX activation. Belt-and-braces
    ///      fallback if a misconfigured registry breaks the URI handler.
    ///   3. Classic Teams Update.exe â€” pre-2024 Teams installations.
    /// We deliberately DON'T try the bare <c>ms-teams.exe</c> WindowsApps
    /// path: it's a 0-byte AppX placeholder that fails silently when invoked
    /// without AppX activation context. Looked plausible, never worked.
    /// </summary>
    public static bool TryLaunch(out string? errorMessage)
    {
        errorMessage = null;
        var attempts = new List<string>();

        // Path 1: URI scheme. The shell handler picks the registered Teams
        // (new MSTeams takes priority on modern Windows). UseShellExecute=true
        // is required â€” Win32 Process creation can't open URIs directly.
        if (TryStart("ms-teams:", useShell: true, out var err1)) return true;
        attempts.Add($"ms-teams: URI â†’ {err1}");

        // Path 2: AppX activation via the explorer.exe shell. Modern Teams
        // ships as MSTeams_8wekyb3d8bbwe; if other code on the box has
        // clobbered the URI registration, this still works because it goes
        // through the AppsFolder verb the OS itself uses for Start menu launches.
        if (TryStart("explorer.exe", false, out var err2,
            arguments: "shell:appsFolder\\MSTeams_8wekyb3d8bbwe!MSTeams"))
            return true;
        attempts.Add($"AppsFolder shell â†’ {err2}");

        // 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)
            {
                attempts.Add($"classic Update.exe â†’ {ex.Message}");
            }
        }
        else
        {
            attempts.Add($"classic Update.exe â†’ not found at {classicUpdater}");
        }

        errorMessage = "No Microsoft Teams installation could be launched. " +
                       "Install Teams from https://www.microsoft.com/microsoft-teams and try again.\n\n" +
                       "Attempts:\n  â€¢ " + string.Join("\n  â€¢ ", attempts);
        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;
    }

    /// <summary>
    /// Hand a meeting URL off to the Teams shell handler. Accepts both the
    /// <c>https://teams.microsoft.com/l/meetup-join/...</c> web format and
    /// the <c>msteams:/l/meetup-join/...</c> deep-link form (either causes
    /// Teams to launch + join the meeting in one shot â€” the OS shell maps
    /// teams.microsoft.com URLs to the registered ms-teams: handler).
    ///
    /// Use case: operator pastes a meeting link they got over email / chat
    /// into Dragon-ISO's quick-join field instead of opening Teams,
    /// hunting down the calendar entry, and clicking Join. With auto-hide
    /// on, the Teams window flashes briefly then disappears; the operator
    /// is now in the meeting, driving routing from DragonISO.
    ///
    /// Returns true if the shell accepted the URL; false if URL is malformed
    /// or rejected. errorMessage populated on failure.
    /// </summary>
    public static bool TryJoinMeeting(string url, out string? errorMessage)
    {
        errorMessage = null;
        if (string.IsNullOrWhiteSpace(url))
        {
            errorMessage = "URL is empty.";
            return false;
        }

        var trimmed = url.Trim();

        // Defensive sanity-check: only accept URLs that obviously target
        // Teams. We don't want to invoke arbitrary shell handlers from a
        // clipboard paste â€” if someone pastes "calc.exe" into the input we
        // shouldn't launch it. Specifically: http(s) URLs must contain
        // "teams.microsoft.com" or "teams.live.com"; otherwise must start
        // with "msteams:".
        var lower = trimmed.ToLowerInvariant();
        var looksLikeTeams =
            lower.StartsWith("msteams:") ||
            (lower.StartsWith("http://") || lower.StartsWith("https://")) &&
                (lower.Contains("teams.microsoft.com") || lower.Contains("teams.live.com"));
        if (!looksLikeTeams)
        {
            errorMessage = "Not a Microsoft Teams meeting URL. " +
                           "Expected a https://teams.microsoft.com/l/meetup-join/... " +
                           "or msteams:/l/meetup-join/... link.";
            return false;
        }

        if (TryStart(trimmed, useShell: true, out var err))
            return true;
        errorMessage = err;
        return false;
    }

    private static bool TryStart(string target, bool useShell, out string error, string? arguments = null)
    {
        error = string.Empty;
        try
        {
            var info = new ProcessStartInfo
            {
                FileName = target,
                UseShellExecute = useShell,
                CreateNoWindow = true,
            };
            if (arguments is not null) info.Arguments = arguments;
            Process.Start(info);
            return true;
        }
        catch (Exception ex)
        {
            error = ex.Message;
            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 DragonISO. 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);

    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern int GetWindowTextW(IntPtr hWnd, [Out] System.Text.StringBuilder lpString, int nMaxCount);

    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern int GetWindowTextLengthW(IntPtr hWnd);

    private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
    /// <summary>
    /// Returns the title bar text of Teams' most-recently-used top-level
    /// window, or empty string if Teams isn't running. Modern Teams puts
    /// the meeting title in the window title while in a call ("Meeting with
    /// Alice | Microsoft Teams"), so this is the cheapest way to surface
    /// meeting context to Dragon-ISO's UI without burning a UIA traversal.
    ///
    /// Includes hidden windows â€” operators using auto-hide still get the
    /// title surfaced, which is the whole point.
    /// </summary>
    public static string GetActiveWindowTitle()
    {
        try
        {
            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 string.Empty;

            string longestTitle = string.Empty;
            EnumWindows((hWnd, _) =>
            {
                if (GetWindow(hWnd, GW_OWNER) != IntPtr.Zero) return true;
                GetWindowThreadProcessId(hWnd, out var pid);
                if (!teamsPids.Contains(pid)) return true;

                var len = GetWindowTextLengthW(hWnd);
                if (len <= 0) return true;
                var sb = new System.Text.StringBuilder(len + 1);
                GetWindowTextW(hWnd, sb, sb.Capacity);
                var title = sb.ToString();
                // Teams creates a few top-level windows per process; the
                // call/meeting window has the longest title (other windows
                // tend to just be "Microsoft Teams"). Pick the longest one
                // as a heuristic for "most informative".
                if (title.Length > longestTitle.Length) longestTitle = title;
                return true;
            }, IntPtr.Zero);
            return longestTitle;
        }
        catch { return string.Empty; }
    }

    /// <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>
    /// <summary>
    /// Return the visible top-level windows owned by any Teams process.
    /// Exposed internal so <see cref="TeamsEmbedHost"/> can pick the
    /// "best" candidate to reparent without re-implementing the
    /// enumeration. Keep this in TeamsLauncher because the launch /
    /// hide / show paths use the same list.
    /// </summary>
    internal static List<IntPtr> EnumerateTopLevelTeamsWindows()
    {
        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 = EnumerateTopLevelTeamsWindows();
        foreach (var w in windows) ShowWindow(w, SW_HIDE);
        return windows.Count;
    }

    /// <summary>
    /// Fire-and-forget background watcher that polls every 250ms for up to
    /// <paramref name="timeout"/> and hides any visible top-level Teams
    /// windows it finds. Used after launch so the operator never sees the
    /// Teams UI flash on screen â€” Teams takes 2-5s to splash + render its
    /// main window, and the splash arrives separately from the main window
    /// (so we keep polling past the first hide to catch follow-up windows).
    ///
    /// Returns the Task so callers can await completion if they want, but
    /// production code should fire-and-forget. Exceptions are swallowed â€”
    /// failure to hide is harmless (user just sees Teams briefly).
    /// </summary>
    public static Task AutoHideAfterLaunchAsync(TimeSpan? timeout = null, CancellationToken ct = default)
    {
        var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(15));
        return Task.Run(async () =>
        {
            try
            {
                var hiddenAny = false;
                while (!ct.IsCancellationRequested && DateTime.UtcNow < deadline)
                {
                    // Poll for visible windows. Each iteration may catch new
                    // ones â€” Teams sometimes opens a small splash, then a
                    // larger main window 1-2s later, then a "What's new"
                    // banner. Keep hiding until we've gone a full second
                    // with nothing new appearing.
                    var hidden = HideWindows();
                    if (hidden > 0)
                    {
                        hiddenAny = true;
                        // Settling delay: after we hide windows, wait a beat
                        // before polling again so we don't busy-loop while
                        // Teams' window manager catches up.
                        await Task.Delay(750, ct).ConfigureAwait(false);
                    }
                    else if (hiddenAny)
                    {
                        // We hid at least once; if the next poll finds
                        // nothing, Teams has settled. Bail early.
                        return;
                    }
                    else
                    {
                        // Teams hasn't materialized yet; keep waiting.
                        await Task.Delay(250, ct).ConfigureAwait(false);
                    }
                }
            }
            catch (OperationCanceledException) { /* expected on cancel */ }
            catch { /* defensive â€” auto-hide is best-effort, never breaks the app */ }
        }, ct);
    }

    // â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    // 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 = EnumerateTopLevelTeamsWindows();
        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;
    }
}
