using System.Diagnostics;
using System.Windows.Automation;

namespace TeamsISO.App.Services;

/// <summary>
/// Phase E.3 — UIAutomation bridge for the in-call controls (mute, camera,
/// leave, share screen). Walks Teams' automation tree to locate the relevant
/// buttons and invokes their <see cref="InvokePattern"/> or <see cref="TogglePattern"/>.
///
/// This is intentionally tolerant of Teams' UI volatility: we search by a
/// chain of (AutomationId, Name, LocalizedControlType) candidates rather than
/// pinning to a single identifier. When Teams ships a new build that renames a
/// button, the operator gets a clear "control not found" toast rather than a
/// crash, and we add the new identifier to the candidate list.
///
/// Limitations:
///   - Requires Teams' main window to be present (not minimized to the system tray
///     in a way that detaches its automation peers; minimized to taskbar is fine).
///   - Some Teams builds host the call UI in a separate WebView2-backed top-level
///     window; we enumerate every top-level window owned by every Teams process,
///     so we'll find it wherever it lives.
///   - Hidden windows (after <see cref="TeamsLauncher.HideWindows"/>) are still
///     traversable by UIAutomation — the buttons exist in the automation tree
///     even when their HWND is SW_HIDDEN. This is what makes the "hide Teams,
///     drive it from TeamsISO" workflow viable.
/// </summary>
public static class TeamsControlBridge
{
    // ────────────────────────────────────────────────────────────────────
    // Localized candidate-name lists.
    //
    // Teams localizes the AutomationElement.Name we match against. The lookup
    // strategy is: ALL candidate strings across all locales are tried for each
    // command, and the first match wins. This gives us a single binary that
    // works regardless of the Teams UI language without needing to detect it
    // — at the cost of a slightly broader match surface (a non-mute button
    // with the German word "Stumm" in its name would false-positive). In
    // practice Teams' button Names are highly distinctive and we haven't seen
    // false positives during development.
    //
    // Adding a locale: append the localized strings to each command's array.
    // Order doesn't matter for correctness; for performance we put the most
    // common locales first since the array is iterated in order.
    // ────────────────────────────────────────────────────────────────────

    private static readonly string[] MuteCandidates =
    {
        // English (US/UK)
        "Mute", "Unmute", "Microphone", "Toggle mute",
        // German
        "Stumm", "Stummschaltung", "Stummschalten", "Stummschaltung aufheben", "Mikrofon",
        // Spanish
        "Silenciar", "Activar audio", "Micrófono",
        // French
        "Désactiver le micro", "Activer le micro", "Micro", "Microphone",
        // Portuguese
        "Desativar áudio", "Ativar áudio", "Microfone",
        // Japanese
        "ミュート", "ミュート解除", "マイク",
    };

    private static readonly string[] CameraCandidates =
    {
        "Camera", "Turn camera on", "Turn camera off", "Video",
        // German
        "Kamera", "Kamera einschalten", "Kamera ausschalten", "Video",
        // Spanish
        "Cámara", "Activar cámara", "Desactivar cámara", "Vídeo",
        // French
        "Caméra", "Activer la caméra", "Désactiver la caméra", "Vidéo",
        // Portuguese
        "Câmera", "Ativar câmera", "Desativar câmera",
        // Japanese
        "カメラ", "ビデオ",
    };

    private static readonly string[] LeaveCandidates =
    {
        "Leave", "Hang up", "End call", "Leave call",
        // German
        "Verlassen", "Auflegen", "Anruf beenden",
        // Spanish
        "Salir", "Colgar", "Finalizar llamada",
        // French
        "Quitter", "Raccrocher", "Terminer l'appel",
        // Portuguese
        "Sair", "Desligar", "Encerrar chamada",
        // Japanese
        "退出", "通話を終了",
    };

    private static readonly string[] ShareCandidates =
    {
        "Share", "Share content", "Share screen", "Open share tray",
        // German
        "Teilen", "Inhalt teilen", "Bildschirm teilen",
        // Spanish
        "Compartir", "Compartir contenido", "Compartir pantalla",
        // French
        "Partager", "Partager du contenu", "Partager l'écran",
        // Portuguese
        "Compartilhar", "Compartilhar conteúdo", "Compartilhar tela",
        // Japanese
        "共有", "コンテンツの共有", "画面を共有",
    };

    private static readonly string[] RaiseHandCandidates =
    {
        "Raise", "Raise hand", "Lower hand",
        // German
        "Hand heben", "Hand senken",
        // Spanish
        "Levantar la mano", "Bajar la mano",
        // French
        "Lever la main", "Baisser la main",
        // Portuguese
        "Levantar a mão", "Abaixar a mão",
        // Japanese
        "手を挙げる", "手を下ろす",
    };

    private static readonly string[] ToggleChatCandidates =
    {
        "Show conversation", "Hide conversation", "Chat", "Show chat",
        // German
        "Unterhaltung anzeigen", "Unterhaltung ausblenden", "Chat",
        // Spanish
        "Mostrar conversación", "Ocultar conversación", "Chat",
        // French
        "Afficher la conversation", "Masquer la conversation", "Conversation",
        // Portuguese
        "Mostrar conversa", "Ocultar conversa", "Chat",
        // Japanese
        "会話を表示", "会話を非表示", "チャット",
    };

    private static readonly string[] BackgroundBlurCandidates =
    {
        "Background effects", "Apply background effects", "Background filters",
        // German
        "Hintergrundeffekte", "Hintergrundfilter",
        // Spanish
        "Efectos de fondo", "Filtros de fondo",
        // French
        "Effets d'arrière-plan", "Filtres d'arrière-plan",
        // Portuguese
        "Efeitos de plano de fundo", "Filtros de plano de fundo",
        // Japanese
        "背景効果", "背景フィルター",
    };

    /// <summary>Result of attempting one of the in-call commands.</summary>
    public enum InvokeResult
    {
        /// <summary>The control was found and invoked successfully.</summary>
        Invoked,
        /// <summary>Teams isn't running, or its automation root couldn't be located.</summary>
        TeamsNotRunning,
        /// <summary>Teams is running but the matching button isn't currently exposed (maybe not in a call).</summary>
        ControlNotFound,
        /// <summary>The button was found but didn't expose a usable invoke / toggle pattern.</summary>
        InvokeFailed,
    }

    public static InvokeResult ToggleMute() => InvokeFirstMatch(MuteCandidates);
    public static InvokeResult ToggleCamera() => InvokeFirstMatch(CameraCandidates);
    public static InvokeResult LeaveCall() => InvokeFirstMatch(LeaveCandidates);
    public static InvokeResult OpenShareTray() => InvokeFirstMatch(ShareCandidates);
    public static InvokeResult ToggleRaiseHand() => InvokeFirstMatch(RaiseHandCandidates);
    public static InvokeResult ToggleChat() => InvokeFirstMatch(ToggleChatCandidates);
    public static InvokeResult OpenBackgroundEffects() => InvokeFirstMatch(BackgroundBlurCandidates);

    /// <summary>
    /// Snapshot of the current call's local-user state. Read via a single
    /// UIA traversal in <see cref="DetectCallState"/>; null sub-fields when
    /// the call isn't active or the button isn't in the tree.
    /// </summary>
    public sealed record CallStateSnapshot(bool IsInCall, bool? IsMuted, bool? IsCameraOff);

    /// <summary>
    /// One-shot UIA probe of Teams' in-call controls. The Mute and Camera
    /// buttons toggle their Name between "Mute"/"Unmute" and "Turn camera
    /// on"/"Turn camera off" depending on state, so reading the Name tells
    /// us whether the operator is currently muted / camera-off.
    ///
    /// Returns IsInCall=false if Teams isn't running or no Leave button
    /// exists. Returns IsMuted/IsCameraOff as null if those buttons aren't
    /// found in this build (defensive — Teams sometimes uses different
    /// candidate names across locales).
    /// </summary>
    public static CallStateSnapshot DetectCallState()
    {
        var roots = GetTeamsAutomationRoots();
        if (roots.Count == 0) return new CallStateSnapshot(false, null, null);

        var inCall = false;
        bool? muted = null;
        bool? camOff = null;

        foreach (var root in roots)
        {
            AutomationElementCollection allButtons;
            try
            {
                allButtons = root.FindAll(
                    TreeScope.Descendants,
                    new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button));
            }
            catch { continue; }

            foreach (AutomationElement btn in allButtons)
            {
                var name = SafeGetName(btn);
                if (string.IsNullOrEmpty(name)) continue;
                var lower = name.ToLowerInvariant();

                if (!inCall && LeaveCandidates.Any(c => name.Contains(c, StringComparison.OrdinalIgnoreCase)))
                    inCall = true;

                // Mute button: name is "Mute" when active-can-mute, "Unmute"
                // when currently muted. Detect by checking for "unmute" first
                // (more specific) before falling to "mute" (more general).
                if (muted is null)
                {
                    if (lower.Contains("unmute") || lower.Contains("stummschaltung aufheben") ||
                        lower.Contains("activar audio") || lower.Contains("activer le micro") ||
                        lower.Contains("ativar áudio") || lower.Contains("ミュート解除"))
                        muted = true;
                    else if (lower.Contains("mute") || lower.Contains("stummschalten") ||
                             lower.Contains("silenciar") || lower.Contains("désactiver le micro") ||
                             lower.Contains("desativar áudio") || lower.Contains("ミュート"))
                        muted = false;
                }

                // Camera button: name is "Turn camera off" when on, "Turn
                // camera on" when off.
                if (camOff is null)
                {
                    if (lower.Contains("turn camera on") || lower.Contains("kamera einschalten") ||
                        lower.Contains("activar cámara") || lower.Contains("activer la caméra") ||
                        lower.Contains("ativar câmera"))
                        camOff = true;
                    else if (lower.Contains("turn camera off") || lower.Contains("kamera ausschalten") ||
                             lower.Contains("desactivar cámara") || lower.Contains("désactiver la caméra") ||
                             lower.Contains("desativar câmera"))
                        camOff = false;
                }
            }
        }
        return new CallStateSnapshot(inCall, muted, camOff);
    }

    /// <summary>
    /// True if Teams is currently in an active call. The Leave / Hang-up
    /// button only exists in the automation tree when a call is in progress,
    /// so its presence is a reliable in-call signal across Teams versions.
    /// Returns false if Teams isn't running, isn't in a call, or the call
    /// UI is in a state we don't recognize.
    ///
    /// This is the "tell me what Teams is doing without me having to look
    /// at it" probe — operators using auto-hide Teams want a status pill
    /// that says "In call · ready" without having to restore the Teams
    /// window. Safe to call from any thread (UIA traversal is thread-safe);
    /// not free (walks the descendant tree) so callers should poll at most
    /// a few times per second.
    /// </summary>
    public static bool IsInCall()
    {
        var roots = GetTeamsAutomationRoots();
        if (roots.Count == 0) return false;

        foreach (var root in roots)
        {
            AutomationElementCollection allButtons;
            try
            {
                allButtons = root.FindAll(
                    TreeScope.Descendants,
                    new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button));
            }
            catch
            {
                // Window died mid-traversal; try the next root.
                continue;
            }

            foreach (AutomationElement btn in allButtons)
            {
                var name = SafeGetName(btn);
                if (string.IsNullOrEmpty(name)) continue;
                if (LeaveCandidates.Any(c => name.Contains(c, StringComparison.OrdinalIgnoreCase)))
                    return true;
            }
        }
        return false;
    }

    private static InvokeResult InvokeFirstMatch(IReadOnlyList<string> candidateNames)
    {
        var roots = GetTeamsAutomationRoots();
        if (roots.Count == 0) return InvokeResult.TeamsNotRunning;

        foreach (var root in roots)
        {
            // Search by Name first (most common case for Teams). Use a NameProperty
            // contains-style match by collecting all Buttons in the subtree and then
            // filtering manually — Condition only supports equality, and Teams'
            // labels can include trailing state ("(unmuted)") that breaks equality.
            var allButtons = root.FindAll(
                TreeScope.Descendants,
                new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Button));

            foreach (AutomationElement btn in allButtons)
            {
                var name = SafeGetName(btn);
                if (string.IsNullOrEmpty(name)) continue;
                if (!candidateNames.Any(c => name.Contains(c, StringComparison.OrdinalIgnoreCase)))
                    continue;

                if (TryInvoke(btn)) return InvokeResult.Invoked;
            }
        }
        return InvokeResult.ControlNotFound;
    }

    /// <summary>
    /// Returns the AutomationElement root for every top-level window owned by
    /// any running Teams process. Multiple roots is the normal case for new
    /// MSTeams (which uses one window per call/chat).
    /// </summary>
    private static List<AutomationElement> GetTeamsAutomationRoots()
    {
        var teamsPids = new HashSet<int>(
            Process.GetProcessesByName("ms-teams")
                .Concat(Process.GetProcessesByName("msteams"))
                .Concat(Process.GetProcessesByName("Teams"))
                .Select(p => { try { return p.Id; } finally { p.Dispose(); } }));

        if (teamsPids.Count == 0) return new List<AutomationElement>();

        // Filter the desktop's children to windows whose ProcessId matches.
        var desktop = AutomationElement.RootElement;
        var allWindows = desktop.FindAll(
            TreeScope.Children,
            new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window));

        var roots = new List<AutomationElement>();
        foreach (AutomationElement w in allWindows)
        {
            try
            {
                var pid = (int)w.Current.ProcessId;
                if (teamsPids.Contains(pid)) roots.Add(w);
            }
            catch
            {
                // Window died between enumeration and property read; skip.
            }
        }
        return roots;
    }

    private static string SafeGetName(AutomationElement el)
    {
        try { return el.Current.Name ?? string.Empty; }
        catch { return string.Empty; }
    }

    /// <summary>
    /// Try Invoke first (most buttons), then Toggle (mute/camera are usually
    /// toggle-pattern). Returns true if either succeeded.
    /// </summary>
    private static bool TryInvoke(AutomationElement el)
    {
        try
        {
            if (el.TryGetCurrentPattern(InvokePattern.Pattern, out var invoke))
            {
                ((InvokePattern)invoke).Invoke();
                return true;
            }
            if (el.TryGetCurrentPattern(TogglePattern.Pattern, out var toggle))
            {
                ((TogglePattern)toggle).Toggle();
                return true;
            }
        }
        catch
        {
            // ElementNotEnabledException, ElementNotAvailableException — Teams
            // disabled the button mid-traversal (e.g. mute is disabled before
            // joining a call). Treat as "found but couldn't invoke" so the
            // caller can surface a useful message.
        }
        return false;
    }
}
