using System;
using System.Linq;
using System.Windows;
using Microsoft.Win32;

namespace DragonISO.App.Services;

/// <summary>
/// Owns the active theme for the WPF host. Three preferences:
/// <list type="bullet">
///   <item><c>System</c> â€” follows the Windows app-mode setting (default for new
///     users; re-reads on <see cref="SystemEvents.UserPreferenceChanged"/>).</item>
///   <item><c>Dark</c> â€” pin dark regardless of OS.</item>
///   <item><c>Light</c> â€” pin light regardless of OS.</item>
/// </list>
/// The two color files (<c>Theme.Dark.xaml</c> + <c>Theme.Light.xaml</c>) are
/// kept in lockstep on the same set of brush keys; this manager swaps the
/// MergedDictionaries entry at runtime. Styles + control templates in
/// <c>WildDragonTheme.xaml</c> reach the brushes via <see langword="DynamicResource"/>,
/// so the visual tree re-resolves without an app restart.
///
/// Preference is persisted via <see cref="UIPreferences"/>'s <c>Theme</c> field,
/// reserved on disk during the v1 â†’ v2 rollout so the rebuild doesn't lose the
/// operator's choice.
/// </summary>
public sealed class ThemeManager
{
    public static ThemeManager Current { get; } = new(
        isSystemDark: ReadSystemDarkFromRegistry,
        loadPreference: TryLoadPreferenceFromDisk,
        savePreference: TrySavePreferenceToDisk,
        subscribeToSystemPreference: true);

    // Pack URIs (rather than relative "/Themes/â€¦") so the resolution
    // works equally well from production (where Application.Current's
    // base URI is the Dragon-ISO entry assembly) and from xUnit tests
    // (where it's the test assembly â€” relative URIs would miss).
    private const string DarkUri = "pack://application:,,,/DragonISO;component/Themes/Theme.Dark.xaml";
    private const string LightUri = "pack://application:,,,/DragonISO;component/Themes/Theme.Light.xaml";
    private const string PreferenceKeySystem = "System";
    private const string PreferenceKeyDark = "Dark";
    private const string PreferenceKeyLight = "Light";

    // Test seams. The production singleton wires these to the real
    // registry / UIPreferences. Tests construct via the internal ctor
    // with their own stubs so they don't touch HKCU or %LOCALAPPDATA%.
    private readonly Func<bool> _isSystemDark;
    private readonly Action<string> _savePreference;

    internal ThemeManager(
        Func<bool> isSystemDark,
        Func<string?> loadPreference,
        Action<string> savePreference,
        bool subscribeToSystemPreference)
    {
        _isSystemDark = isSystemDark;
        _savePreference = savePreference;

        // Hydrate preference from the seam on first access. Disk / load
        // failures fall back to defaults so the app always boots into a
        // deterministic theme.
        try
        {
            var loaded = loadPreference();
            if (IsValidPreference(loaded))
            {
                _preference = loaded!;
            }
        }
        catch
        {
            // Defensive â€” ctor must not throw or the app loses theming.
        }

        // Re-evaluate when Windows app-mode flips, but only when the
        // operator hasn't pinned a preference. The explicit choice wins.
        // Tests opt out so they don't latch into a process-wide event.
        if (subscribeToSystemPreference)
        {
            SystemEvents.UserPreferenceChanged += OnSystemPreferenceChanged;
        }
    }

    private string _preference = PreferenceKeySystem;

    /// <summary>Current preference. One of "System", "Dark", "Light".</summary>
    public string Preference => _preference;

    /// <summary>Fires after a theme swap with the resolved (absolute) theme.</summary>
    public event EventHandler<string>? Themed;

    /// <summary>
    /// Resolve the preference to an absolute theme name ("Dark" or "Light")
    /// suitable for the dictionary lookup. "System" resolves to the OS
    /// app-mode at the time of the call.
    /// </summary>
    public string ResolveTheme() => _preference switch
    {
        PreferenceKeyDark => PreferenceKeyDark,
        PreferenceKeyLight => PreferenceKeyLight,
        _ => _isSystemDark() ? PreferenceKeyDark : PreferenceKeyLight,
    };

    /// <summary>
    /// Set the operator's preference, persist, and apply the resolved theme.
    /// </summary>
    public void Set(string preference)
    {
        if (!IsValidPreference(preference))
        {
            throw new ArgumentException(
                "Preference must be 'System', 'Dark', or 'Light'.",
                nameof(preference));
        }

        _preference = preference;
        try { _savePreference(preference); }
        catch { /* persistence is best-effort */ }
        Apply();
    }

    /// <summary>
    /// Cycle the theme between Dark and Light (one-click toggle from the header
    /// theme icon). If the current preference is "System", the cycle pins to
    /// the OPPOSITE of the currently-resolved theme so the click has a
    /// visible effect.
    /// </summary>
    public void Toggle()
    {
        var current = ResolveTheme();
        Set(current == PreferenceKeyDark ? PreferenceKeyLight : PreferenceKeyDark);
    }

    /// <summary>
    /// Apply the current resolved theme. Should be called once during app
    /// startup (after Application.Current.Resources is initialized) and
    /// whenever <see cref="Preference"/> changes â€” <see cref="Set"/> already
    /// does the latter for you.
    /// </summary>
    public void Apply()
    {
        var theme = ResolveTheme();
        var uri = theme == PreferenceKeyDark ? DarkUri : LightUri;
        SwapColorDictionary(uri);
        Themed?.Invoke(this, theme);
    }

    private static void SwapColorDictionary(string newUri)
    {
        var app = Application.Current;
        if (app is null) return;
        var dicts = app.Resources.MergedDictionaries;

        // Find the existing theme color dictionary by source URI. We
        // distinguish "color" dictionaries from "WildDragonTheme" by name â€”
        // the color files are at Theme.Dark.xaml / Theme.Light.xaml; the
        // styles file is at WildDragonTheme.xaml. Replace in place to
        // preserve merge order so DynamicResource refs resolve to the new
        // brushes.
        ResourceDictionary? old = null;
        for (var i = 0; i < dicts.Count; i++)
        {
            var src = dicts[i].Source?.OriginalString ?? string.Empty;
            if (src.EndsWith("Theme.Dark.xaml", StringComparison.OrdinalIgnoreCase) ||
                src.EndsWith("Theme.Light.xaml", StringComparison.OrdinalIgnoreCase))
            {
                old = dicts[i];
                break;
            }
        }

        var fresh = new ResourceDictionary { Source = new Uri(newUri, UriKind.Absolute) };
        if (old is null)
        {
            dicts.Insert(0, fresh);
        }
        else
        {
            var idx = dicts.IndexOf(old);
            dicts.RemoveAt(idx);
            dicts.Insert(idx, fresh);
        }
    }

    /// <summary>
    /// Read Windows' AppsUseLightTheme registry value. 1 = light, 0 = dark.
    /// Returns true (dark) on any read failure â€” the dark scene is the
    /// default per DESIGN.md so a missing value still lands somewhere
    /// sensible. Backs the singleton's _isSystemDark seam.
    /// </summary>
    private static bool ReadSystemDarkFromRegistry()
    {
        try
        {
            using var key = Registry.CurrentUser.OpenSubKey(
                @"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize");
            if (key?.GetValue("AppsUseLightTheme") is int value)
            {
                return value == 0;
            }
        }
        catch
        {
            // Registry access can fail under unusual security contexts.
        }
        return true;
    }

    /// <summary>
    /// Load the operator's persisted theme preference from
    /// %LOCALAPPDATA%\Dragon-ISO\ui-prefs.json. Returns null on any read
    /// failure (missing file, corrupt JSON, schema mismatch) so the
    /// caller falls back to the in-memory default of "System". Backs
    /// the singleton's loadPreference seam.
    /// </summary>
    private static string? TryLoadPreferenceFromDisk()
    {
        try { return UIPreferences.Load().Theme; }
        catch { return null; }
    }

    /// <summary>
    /// Persist the operator's theme preference to ui-prefs.json. Errors
    /// are swallowed â€” persistence is best-effort and a single failed
    /// save shouldn't break the in-session UI experience. Backs the
    /// singleton's savePreference seam.
    /// </summary>
    private static void TrySavePreferenceToDisk(string preference)
    {
        try { UIPreferences.SetTheme(preference); }
        catch { /* best-effort */ }
    }

    private void OnSystemPreferenceChanged(object? sender, UserPreferenceChangedEventArgs e)
    {
        if (e.Category != UserPreferenceCategory.General) return;
        if (_preference != PreferenceKeySystem) return;
        // Marshal to the UI thread â€” registry events fire on a system pool
        // thread and resource dictionary mutations require dispatcher access.
        Application.Current?.Dispatcher.BeginInvoke(new Action(Apply));
    }

    private static bool IsValidPreference(string? value) =>
        value is PreferenceKeySystem or PreferenceKeyDark or PreferenceKeyLight;
}
