using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
using Microsoft.Extensions.Logging;
using TeamsISO.App.ViewModels;
using TeamsISO.Engine.Controller;
using TeamsISO.Engine.Interop;
using TeamsISO.Engine.Logging;
using TeamsISO.Engine.NdiInterop;
using TeamsISO.Engine.Persistence;
using TeamsISO.Engine.Pipeline;

// Application + MessageBox aliases live in GlobalUsings.cs (project-wide).
// Don't redeclare here — Roslyn errors with CS1537 on duplicate alias.

namespace TeamsISO.App;

public partial class App : Application
{
    /// <summary>
    /// Per-user mutex name. Including the SID-equivalent (the username) ensures two
    /// different Windows users can each run TeamsISO on the same machine, while one
    /// user can't spawn duplicate instances that would contend over the NDI runtime
    /// and the shared %APPDATA%\TeamsISO\config.json.
    /// </summary>
    private static readonly string SingleInstanceMutexName =
        $"Local\\WildDragon.TeamsISO.SingleInstance.{Environment.UserName}";

    private System.Threading.Mutex? _singleInstanceMutex;
    private bool _ownsSingleInstanceMutex;
    private ThreadMessageEventHandler? _bringToFrontHandler;
    private ILoggerFactory? _loggerFactory;
    private NdiInteropPInvoke? _interop;
    private IsoController? _controller;
    private MainViewModel? _viewModel;
    private TeamsISO.App.Services.ControlSurfaceServer? _controlSurface;
    private TeamsISO.App.Services.OscBridge? _oscBridge;
    private TeamsISO.App.Services.DiskSpaceWatcher? _diskSpaceWatcher;
    private TeamsISO.App.Services.TrayIconHost? _trayIcon;

    /// <summary>
    /// REST control surface lifetime. Lives on App so the settings VM can flip
    /// it on/off without us plumbing yet another DI dependency through MainViewModel.
    /// Null between process startup and the OnStartup wire-up, and after OnExit.
    /// </summary>
    internal TeamsISO.App.Services.ControlSurfaceServer? ControlSurface => _controlSurface;

    /// <summary>OSC bridge (UDP) lifetime — same lifecycle pattern as the REST surface.</summary>
    internal TeamsISO.App.Services.OscBridge? OscBridge => _oscBridge;

    /// <summary>Tray-icon host. Exposed so the settings VM can flip the minimize-to-tray toggle.</summary>
    internal TeamsISO.App.Services.TrayIconHost? TrayIcon => _trayIcon;

    [DllImport("user32.dll")]
    private static extern uint RegisterWindowMessageW(string lpString);
    [DllImport("user32.dll")]
    private static extern int PostMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    [DllImport("user32.dll")]
    private static extern int SendNotifyMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    private const IntPtr HWND_BROADCAST = -1;

    protected override async void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        // Crash diagnostics — wire the three exception channels WPF leaves open by
        // default to a single handler that logs Fatal to Serilog (which has the
        // rolling-daily file sink at %LOCALAPPDATA%\TeamsISO\Logs) and then shows
        // the user a dialog with the log path so they can attach it to a bug
        // report. We deliberately don't catch StackOverflowException or
        // ExecutionEngineException — both are uncatchable in modern .NET; if one
        // fires the OS Watson dialog will take it from here.
        AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandled;
        DispatcherUnhandledException += OnDispatcherUnhandled;
        System.Threading.Tasks.TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;

        // Single-instance gate: if another TeamsISO is already running for this user,
        // broadcast the bring-to-front message and exit silently. This prevents the
        // NDI/config contention seen during testing where two finders, two senders
        // with the same default name, and two writers to config.json all raced.
        bool createdNew;
        _singleInstanceMutex = new System.Threading.Mutex(initiallyOwned: true, SingleInstanceMutexName, out createdNew);
        _ownsSingleInstanceMutex = createdNew;
        if (!createdNew)
        {
            var bringToFront = RegisterWindowMessageW("WildDragon.TeamsISO.BringToFront");
            if (bringToFront != 0)
                SendNotifyMessageW(HWND_BROADCAST, bringToFront, IntPtr.Zero, IntPtr.Zero);
            Shutdown(0);
            return;
        }

        // Listen for the broadcast — if a *new* instance launches and finds us already
        // running, it'll send this message; we surface our window in response. Hold the
        // delegate in a field so OnExit can unsubscribe cleanly even though the
        // AppDomain teardown would also drop it.
        var bringToFrontMsg = RegisterWindowMessageW("WildDragon.TeamsISO.BringToFront");
        _bringToFrontHandler = (ref System.Windows.Interop.MSG msg, ref bool handled) =>
        {
            if (msg.message == (int)bringToFrontMsg && MainWindow is not null)
            {
                if (MainWindow.WindowState == WindowState.Minimized) MainWindow.WindowState = WindowState.Normal;
                MainWindow.Activate();
                MainWindow.Topmost = true;
                MainWindow.Topmost = false;
                handled = true;
            }
        };
        ComponentDispatcher.ThreadFilterMessage += _bringToFrontHandler;

        try
        {
            // WPF host: write to both console (visible if attached) and a rolling daily
            // file under %LOCALAPPDATA%\TeamsISO\Logs so users have something to grab when
            // they file an issue.
            _loggerFactory = EngineLogging.CreateDefault(LogLevel.Information);
            var logger = _loggerFactory.CreateLogger<App>();
            logger.LogInformation(
                "TeamsISO.App starting up. Build: {Version}. Process: {Pid}.",
                typeof(App).Assembly.GetName().Version,
                Environment.ProcessId);

            // ---- Preflight: NDI runtime ----
            try
            {
                _interop = new NdiInteropPInvoke(_loggerFactory.CreateLogger<NdiInteropPInvoke>());
            }
            catch (Exception ex)
            {
                MessageBox.Show(
                    "TeamsISO could not initialize the NDI runtime.\n\n" +
                    "Install the NDI Runtime from https://ndi.video/tools/ and try again.\n\n" +
                    "Details: " + ex.Message,
                    "TeamsISO — NDI runtime missing",
                    MessageBoxButton.OK,
                    MessageBoxImage.Error);
                Shutdown(2);
                return;
            }

            // ---- Engine wiring ----
            var configPath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                "TeamsISO", "config.json");
            var configStore = new ConfigStore(configPath, _loggerFactory.CreateLogger<ConfigStore>());

            var probe = new NdiRuntimeProbe(_interop, NdiVersion.ExpectedRuntimeVersionPrefix);
            var scaler = new ManagedNearestNeighborFrameScaler();

            var loggerFactoryRef = _loggerFactory;
            var interopRef = _interop;
            IsoPipeline PipelineFactory(IsoPipelineConfig config)
            {
                var clock = new PeriodicTimerFrameClock(config.Settings.FramerateHz);
                return new IsoPipeline(
                    config, interopRef, scaler, clock,
                    ExponentialBackoff.Default,
                    (delay, ct) => Task.Delay(delay, ct),
                    loggerFactoryRef);
            }

            _controller = new IsoController(
                _interop, PipelineFactory, configStore, probe, _loggerFactory);

            _viewModel = new MainViewModel(_controller, Dispatcher);
            var window = new MainWindow(_viewModel);
            window.Show();
            MainWindow = window;

            // REST control surface for Stream Deck / Companion. Off by default —
            // operators turn it on via the DISPLAY tab. When the toggle flips,
            // GlobalSettingsViewModel reaches into App.Current to start/stop it.
            _controlSurface = new TeamsISO.App.Services.ControlSurfaceServer(
                _controller,
                () => _viewModel,
                _loggerFactory.CreateLogger<TeamsISO.App.Services.ControlSurfaceServer>());
            _oscBridge = new TeamsISO.App.Services.OscBridge(
                _controller,
                () => _viewModel,
                _loggerFactory.CreateLogger<TeamsISO.App.Services.OscBridge>());

            // Disk space watcher: polls the recording drive every 5s while
            // recording is on. Auto-disables recording at <1GB free so an
            // unattended long show doesn't crash the host on disk-full.
            _diskSpaceWatcher = new TeamsISO.App.Services.DiskSpaceWatcher(
                _controller, _viewModel.Toast, Dispatcher);

            // Tray icon host. Disabled by default; the settings VM flips
            // Enabled when the operator toggles the DISPLAY checkbox. Hosting
            // it from App ensures the icon's lifetime matches the process,
            // not the main window (which gets hidden during minimize-to-tray).
            _trayIcon = new TeamsISO.App.Services.TrayIconHost(window)
            {
                Enabled = _viewModel.Settings.MinimizeToTray,
            };

            // First-launch onboarding. The dialog explains the once-per-machine
            // setup (NDI runtime, Teams admin permission, transcoder topology)
            // that the UI alone can't communicate clearly. Suppressed after the
            // user dismisses it with the checkbox checked. We show it AFTER the
            // main window so the dialog has a sensible Owner for centering and
            // z-order.
            if (OnboardingWindow.ShouldShow())
            {
                try
                {
                    var onboarding = new OnboardingWindow { Owner = window };
                    onboarding.ShowDialog();
                }
                catch
                {
                    // Defensive: an onboarding-dialog failure should never block startup.
                }
            }

            // Parse CLI args BEFORE InitializeAsync so any --apply-preset request
            // overrides the persisted auto-apply preference cleanly.
            ApplyCommandLineArgs(e.Args);

            await _viewModel.InitializeAsync(CancellationToken.None);

            // Auto-launch Teams in the background if the operator has opted in.
            // Combined with AutoHideTeamsWindows this gives the "I only see
            // TeamsISO" experience — Teams runs but never appears on screen,
            // and all interaction routes through the IN-CALL bar + participants
            // DataGrid. Fire-and-forget so a slow Teams launch doesn't delay
            // TeamsISO's window from appearing.
            if (_viewModel.Settings.LaunchTeamsOnStartup && !Services.TeamsLauncher.IsRunning())
            {
                _ = Task.Run(() =>
                {
                    try
                    {
                        if (Services.TeamsLauncher.TryLaunch(out var launchError))
                        {
                            if (_viewModel.Settings.AutoHideTeamsWindows)
                                _ = Services.TeamsLauncher.AutoHideAfterLaunchAsync();
                        }
                        else
                        {
                            logger.LogWarning("Auto-launch Teams on startup failed: {Error}", launchError);
                        }
                    }
                    catch (Exception ex)
                    {
                        logger.LogWarning(ex, "Auto-launch Teams on startup threw");
                    }
                });
            }
            else if (_viewModel.Settings.AutoHideTeamsWindows && Services.TeamsLauncher.IsRunning())
            {
                // Teams is already up from a previous session. If auto-hide is
                // on, hide it now so the operator's "I only see TeamsISO" rule
                // applies even when Teams was launched externally.
                _ = Services.TeamsLauncher.AutoHideAfterLaunchAsync();
            }

            // Background update check, throttled to once per 24h. Fire-and-forget
            // so a slow / offline update server never delays startup. Surfaces a
            // banner via UpdateBanner if newer; failures just log.
            if (Services.UpdateChecker.LaunchCheckEnabled)
            {
                _ = Task.Run(async () =>
                {
                    try
                    {
                        var result = await Services.UpdateChecker.CheckIfDueAsync(TimeSpan.FromHours(24));
                        if (result?.Status == Services.UpdateChecker.UpdateStatus.UpdateAvailable
                            && !string.IsNullOrEmpty(result.LatestTag)
                            && !string.IsNullOrEmpty(result.CurrentVersion))
                        {
                            await Dispatcher.InvokeAsync(() =>
                                _viewModel.UpdateBanner.Show(result.CurrentVersion!, result.LatestTag!));
                        }
                    }
                    catch (Exception ex)
                    {
                        logger.LogDebug(ex, "Background update check failed");
                    }
                });
            }
        }
        catch (Exception ex)
        {
            MessageBox.Show(
                "TeamsISO failed to start.\n\nDetails: " + ex,
                "TeamsISO — startup error",
                MessageBoxButton.OK,
                MessageBoxImage.Error);
            Shutdown(1);
        }
    }

    /// <summary>
    /// Where the rolling Serilog file sink writes. Reused by the crash dialog so we
    /// can show the user the exact directory to attach when filing a bug.
    /// </summary>
    private static string LogDirectory =>
        Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "TeamsISO", "Logs");

    /// <summary>
    /// Parse the supported CLI flags. Currently:
    ///   <c>--apply-preset NAME</c> — apply the named preset once participants
    ///   populate. Equivalent to running TeamsISO and clicking Presets → select →
    ///   Apply, but driven from a desktop shortcut.
    /// Unrecognized flags are silently ignored — operators using shortcut.lnk
    /// files don't need to fight argument parsers.
    /// </summary>
    private void ApplyCommandLineArgs(string[] args)
    {
        if (_viewModel is null) return;
        for (var i = 0; i < args.Length; i++)
        {
            switch (args[i])
            {
                case "--apply-preset":
                    if (i + 1 < args.Length && !string.IsNullOrEmpty(args[i + 1]))
                    {
                        _viewModel.RequestApplyPresetOnStartup(args[i + 1]);
                        i++; // consume the value
                    }
                    break;
            }
        }
    }

    private void OnAppDomainUnhandled(object sender, UnhandledExceptionEventArgs e)
    {
        // IsTerminating is almost always true here — finalizers and managed-thread
        // top-frames don't have a graceful path back. Log + show a dialog inline
        // since the process will exit either way.
        var ex = e.ExceptionObject as Exception;
        TryLogFatal("AppDomain.UnhandledException", ex);
        TryShowCrashDialog(ex, terminating: e.IsTerminating);
    }

    private void OnDispatcherUnhandled(object sender, DispatcherUnhandledExceptionEventArgs e)
    {
        TryLogFatal("Dispatcher.UnhandledException", e.Exception);
        TryShowCrashDialog(e.Exception, terminating: false);
        // Mark Handled so a single bad UI thunk doesn't take the whole app down —
        // the user has the dialog and the log; they can choose to keep going.
        e.Handled = true;
    }

    private void OnUnobservedTaskException(object? sender, System.Threading.Tasks.UnobservedTaskExceptionEventArgs e)
    {
        TryLogFatal("TaskScheduler.UnobservedTaskException", e.Exception);
        // Don't show a dialog here — these fire from the finalizer thread and
        // tend to be cleanup-time noise, not user-actionable. Log only.
        e.SetObserved();
    }

    private void TryLogFatal(string source, Exception? ex)
    {
        try
        {
            var logger = _loggerFactory?.CreateLogger<App>();
            logger?.LogCritical(ex, "{Source} fired", source);
        }
        catch
        {
            // Logger itself failed (rare — disk full, permission denied). Swallow:
            // there's nothing useful we can do, and re-throwing during crash
            // handling makes things worse.
        }
    }

    private static void TryShowCrashDialog(Exception? ex, bool terminating)
    {
        try
        {
            var heading = terminating
                ? "TeamsISO encountered an unrecoverable error and will exit."
                : "TeamsISO encountered an error.";
            var details = ex?.GetType().Name + ": " + (ex?.Message ?? "(no details)");
            var body =
                heading + "\n\n" +
                details + "\n\n" +
                $"A full diagnostic log has been written to:\n{LogDirectory}\n\n" +
                "Attach the most recent file from that directory to your bug report.";
            MessageBox.Show(body, "TeamsISO — Error",
                MessageBoxButton.OK, MessageBoxImage.Error);
        }
        catch
        {
            // Even the dialog failed (e.g., during shutdown when the message pump
            // is already gone). Nothing more to do.
        }
    }

    protected override async void OnExit(ExitEventArgs e)
    {
        try
        {
            _trayIcon?.Dispose();
            _diskSpaceWatcher?.Dispose();
            if (_controlSurface is not null)
                await _controlSurface.DisposeAsync();
            if (_oscBridge is not null)
                await _oscBridge.DisposeAsync();
            _viewModel?.Dispose();
            if (_controller is not null)
                await _controller.DisposeAsync();
            _interop?.Dispose();
            _loggerFactory?.Dispose();
        }
        catch
        {
            // Best-effort shutdown
        }
        finally
        {
            // Unsubscribe the bring-to-front filter so the delegate doesn't outlive
            // the App; ComponentDispatcher is process-static.
            if (_bringToFrontHandler is not null)
            {
                ComponentDispatcher.ThreadFilterMessage -= _bringToFrontHandler;
                _bringToFrontHandler = null;
            }
            // Release the Mutex iff we acquired it. The "lost the race" path above
            // sets _ownsSingleInstanceMutex=false and we skip ReleaseMutex (which
            // would throw ApplicationException on an unowned Mutex).
            try { if (_ownsSingleInstanceMutex) _singleInstanceMutex?.ReleaseMutex(); }
            catch { /* defensive: already-released or invalid handle */ }
            _singleInstanceMutex?.Dispose();
        }
        base.OnExit(e);
    }
}
