using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Globalization;
using System.IO;
using System.Windows.Forms;
using AutoIngest.Core;
using AutoIngest.Engine;

namespace AutoIngest.App
{
    /// <summary>
    /// Tray-resident photo importer. The window is a small borderless toast that stays
    /// hidden by default and only pops into the bottom-right corner of the screen while
    /// an SD/USB import is running (and for a few seconds after it finishes).
    ///
    /// Static UI lives in MainForm.Designer.cs; this file holds the toast show/hide
    /// logic, tray icon, monitor wiring, and config.
    /// </summary>
    public partial class MainForm : Form
    {
        #region theme
        // Aliases over the shared Theme palette so this file keeps its existing CLR_*
        // references while SettingsForm and any future UI use Theme.* directly.
        static readonly Color CLR_BG       = Theme.BG;
        static readonly Color CLR_BG2      = Theme.BG2;
        static readonly Color CLR_TEXT     = Theme.TEXT;
        static readonly Color CLR_TEXT_DIM = Theme.TEXT_DIM;
        static readonly Color CLR_ACCENT   = Theme.ACCENT;
        static readonly Color CLR_GREEN    = Theme.GREEN;
        static readonly Color CLR_RED      = Theme.RED;
        #endregion

        #region fields
        ImportCoordinator? _coordinator;
        DeviceRegistry? _registry;
        readonly AutoIngest.Engine.ImageConverter _converter = null!;
        AppConfig _config = null!;
        bool _firstLaunch;

        // Runtime-only UI (not Designer-serialized).
        NotifyIcon trayIcon = null!;
        ContextMenuStrip trayMenu = null!;
        Bitmap? _trayBitmap;

        /// <summary>Auto-hides the toast a few seconds after the last activity.</summary>
        System.Windows.Forms.Timer? _hideTimer;

        /// <summary>True while an import is actively running — keeps the toast pinned open.</summary>
        bool _importing;

        const int HIDE_DELAY_MS = 4000;
        const string APP_NAME = "AutoIngest";
        #endregion

        public MainForm()
        {
            InitializeComponent();

            // Let the WinForms Designer instantiate the form without running
            // runtime-only initialization (config load, monitor, file I/O, etc.).
            if (LicenseManager.UsageMode == LicenseUsageMode.Designtime)
                return;

            _converter = new AutoIngest.Engine.ImageConverter();
            // Snapshot whether this is a brand-new install BEFORE LoadConfig (which is a read and
            // doesn't create the file, but capturing explicitly is clearer and future-proof).
            _firstLaunch = !ConfigManager.ConfigExists;
            _config = ConfigManager.LoadConfig();

            _hideTimer = new System.Windows.Forms.Timer { Interval = HIDE_DELAY_MS };
            _hideTimer.Tick += (_, _) =>
            {
                _hideTimer.Stop();
                if (!_importing) Hide();
            };

            BuildTrayIcon();
            WireMonitorEvents();

            Shown += (_, _) =>
            {
                // The very first Shown fires after Application.Run creates the form.
                // We want to start hidden, so immediately hide (the form never had to
                // be visible in the first place — but WinForms shows it once on Run).
                Hide();

                // First-ever install: run the welcome/branding wizard once before starting the
                // monitor. Existing users (config already on disk) skip straight through.
                if (_firstLaunch)
                {
                    try
                    {
                        using var wiz = new WelcomeForm(_config);
                        wiz.ShowDialog(this);
                        // Branding may have changed during the wizard — refresh the tray icon so
                        // it shows the user's mark immediately rather than on next launch.
                        RefreshTrayIcon();
                    }
                    catch (Exception ex)
                    {
                        // Never let a wizard failure block startup — log and continue with defaults.
                        Log($"Setup wizard skipped: {ex.Message}", CLR_RED);
                        _config.SetupCompleted = true;
                        ConfigManager.SaveConfig(_config);
                    }
                }

                StartMonitor();
            };

            FormClosing += MainForm_FormClosing;

            // Start hidden — tray only.
            Visible = false;
            WindowState = FormWindowState.Minimized;
        }

        #region startup / monitor
        void StartMonitor()
        {
            _registry = new DeviceRegistry(_config);

            var importer = new DeviceImporter(_converter, _config);
            var sources = new List<IDeviceSource>
            {
                new SDCardMonitor(),
                new MtpDeviceSource(() => _config.DeleteFromPhoneAfterImport)
            };

            _coordinator = new ImportCoordinator(importer, _registry, _config, sources);
            WireRuntimeMonitorEvents();
            _coordinator.ResolveUnknownDevice += ResolveUnknownDevice;

            if (_config.AutoImport)
                _coordinator.Start();

            Log("AutoIngest ready.", CLR_ACCENT);
            if (_registry!.All.Count > 0)
                Log($"{_registry.All.Count} device(s) registered. Insert one to begin.", CLR_TEXT_DIM);
            else
                Log("No devices registered yet. Insert a device to approve it.", CLR_TEXT_DIM);
        }

        /// <summary>
        /// Called on the coordinator thread when an unregistered device is connected. We marshal
        /// to the UI thread and ask the user: register + import, import once, or ignore. If the
        /// prompt is disabled in settings, the coordinator never calls this — we silently skip.
        /// </summary>
        UnknownDeviceResolution ResolveUnknownDevice(DeviceIdentity device)
        {
            if (InvokeRequired)
            {
                return (UnknownDeviceResolution)Invoke(new Func<DeviceIdentity, UnknownDeviceResolution>(ResolveUnknownDevice), device);
            }

            // Show a brief toast so the user sees something happened even if they're not looking
            // at a dialog, then prompt.
            lblStatus.Text = $"\u25CF  New device: {device.DisplayName}";
            lblStatus.ForeColor = CLR_ACCENT;
            ShowToast();
            ArmAutoHide();

            string kind = device.Kind.Contains("weak", StringComparison.OrdinalIgnoreCase)
                ? $"{device.Kind} — id may be unreliable"
                : device.Kind;

            var result = MessageBox.Show(this,
                $"A new device was connected:\n\n" +
                $"  {device.DisplayName}\n" +
                $"  Type: {kind}\n\n" +
                "Register it so it imports automatically from now on?\n\n" +
                "  Yes        = Register & import\n" +
                "  No         = Import this once only\n" +
                "  Cancel     = Ignore this session",
                "New device — AutoIngest",
                MessageBoxButtons.YesNoCancel,
                MessageBoxIcon.Question,
                MessageBoxDefaultButton.Button1);

            return result switch
            {
                DialogResult.Yes => UnknownDeviceResolution.RegisterAndImport,
                DialogResult.No => UnknownDeviceResolution.ImportOnce,
                _ => UnknownDeviceResolution.Ignore,
            };
        }
        #endregion

        #region toast show / hide
        const int TOAST_MARGIN = 12;

        /// <summary>
        /// Positions the toast at the bottom-right of the working area (just above the
        /// taskbar, clear of the screen edges on every monitor / DPI scale) and shows it.
        /// Uses SetBounds so x/y and the rendered size are applied atomically, then clamps
        /// inside the working area so the form is always fully on-screen.
        /// </summary>
        void ShowToast()
        {
            // Make sure the form has a real size before we measure it. On the first show
            // the layout may not have run yet, so force it.
            if (Width == 0 || Height == 0)
            {
                PerformLayout();
                var pref = PreferredSize;
                if (Width == 0) Width = pref.Width;
                if (Height == 0) Height = pref.Height;
            }

            // GetWorkingArea(this) resolves the actual screen the form is on and accounts
            // for the taskbar; fall back to the primary screen's working area.
            var area = Screen.GetWorkingArea(this);
            if (area.IsEmpty) area = Screen.PrimaryScreen?.WorkingArea ?? SystemInformation.WorkingArea;

            int w = Width;
            int h = Height;

            // Target the bottom-right corner with a margin, then clamp into the working
            // area so rounding / DPI scaling can never push part of it off-screen.
            int x = area.Right - w - TOAST_MARGIN;
            int y = area.Bottom - h - TOAST_MARGIN;
            if (x < area.Left + TOAST_MARGIN) x = area.Left + TOAST_MARGIN;
            if (y < area.Top + TOAST_MARGIN) y = area.Top + TOAST_MARGIN;
            if (x + w > area.Right - TOAST_MARGIN) x = area.Right - w - TOAST_MARGIN;
            if (y + h > area.Bottom - TOAST_MARGIN) y = area.Bottom - h - TOAST_MARGIN;

            _hideTimer?.Stop();
            if (!Visible)
            {
                // SetBounds applies position + size atomically before the window is shown,
                // so it appears exactly where we want it with no flicker or repositioning.
                SetBounds(x, y, w, h);
                Show();
                WindowState = FormWindowState.Normal;
            }
            else
            {
                SetBounds(x, y, w, h);
            }
        }

        /// <summary>
        /// Arm the auto-hide timer. Called after any activity; if no further activity
        /// arrives within HIDE_DELAY_MS the toast hides itself (unless an import is
        /// actively running).
        /// </summary>
        void ArmAutoHide()
        {
            _hideTimer?.Stop();
            _hideTimer?.Start();
        }
        #endregion

        #region monitor events
        // Wired at design time (cheap — just lambda closures).
        void WireMonitorEvents() { }

        // Wired once the coordinator exists (after StartMonitor creates it). These passthrough
        // events come from DeviceImporter via ImportCoordinator — same shape as the old monitor.
        void WireRuntimeMonitorEvents()
        {
            var monitor = _coordinator!;

            monitor.DriveFound += (driveId, label) =>
            {
                _importing = true;
                SafeInvoke(() =>
                {
                    lblStatus.Text = $"\u25CF  Reading {label}";
                    lblStatus.ForeColor = CLR_ACCENT;
                    ShowToast();
                });
                Log($"Drive found: {label}", CLR_ACCENT);
            };

            monitor.ImportProgress += (current, total, filename) =>
            {
                SafeInvoke(() =>
                {
                    lblStatus.Text = $"\u25CF  {current}/{total}: {filename}";
                    ShowToast();
                });
            };

            monitor.ImportDone += (moved, converted, sizeStr, destFolder) =>
            {
                _importing = false;
                SafeInvoke(() =>
                {
                    lblStatus.Text = $"\u2713  Done \u2014 {moved} moved, {converted} converted ({sizeStr})";
                    lblStatus.ForeColor = CLR_GREEN;
                    ShowToast();
                    ArmAutoHide();
                });
                Log($"Import complete: {moved} moved, {converted} converted, {sizeStr} total -> {destFolder}", CLR_GREEN);
            };

            monitor.ImportError += msg =>
            {
                // An error terminates the active import from the UI's perspective — without this
                // the toast would stay pinned open (the hide timer checks _importing).
                _importing = false;
                SafeInvoke(() =>
                {
                    ShowToast();
                    ArmAutoHide();
                });
                Log($"Error: {msg}", CLR_RED);
            };

            monitor.LogMessage += (msg, color) =>
            {
                var c = ParseHexColor(color) ?? CLR_TEXT;
                Log(msg, c);
            };
        }

        /// <summary>
        /// Marshals an action to the UI thread, but safely: if the form (or its handle) is gone —
        /// which can happen during shutdown while a coordinator/retention background callback is
        /// still in flight — the call is dropped rather than throwing ObjectDisposedException /
        /// InvalidOperationException up the background thread and crashing. Also guards against
        /// the rare InvokeRequired-on-no-handle case.
        /// </summary>
        void SafeInvoke(Action action)
        {
            if (IsDisposed || !IsHandleCreated) return;
            try
            {
                if (InvokeRequired) Invoke(action);
                else action();
            }
            catch (ObjectDisposedException) { /* control gone */ }
            catch (InvalidOperationException) { /* form disposing/disposed */ }
        }
        #endregion

        #region log
        void Log(string msg, Color color)
        {
            string line = $"[{DateTime.Now:HH:mm:ss}] {msg}\n";
            // Mirror to the persistent file log so a "photos disappeared" report is diagnosable
            // after the toast log has vanished on restart.
            FileLogger.Log(msg);
            if (txtLog.InvokeRequired)
                txtLog.Invoke(new Action(() => AppendLog(line, color)));
            else
                AppendLog(line, color);
        }

        void AppendLog(string text, Color color)
        {
            txtLog.SelectionStart = txtLog.TextLength;
            txtLog.SelectionColor = color;
            txtLog.AppendText(text);
            txtLog.SelectionColor = CLR_TEXT;
            txtLog.ScrollToCaret();
        }

        static Color? ParseHexColor(string hex)
        {
            if (hex.Length < 7 || hex[0] != '#') return null;
            try
            {
                int r = int.Parse(hex[1..3], NumberStyles.HexNumber);
                int g = int.Parse(hex[3..5], NumberStyles.HexNumber);
                int b = int.Parse(hex[5..7], NumberStyles.HexNumber);
                return Color.FromArgb(r, g, b);
            }
            catch (FormatException) { return null; }
            catch (OverflowException) { return null; }
        }
        #endregion

        #region tray icon
        void BuildTrayIcon()
        {
            trayMenu = new ContextMenuStrip
            {
                BackColor = CLR_BG2,
                ForeColor = CLR_TEXT,
                Renderer = new DarkMenuRenderer()
            };

            var showItem = new ToolStripMenuItem("Show status");
            showItem.Click += (_, _) => { ShowToast(); };
            var folderItem = new ToolStripMenuItem("Open import folder");
            folderItem.Click += (_, _) => OpenImportFolder();
            var devicesItem = new ToolStripMenuItem("Settings…");
            devicesItem.Click += (_, _) => OpenSettings();
            var resetItem = new ToolStripMenuItem("Reset drive memory");
            resetItem.Click += (_, _) =>
            {
                _coordinator?.ResetMemory();
                Log("Drive memory cleared. All devices will be re-scanned.", CLR_ACCENT);
            };

            // Options submenu: feature toggles. Each is a checked item bound to a config field,
            // saved immediately on flip, and read live by the importer so the change applies to
            // the very next import without a restart. New one-off toggles slot in here.
            var optionsItem = new ToolStripMenuItem("Options");

            var autoOrientItem = new ToolStripMenuItem("Auto-orient (EXIF)")
            {
                Checked = _config.AutoOrient,
                CheckOnClick = true
            };
            autoOrientItem.Click += (_, _) =>
            {
                _config.AutoOrient = autoOrientItem.Checked;
                ConfigManager.SaveConfig(_config);
                Log($"Auto-orient {(autoOrientItem.Checked ? "enabled" : "disabled")} — applies to next import.",
                    CLR_ACCENT);
            };

            var stripExifItem = new ToolStripMenuItem("Strip metadata (EXIF/GPS)")
            {
                Checked = _config.StripExif,
                CheckOnClick = true
            };
            stripExifItem.Click += (_, _) =>
            {
                _config.StripExif = stripExifItem.Checked;
                ConfigManager.SaveConfig(_config);
                Log($"Metadata stripping {(stripExifItem.Checked ? "enabled" : "disabled")} — applies to next import.",
                    CLR_ACCENT);
            };

            // Delete-from-phone toggle: phones only. SD/USB always moved/converted (their originals
            // always go). Phones default to copy-and-leave so the user curates phone storage; this
            // makes a phone behave like a card — original deleted after the local copy verifies.
            // Turning ON requires explicit confirmation because phone-side deletion is irreversible
            // (there is no Recycle Bin over MTP). Decline → revert the checkbox, leave config off.
            var phoneDeleteItem = new ToolStripMenuItem("Delete from phone after import")
            {
                Checked = _config.DeleteFromPhoneAfterImport,
                CheckOnClick = true
            };
            phoneDeleteItem.Click += (_, _) =>
            {
                if (phoneDeleteItem.Checked && !_config.DeleteFromPhoneAfterImport)
                {
                    var confirm = MessageBox.Show(this,
                        "Delete from phone after import?\n\n" +
                        "When this is on, a phone's photo originals are PERMANENTLY removed after the " +
                        "local copy is verified. Phones have no Recycle Bin over MTP — this cannot be undone.\n\n" +
                        "SD/USB cards are unaffected (they always moved).\n\n" +
                        "Enable anyway?",
                        "Delete from phone after import",
                        MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
                    if (confirm != DialogResult.Yes)
                    {
                        phoneDeleteItem.Checked = false;
                        return;
                    }
                }
                _config.DeleteFromPhoneAfterImport = phoneDeleteItem.Checked;
                ConfigManager.SaveConfig(_config);
                Log($"Phone delete {(phoneDeleteItem.Checked ? "enabled" : "disabled")} — applies to next phone import. SD/USB unaffected.",
                    CLR_ACCENT);
            };

            // Branding submenu: a master toggle plus an editor entry. Branding covers text/logo
            // watermarks + EXIF copyright, which are too dense for bare toggles, so the editor
            // opens the BrandingForm. The toggle reflects the master switch (Branding.Enabled),
            // which is on when any branding feature is configured.
            var brandingItem = new ToolStripMenuItem("Branding");
            var brandingToggle = new ToolStripMenuItem("Enabled")
            {
                Checked = _config.Branding.Enabled,
                CheckOnClick = true
            };
            brandingToggle.Click += (_, _) =>
            {
                _config.Branding.Enabled = brandingToggle.Checked;
                ConfigManager.SaveConfig(_config);
                Log($"Branding {(brandingToggle.Checked ? "enabled" : "disabled")} — applies to next import.",
                    CLR_ACCENT);
                // Toggling branding on/off swaps between the brand mark and the green circle.
                RefreshTrayIcon();
            };
            var brandingEdit = new ToolStripMenuItem("Edit branding…");
            brandingEdit.Click += (_, _) =>
            {
                _hideTimer?.Stop();
                using var f = new BrandingForm(_config);
                f.ShowDialog(this);
                // Reflect the (possibly changed) master switch back in the menu, and refresh the
                // tray icon so it picks up the user's new/changed mark immediately.
                brandingToggle.Checked = _config.Branding.Enabled;
                RefreshTrayIcon();
            };
            brandingItem.DropDownItems.AddRange(new ToolStripItem[] { brandingToggle, new ToolStripSeparator(), brandingEdit });

            // Start-with-Windows toggle: copies the exe to %LocalAppData%\Programs\AutoIngest\ and
            // drops a .lnk in the user's Startup folder (the classic autorun mechanism — works on
            // Win11 unchanged). The shortcut's existence is the source of truth, so we refresh the
            // checkbox each time the menu opens in case it changed externally.
            var autostartItem = new ToolStripMenuItem("Start with Windows")
            {
                Checked = AutostartManager.IsEnabled,
                CheckOnClick = false // we control Checked explicitly after the action
            };
            autostartItem.Click += (_, _) =>
            {
                if (autostartItem.Checked)
                {
                    // Currently enabled → turn off. Non-destructive: removes the shortcut, leaves
                    // the installed exe in place so re-enabling doesn't re-copy.
                    try
                    {
                        AutostartManager.Disable();
                        autostartItem.Checked = false;
                        Log("Removed from Startup. AutoIngest won't start at login.",
                            CLR_ACCENT);
                    }
                    catch (Exception ex)
                    {
                        ShowToast();
                        Log($"Couldn't disable autostart: {ex.Message}", CLR_RED);
                    }
                }
                else
                {
                    // Currently off → turn on: install + create shortcut. Takes effect next login.
                    try
                    {
                        bool copied = AutostartManager.Enable();
                        autostartItem.Checked = true;
                        Log(copied
                            ? $"Installed to {AutostartManager.StableHome} and added to Startup. Starts at next login."
                            : "Added to Startup (already installed). Starts at next login.",
                            CLR_GREEN);
                    }
                    catch (Exception ex)
                    {
                        ShowToast();
                        Log($"Couldn't enable autostart: {ex.Message}", CLR_RED);
                        MessageBox.Show(this,
                            "Couldn't set up autostart:\n\n" + ex.Message +
                            "\n\nThis usually means antivirus blocked the Startup shortcut, or the " +
                            "destination folder isn't writable.",
                            "Autostart", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                        // Revert: read true state in case a partial change happened.
                        autostartItem.Checked = AutostartManager.IsEnabled;
                    }
                }
            };

            optionsItem.DropDownItems.AddRange(new ToolStripItem[] { autoOrientItem, stripExifItem, phoneDeleteItem, brandingItem, autostartItem });

            // Refresh filesystem-derived checkboxes every time the menu opens, so the autostart
            // toggle stays accurate if it was changed externally (e.g. the user deleted the link).
            trayMenu.Opened += (_, _) =>
            {
                autostartItem.Checked = AutostartManager.IsEnabled;
            };

            var aboutItem = new ToolStripMenuItem("About");
            aboutItem.Click += (_, _) =>
                MessageBox.Show(
                    APP_NAME + "\n" +
                    "Camera photo importer for ecommerce workflows\n\n" +
                    "Author: Jeremy Anderson\n" +
                    "Email:  info@dcos.net\n" +
                    "Web:    https://dcos.net\n\n" +
                    "C# / .NET 8 / Magick.NET\n\n" +
                    "Licensed under the MIT License.\n" +
                    "See https://opensource.org/license/MIT/",
                    "About " + APP_NAME, MessageBoxButtons.OK, MessageBoxIcon.Information);
            var exitItem = new ToolStripMenuItem("Exit");
            exitItem.Click += (_, _) => ExitApplication();

            trayMenu.Items.AddRange(new ToolStripItem[]
            {
                showItem, folderItem, devicesItem, resetItem, optionsItem,
                new ToolStripSeparator(), aboutItem,
                new ToolStripSeparator(), exitItem
            });

            // Tray icon is a micro version of the user's branding (logo or store initial) on an
            // auto-contrasting tile. Falls back to the green circle when branding is off. Held in
            // _trayBitmap for the NotifyIcon handle's lifetime; RefreshTrayIcon rebuilds it when
            // branding changes.
            _trayBitmap = TrayIconFactory.Build(_config.Branding, _config);

            trayIcon = new NotifyIcon
            {
                Text = APP_NAME,
                Visible = true,
                ContextMenuStrip = trayMenu
            };
            SetTrayIcon(_trayBitmap);
            // Left-click toggles the toast so the user can peek at status on demand.
            trayIcon.MouseClick += (_, e) =>
            {
                if (e.Button == MouseButtons.Left)
                {
                    if (Visible) Hide();
                    else { ShowToast(); ArmAutoHide(); }
                }
            };
            trayIcon.DoubleClick += (_, _) => ShowToast();
        }

        /// <summary>
        /// Sets the tray icon from a bitmap, owning the GDI HICON lifetime. Bitmap.GetHicon()
        /// allocates an HICON the caller must free with DestroyIcon; Icon.FromHandle does NOT take
        /// ownership. We track the live handle and free the previous one on swap so repeated
        /// RefreshTrayIcon calls (branding changes) don't leak HICONs.
        /// </summary>
        IntPtr _currentIconHandle = IntPtr.Zero;
        void SetTrayIcon(Bitmap bmp)
        {
            if (trayIcon == null || bmp == null) return;
            IntPtr h = bmp.GetHicon();
            try { trayIcon.Icon = Icon.FromHandle(h); }
            catch { /* keep previous icon if the handle couldn't be made */ DestroyIcon(h); return; }
            // Free the previous handle now that the NotifyIcon has adopted the new one.
            if (_currentIconHandle != IntPtr.Zero) DestroyIcon(_currentIconHandle);
            _currentIconHandle = h;
        }

        /// <summary>
        /// Rebuilds the tray icon from the current branding and swaps it in. Call after anything
        /// that changes branding (welcome wizard, BrandingForm save) so the icon reflects the
        /// user's mark live. Safe to call repeatedly; frees the old HICON and disposes the old
        /// bitmap.
        /// </summary>
        void RefreshTrayIcon()
        {
            if (trayIcon == null) return;
            var fresh = TrayIconFactory.Build(_config.Branding, _config);
            var oldBitmap = _trayBitmap;
            _trayBitmap = fresh;
            SetTrayIcon(fresh);
            try { oldBitmap?.Dispose(); } catch { }
        }
        #endregion

        #region helpers
        static void OpenImportFolder()
        {
            string today = DateTime.Now.ToString("yyyy-MM-dd");
            string picturesFolder = Environment.GetFolderPath(Environment.SpecialFolder.MyPictures);
            string todayFolder = Path.Combine(picturesFolder, today);

            string target = Directory.Exists(todayFolder) ? todayFolder
                       : Directory.Exists(picturesFolder) ? picturesFolder
                       : "";

            if (target != "")
                Process.Start("explorer.exe", target);
            else
                MessageBox.Show("No Pictures folder found.", APP_NAME,
                    MessageBoxButtons.OK, MessageBoxIcon.Information);
        }

        void OpenSettings()
        {
            // Pause auto-hide so the toast doesn't fight the dialog for attention.
            _hideTimer?.Stop();
            using var form = new SettingsForm(
                _registry!,
                _config,
                () => _coordinator!.GetCurrentlyConnected(),
                () => _coordinator?.IsAccessTimeTrackingEnabled ?? false,
                () => _coordinator!.RunRetentionSweepNow());
            form.ShowDialog(this);
        }

        void ExitApplication()
        {
            // Free the live HICON before the NotifyIcon tears down.
            if (_currentIconHandle != IntPtr.Zero) { DestroyIcon(_currentIconHandle); _currentIconHandle = IntPtr.Zero; }
            trayIcon.Visible = false;
            _coordinator?.Stop();
            Application.Exit();
        }

        [System.Runtime.InteropServices.DllImport("user32.dll")]
        static extern bool DestroyIcon(IntPtr handle);
        #endregion

        #region form events
        // Closing the toast (alt-f4 / clicking it off) should never quit the app —
        // it just hides. The app only exits via the tray's Exit item.
        void MainForm_FormClosing(object? sender, FormClosingEventArgs e)
        {
            if (e.CloseReason == CloseReason.UserClosing ||
                e.CloseReason == CloseReason.FormOwnerClosing)
            {
                e.Cancel = true;
                Hide();
            }
            // Any other close reason (taskmgr, shutdown, Application.Exit) is allowed.
        }
        #endregion
    }

    #region dark menu renderer
    public class DarkMenuRenderer : ToolStripProfessionalRenderer
    {
        public DarkMenuRenderer() : base(new DarkColorTable()) { }
    }

    public class DarkColorTable : ProfessionalColorTable
    {
        public override Color MenuStripGradientBegin => Color.FromArgb(42, 42, 42);
        public override Color MenuStripGradientEnd => Color.FromArgb(42, 42, 42);
        public override Color MenuItemSelected => Color.FromArgb(88, 166, 255);
        public override Color MenuItemBorder => Color.FromArgb(68, 68, 68);
        public override Color MenuBorder => Color.FromArgb(68, 68, 68);
        public override Color SeparatorDark => Color.FromArgb(68, 68, 68);
        public override Color SeparatorLight => Color.FromArgb(68, 68, 68);
        public override Color ImageMarginGradientBegin => Color.FromArgb(42, 42, 42);
        public override Color ImageMarginGradientMiddle => Color.FromArgb(42, 42, 42);
        public override Color ImageMarginGradientEnd => Color.FromArgb(42, 42, 42);
        public override Color ToolStripDropDownBackground => Color.FromArgb(35, 35, 35);
        public override Color ButtonSelectedHighlight => Color.FromArgb(88, 166, 255);
        public override Color ButtonSelectedBorder => Color.FromArgb(88, 166, 255);
        public override Color CheckBackground => Color.FromArgb(42, 42, 42);
        public override Color CheckSelectedBackground => Color.FromArgb(88, 166, 255);
    }
    #endregion
}
