using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CmlLib.Core.Auth;
using ModpackLauncher.Models;
using ModpackLauncher.Services;

namespace ModpackLauncher;

public partial class MainWindow : Window
{
    private enum LauncherState
    {
        Checking,        // initial / fetching manifest
        ConfigError,     // manifest URL not set or unreachable + nothing local
        NeedsDownload,   // no local pack, manifest reachable
        NeedsUpdate,     // local pack version != manifest version
        ReadyNotSignedIn,// up-to-date but not signed in
        Ready            // up-to-date and signed in
    }

    private readonly LauncherConfig _config;
    private readonly AuthService _auth;
    private readonly ManifestSyncService _sync;
    private LauncherSettings _settings;
    private LaunchService? _launch;
    private MSession? _session;
    private Manifest? _remoteManifest;
    private LauncherState _state = LauncherState.Checking;
    private bool _busy;
    private bool _suppressAutoSave;
    private bool _infoPanelOpen;
    // Cached "what the play button should show when not busy". Updated by ApplyState,
    // restored by SetBusy(false). Avoids race between login flow's SetBusy and the
    // RefreshStateAsync triggered by ApplySession.
    private string _playButtonLabel = "Play";
    private bool _playButtonEnabled;

    public MainWindow()
    {
        InitializeComponent();

        FileLog.Init();
        _config = LauncherConfig.Load();
        _auth = new AuthService(_config.MsalClientId);
        _sync = new ManifestSyncService();
        _sync.SetBasicAuth(_config.HttpUsername, _config.HttpPassword);

        var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
        var versionText = version is null ? "" : $" v{version.Major}.{version.Minor}.{version.Build}";
        Title = $"{_config.PackName} Launcher{versionText}";
        PackNameText.Text = _config.PackName;
        TitleText.Text = $"{_config.PackName} Launcher{versionText}";

        _settings = LauncherSettings.Load();

        // Initialise RAM control without triggering auto-save
        _suppressAutoSave = true;
        RamBox.Value = _settings.MemoryMB ?? _config.MemoryMB;
        _suppressAutoSave = false;
        RamBox.ValueChanged += OnRamValueChanged;

        UpdateInstallDirDisplay();
        UpdateRamWarning((int)(RamBox.Value ?? _config.MemoryMB));

        var localVersion = _sync.GetLocalPackVersion(GetInstallDir());
        PackVersionText.Text = localVersion?.Version is { } v
            ? $"Installed: v{v}"
            : "No pack synced yet";

        // Maximize is disabled on this launcher -- the slide-out animation gets unhappy
        // when the window can't actually resize. If something (Win+Up, Aero Snap, etc.)
        // pushes us to Maximized anyway, snap back to Normal.
        PropertyChanged += (_, e) =>
        {
            if (e.Property == WindowStateProperty && WindowState == WindowState.Maximized)
            {
                WindowState = WindowState.Normal;
            }
        };

        Opened += async (_, _) =>
        {
            CheckSystemRequirements();
            await TrySilentSignInAsync();
            await RefreshStateAsync(refetchManifest: true);
        };

        // Auto-refresh manifest when window regains focus (e.g. user alt-tabs back)
        Activated += async (_, _) =>
        {
            if (_busy) return;
            await RefreshStateAsync(refetchManifest: true);
        };

    }

    private async void OnRefreshClick(object? sender, RoutedEventArgs e)
    {
        if (_busy) return;
        _remoteManifest = null; // force a fresh fetch
        await RefreshStateAsync(refetchManifest: true);
    }

    /// <summary>
    /// Compare our running assembly version to the manifest's launcherVersion field.
    /// If the manifest reports something newer, surface a non-blocking banner that
    /// links to the public download URL. Doesn't auto-update -- friends decide when.
    /// </summary>
    private void CheckLauncherVersion(Manifest manifest)
    {
        UpdateBanner.IsVisible = false;
        if (string.IsNullOrWhiteSpace(manifest.LauncherVersion)) return;

        var current = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
        if (current is null) return;
        if (!Version.TryParse(manifest.LauncherVersion, out var advertised)) return;
        if (current >= advertised) return;

        UpdateBannerText.Text = $"A newer launcher (v{advertised}) is available -- you're on v{current.Major}.{current.Minor}.{current.Build}.";
        UpdateBannerDownloadButton.Tag = manifest.LauncherUrl ?? "https://bns.sijbers.uk/launcher/BrassAndSigil-Launcher.exe";
        UpdateBanner.IsVisible = true;
        AppendLog($"[update] Newer launcher available: v{advertised} (running v{current})");
    }

    private async void OnUpdateBannerDownloadClick(object? sender, RoutedEventArgs e)
    {
        var url = UpdateBannerDownloadButton.Tag as string;
        if (string.IsNullOrEmpty(url))
        {
            AppendLog("[update] No download URL configured in manifest.");
            return;
        }

        // First click: try in-place self-update. On failure (network blip, write-protected
        // dir, antivirus blocking), fall back to opening the browser so the user can still
        // grab the new exe manually.
        UpdateBannerDownloadButton.IsEnabled = false;
        var originalLabel = UpdateBannerDownloadButton.Content as string ?? "Install update";
        UpdateBannerDownloadButton.Content = "Updating...";

        try
        {
            var svc = new ModpackLauncher.Services.SelfUpdateService();
            var progress = new Progress<(long bytes, long? total)>(p =>
            {
                var mb = p.bytes / 1024.0 / 1024.0;
                UpdateBannerDownloadButton.Content = p.total.HasValue
                    ? $"{(double)p.bytes * 100 / p.total.Value:F0}%"
                    : $"{mb:F1} MB";
            });
            await svc.DownloadAndInstallAsync(url, progress, AppendLog);
            // If the method returns, it means it didn't exit -- that shouldn't normally
            // happen, but reset the UI just in case.
            UpdateBannerDownloadButton.Content = originalLabel;
            UpdateBannerDownloadButton.IsEnabled = true;
        }
        catch (Exception ex)
        {
            AppendLog($"[update] Auto-update failed: {ex.Message}");
            AppendLog("[update] Opening browser as fallback...");
            try
            {
                Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true });
            }
            catch (Exception ex2)
            {
                AppendLog($"[update] Browser fallback also failed: {ex2.Message}");
            }
            UpdateBannerDownloadButton.Content = originalLabel;
            UpdateBannerDownloadButton.IsEnabled = true;
        }
    }

    private const double InfoPanelExtraWidth = 334; // 320 panel + 14 gap

    private void OnInfoClick(object? sender, RoutedEventArgs e)
    {
        _infoPanelOpen = !_infoPanelOpen;
        ApplyInfoPanelState();
    }

    private void ApplyInfoPanelState()
    {
        var currentContainerWidth = InfoPanelContainer.Width;
        var targetContainerWidth  = _infoPanelOpen ? InfoPanelExtraWidth : 0;
        var deltaWidth            = targetContainerWidth - currentContainerWidth;

        AnimateSlideOut(currentContainerWidth, targetContainerWidth, Width, Width + deltaWidth);
    }

    private DispatcherTimer? _widthAnimTimer;

    private void AnimateSlideOut(double containerStart, double containerEnd,
                                  double windowStart,    double windowEnd,
                                  Action? onComplete = null)
    {
        _widthAnimTimer?.Stop();

        var startTime = DateTime.UtcNow;
        const double durationMs = 220;

        _widthAnimTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(15) };
        _widthAnimTimer.Tick += (_, _) =>
        {
            var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
            var t = Math.Min(1.0, elapsed / durationMs);
            var eased = 1 - Math.Pow(1 - t, 3); // ease-out cubic
            // Animate both in lockstep so the Star column (RootBorder) keeps its width.
            InfoPanelContainer.Width = containerStart + (containerEnd - containerStart) * eased;
            Width                    = windowStart    + (windowEnd    - windowStart)    * eased;
            if (t >= 1.0)
            {
                InfoPanelContainer.Width = containerEnd;
                Width                    = windowEnd;
                _widthAnimTimer?.Stop();
                _widthAnimTimer = null;
                onComplete?.Invoke();
            }
        };
        _widthAnimTimer.Start();
    }

    private void PopulateInfoPanel()
    {
        InfoPanelContent.Children.Clear();
        if (_remoteManifest is null)
        {
            InfoPanelContent.Children.Add(new TextBlock
            {
                Text = "Pack info will appear once the manifest has been fetched.",
                Foreground = new SolidColorBrush(Color.Parse("#9F8E72")),
                TextWrapping = TextWrapping.Wrap,
                FontSize = 12
            });
            return;
        }

        AddInfoSection("Pack",
            ("Name",    _remoteManifest.Name ?? "(unnamed)"),
            ("Version", _remoteManifest.Version ?? "?"));

        AddInfoSection("Minecraft",
            ("Version", _remoteManifest.Minecraft.Version),
            ("Loader",  _remoteManifest.Loader is null
                ? "vanilla"
                : $"{_remoteManifest.Loader.Type} {_remoteManifest.Loader.Version}"));

        var modFiles = _remoteManifest.Files
            .Where(f => f.Path.StartsWith("mods/", StringComparison.OrdinalIgnoreCase))
            .ToList();
        AddModListSection($"Mods ({modFiles.Count})", modFiles);

        var others = _remoteManifest.Files
            .Where(f => !f.Path.StartsWith("mods/", StringComparison.OrdinalIgnoreCase))
            .ToList();
        if (others.Count > 0)
        {
            AddOtherFilesSection($"Other files ({others.Count})", others);
        }
    }

    private void AddInfoSection(string title, params (string Label, string Value)[] rows)
    {
        var stack = new StackPanel { Spacing = 4 };
        stack.Children.Add(new TextBlock
        {
            Text = title.ToUpper(),
            FontSize = 10,
            FontWeight = FontWeight.Bold,
            Foreground = new SolidColorBrush(Color.Parse("#E8B95C")),
            Margin = new Thickness(0, 0, 0, 4)
        });
        foreach (var (label, value) in rows)
        {
            var row = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,*") };
            row.Children.Add(new TextBlock
            {
                Text = label, Margin = new Thickness(0, 0, 8, 0),
                Foreground = new SolidColorBrush(Color.Parse("#7A8497")),
                FontSize = 12,
                MinWidth = 60
            });
            var valueBlock = new TextBlock
            {
                Text = value,
                Foreground = new SolidColorBrush(Color.Parse("#E8DFC8")),
                FontSize = 12,
                TextWrapping = TextWrapping.Wrap
            };
            Grid.SetColumn(valueBlock, 1);
            row.Children.Add(valueBlock);
            stack.Children.Add(row);
        }
        InfoPanelContent.Children.Add(stack);
    }

    private void AddModListSection(string title, System.Collections.Generic.List<ManifestFile> mods)
    {
        var stack = new StackPanel { Spacing = 6 };
        stack.Children.Add(new TextBlock
        {
            Text = title.ToUpper(),
            FontSize = 10,
            FontWeight = FontWeight.Bold,
            Foreground = new SolidColorBrush(Color.Parse("#E8B95C")),
            Margin = new Thickness(0, 0, 0, 4)
        });
        foreach (var mod in mods)
        {
            var (name, version) = ParseModFilename(System.IO.Path.GetFileName(mod.Path));
            var row = new StackPanel { Spacing = 1 };
            row.Children.Add(new TextBlock
            {
                Text = name,
                Foreground = new SolidColorBrush(Color.Parse("#E8DFC8")),
                FontSize = 12,
                FontWeight = FontWeight.Medium
            });
            row.Children.Add(new TextBlock
            {
                Text = version,
                Foreground = new SolidColorBrush(Color.Parse("#7A8497")),
                FontSize = 11
            });
            stack.Children.Add(row);
        }
        InfoPanelContent.Children.Add(stack);
    }

    private void AddOtherFilesSection(string title, System.Collections.Generic.List<ManifestFile> files)
    {
        var stack = new StackPanel { Spacing = 4 };
        stack.Children.Add(new TextBlock
        {
            Text = title.ToUpper(),
            FontSize = 10,
            FontWeight = FontWeight.Bold,
            Foreground = new SolidColorBrush(Color.Parse("#E8B95C")),
            Margin = new Thickness(0, 0, 0, 4)
        });
        foreach (var f in files)
        {
            stack.Children.Add(new TextBlock
            {
                Text = f.Path,
                Foreground = new SolidColorBrush(Color.Parse("#B7C0D6")),
                FontSize = 11,
                TextWrapping = TextWrapping.Wrap
            });
        }
        InfoPanelContent.Children.Add(stack);
    }

    private static (string Name, string Version) ParseModFilename(string filename)
    {
        var stem = System.IO.Path.GetFileNameWithoutExtension(filename);
        // Split on both '-' and '_' -- Modrinth filenames mix both conventions
        // (e.g. Terralith_1.21.x_v2.5.8.jar uses underscores).
        var parts = stem.Split(new[] { '-', '_' });

        // Recognise version segments that start with a digit OR with 'v' followed
        // by a digit (e.g. "v2.5.8"). Find the smallest index that matches so the
        // entire trailing version chain is captured.
        int versionIdx = -1;
        for (int i = parts.Length - 1; i >= 0; i--)
        {
            var p = parts[i];
            if (p.Length == 0) continue;
            var c0 = p[0];
            var startsWithDigit  = char.IsDigit(c0);
            var startsWithVDigit = (c0 == 'v' || c0 == 'V') && p.Length >= 2 && char.IsDigit(p[1]);
            if (startsWithDigit || startsWithVDigit) versionIdx = i;
        }
        if (versionIdx <= 0) return (stem, "");

        var skipWords = new System.Collections.Generic.HashSet<string>(StringComparer.OrdinalIgnoreCase)
        { "neoforge", "forge", "fabric", "bundled" };
        var acronyms = new System.Collections.Generic.HashSet<string>(StringComparer.OrdinalIgnoreCase)
        { "ftb", "tfmg", "jei", "rei", "emi", "ae2", "ic2", "kubejs", "rpl", "c2me", "yungs" };

        var nameParts = parts.Take(versionIdx)
            .Where(s => !skipWords.Contains(s))
            .Select(s => acronyms.Contains(s) ? s.ToUpper() : (s.Length > 0 ? char.ToUpper(s[0]) + s.Substring(1) : s));
        var name = string.Join(" ", nameParts);
        if (string.IsNullOrWhiteSpace(name)) name = stem;
        var version = string.Join("-", parts.Skip(versionIdx));
        return (name, version);
    }

    private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
    {
        if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
        {
            BeginMoveDrag(e);
        }
    }

    private void OnMinimizeClick(object? sender, RoutedEventArgs e)
    {
        WindowState = WindowState.Minimized;
    }

    private void OnCloseClick(object? sender, RoutedEventArgs e)
    {
        Close();
    }

    private void CheckSystemRequirements()
    {
        // WebView2 Runtime is required for the default sign-in flow. Preinstalled on
        // most modern Windows but not guaranteed -- surface a helpful message at startup.
        if (!_auth.HasCustomClientId && !WebView2Check.IsInstalled())
        {
            AppendLog("[system] Microsoft Edge WebView2 Runtime not detected.");
            AppendLog($"[system] Sign-in won't work until it's installed: {WebView2Check.DownloadUrl}");
            UpdateStatus("WebView2 Runtime missing",
                $"Sign-in needs Microsoft Edge WebView2 Runtime -- install from {WebView2Check.DownloadUrl}");
        }
    }

    private async Task TrySilentSignInAsync()
    {
        var session = await _auth.TryAuthenticateSilentlyAsync();
        if (session != null)
        {
            _session = session;
            ApplySession(session);
        }
    }

    private void ApplySession(MSession session)
    {
        _session = session;
        UserText.Text = $"Signed in as {session.Username}";
        LoginButton.IsVisible = false;
        LogoutButton.IsVisible = true;
        _ = RefreshStateAsync();
        _ = RefreshWhitelistStatusAsync();
    }

    private void ClearSession()
    {
        _session = null;
        UserText.Text = "Not signed in";
        LoginButton.IsVisible = true;
        LogoutButton.IsVisible = false;
        WhitelistStatusText.IsVisible = false;
        RequestAccessButton.IsVisible = false;
        _ = RefreshStateAsync();
    }

    private async Task RefreshWhitelistStatusAsync()
    {
        if (_session is null || _remoteManifest is null) return;
        var panelUrl = _remoteManifest.PanelUrl;
        if (string.IsNullOrWhiteSpace(panelUrl))
        {
            // No panel configured in the manifest -- feature disabled.
            WhitelistStatusText.IsVisible = false;
            RequestAccessButton.IsVisible = false;
            return;
        }
        var status = await new WhitelistRequestService().GetStatusAsync(panelUrl, _session.Username ?? "");
        ApplyWhitelistStatus(status);
    }

    private void ApplyWhitelistStatus(string status)
    {
        // Status values: "pending", "approved", "denied", "unknown" (no record), "" (network error).
        switch (status)
        {
            case "pending":
                WhitelistStatusText.Text = "Whitelist request pending";
                WhitelistStatusText.IsVisible = true;
                RequestAccessButton.IsVisible = false;
                break;
            case "approved":
                // Once approved, the server removes the record; this branch is rare
                // (status returns "unknown" almost immediately after approve fires).
                WhitelistStatusText.Text = "Whitelisted ✓";
                WhitelistStatusText.IsVisible = true;
                RequestAccessButton.IsVisible = false;
                break;
            case "denied":
                WhitelistStatusText.Text = "Request denied";
                WhitelistStatusText.IsVisible = true;
                RequestAccessButton.IsVisible = true; // allow retry
                RequestAccessButton.Content = "Request again";
                break;
            case "":
                // Network error -- hide both, don't claim anything.
                WhitelistStatusText.IsVisible = false;
                RequestAccessButton.IsVisible = false;
                break;
            default: // "unknown" -- never requested
                WhitelistStatusText.IsVisible = false;
                RequestAccessButton.IsVisible = true;
                RequestAccessButton.Content = "Request access";
                break;
        }
    }

    private async void OnRequestAccessClick(object? sender, RoutedEventArgs e)
    {
        if (_session is null || _remoteManifest?.PanelUrl is not { } panelUrl) return;
        RequestAccessButton.IsEnabled = false;
        try
        {
            var resp = await new WhitelistRequestService().SubmitAsync(panelUrl, _session.Username ?? "", null);
            if (resp.Ok)
            {
                AppendLog($"[whitelist] Request sent for {_session.Username}.");
                ApplyWhitelistStatus("pending");
            }
            else
            {
                AppendLog($"[whitelist] Request failed: {resp.Error ?? "unknown"}");
                UpdateStatus("Request failed", resp.Error ?? "Couldn't reach server.");
            }
        }
        finally { RequestAccessButton.IsEnabled = true; }
    }

    private async Task RefreshStateAsync(bool refetchManifest = false)
    {
        if (_busy) return;

        if (string.IsNullOrWhiteSpace(_config.ManifestUrl) ||
            _config.ManifestUrl.Contains("example.com"))
        {
            ApplyState(LauncherState.ConfigError, "Setup needed",
                "Set 'manifestUrl' in launcher-config.json and rebuild.");
            return;
        }

        if (refetchManifest || _remoteManifest == null)
        {
            ApplyState(LauncherState.Checking, "Checking...", "Looking for updates");
            try
            {
                _remoteManifest = await _sync.FetchManifestOnlyAsync(_config.ManifestUrl);
                PopulateInfoPanel();
                CheckLauncherVersion(_remoteManifest);
                _ = RefreshWhitelistStatusAsync();
            }
            catch (Exception ex)
            {
                AppendLog($"[manifest fetch] {ex.GetType().Name}: {ex.Message}");
                // Offline fallback -- allow play if we have a local pack
                var fallback = _sync.GetLocalPackVersion(GetInstallDir());
                if (fallback?.Version != null)
                {
                    if (_session != null)
                    {
                        ApplyState(LauncherState.Ready, "Play",
                            $"Offline: pack server unreachable, using local v{fallback.Version}");
                    }
                    else
                    {
                        ApplyState(LauncherState.ReadyNotSignedIn, "Play",
                            "Offline mode -- sign in to play");
                    }
                }
                else
                {
                    ApplyState(LauncherState.ConfigError, "Connection error",
                        $"Couldn't reach pack server: {ex.Message}");
                }
                return;
            }
        }

        var local = _sync.GetLocalPackVersion(GetInstallDir());
        var remote = _remoteManifest!;

        if (local == null)
        {
            ApplyState(LauncherState.NeedsDownload, "Download",
                $"{remote.Name ?? "Pack"} v{remote.Version ?? "?"} ready to download");
            return;
        }

        if (!string.Equals(local.Version, remote.Version, StringComparison.Ordinal))
        {
            ApplyState(LauncherState.NeedsUpdate, "Update",
                $"Update available: v{local.Version} → v{remote.Version}");
            return;
        }

        // Version matches -- but also verify the actual files are on disk. Catches AV
        // quarantines, manual deletions, and interrupted downloads.
        var missing = _sync.FindMissingFiles(remote, GetInstallDir());
        if (missing.Count > 0)
        {
            ApplyState(LauncherState.NeedsUpdate, "Repair",
                $"Pack files missing ({missing.Count}). Click to redownload.");
            return;
        }

        if (_session == null)
        {
            ApplyState(LauncherState.ReadyNotSignedIn, "Play",
                $"Up to date (v{local.Version}). Sign in to play.");
        }
        else
        {
            ApplyState(LauncherState.Ready, "Play",
                $"Up to date (v{local.Version}). Ready to launch.");
        }
    }

    private void ApplyState(LauncherState state, string buttonLabel, string subtext)
    {
        _state = state;
        _playButtonLabel = buttonLabel;
        _playButtonEnabled = state is LauncherState.NeedsDownload
                                   or LauncherState.NeedsUpdate
                                   or LauncherState.Ready;
        if (!_busy)
        {
            PlayButton.Content = _playButtonLabel;
            PlayButton.IsEnabled = _playButtonEnabled;
        }
        StatusText.Text = state switch
        {
            LauncherState.Ready or LauncherState.ReadyNotSignedIn => "Ready",
            LauncherState.NeedsDownload => "Download required",
            LauncherState.NeedsUpdate   => "Update available",
            LauncherState.Checking      => "Checking...",
            LauncherState.ConfigError   => "Setup needed",
            _                           => "..."
        };
        StatusSubtext.Text = subtext;
    }

    private async void OnLoginClick(object? sender, RoutedEventArgs e)
    {
        if (_busy) return;
        try
        {
            SetBusy(true);
            UpdateStatus("Signing in...", "A browser window should appear.");
            var session = await _auth.SignInInteractivelyAsync();
            ApplySession(session);
            UpdateStatus("Signed in", $"Welcome, {session.Username}.");
        }
        catch (Exception ex)
        {
            LogException("auth error", ex);
            UpdateStatus("Sign-in failed", ex.Message);
        }
        finally
        {
            SetBusy(false);
            // ApplySession's fire-and-forget RefreshStateAsync was a no-op because
            // _busy was still true. Re-run state now that we're idle so the Play
            // button enables based on the new session + current pack state.
            await RefreshStateAsync();
        }
    }

    private void LogException(string label, Exception ex)
    {
        AppendLog($"[{label}] {ex.GetType().Name}: {ex.Message}");
        var inner = ex.InnerException;
        var depth = 0;
        while (inner != null && depth < 5)
        {
            AppendLog($"  ↳ {inner.GetType().Name}: {inner.Message}");
            inner = inner.InnerException;
            depth++;
        }
        // Reflectively surface any Code / XErr / Redirect / StatusCode properties (XboxAuthException, etc.)
        foreach (var prop in ex.GetType().GetProperties())
        {
            if (prop.Name is "Code" or "XErr" or "ErrorCode" or "StatusCode" or "Redirect" or "Identity" or "Message" or "Source" or "InnerException" or "TargetSite" or "StackTrace" or "Data" or "HelpLink" or "HResult")
            {
                if (prop.Name is "Message" or "Source" or "InnerException" or "TargetSite" or "StackTrace" or "Data" or "HelpLink" or "HResult") continue;
                try
                {
                    var val = prop.GetValue(ex);
                    if (val != null) AppendLog($"  • {prop.Name}: {val}");
                }
                catch { }
            }
        }
    }

    private async void OnLogoutClick(object? sender, RoutedEventArgs e)
    {
        await _auth.SignOutAsync();
        ClearSession();
        UpdateStatus("Signed out", "Click Sign in to authenticate again.");
    }

    private async void OnPlayClick(object? sender, RoutedEventArgs e)
    {
        if (_busy) return;

        switch (_state)
        {
            case LauncherState.NeedsDownload:
            case LauncherState.NeedsUpdate:
                await DoSyncAndInstallAsync();
                break;
            case LauncherState.Ready:
                await DoLaunchAsync();
                break;
            // Other states leave the button disabled -- shouldn't reach here.
        }
    }

    private async Task DoSyncAndInstallAsync()
    {
        try
        {
            SetBusy(true);
            var installDir = GetInstallDir();
            Directory.CreateDirectory(installDir);
            var progress = new Progress<ProgressReport>(OnProgress);

            UpdateStatus("Syncing pack...", "Fetching manifest and downloading mods.");
            var syncResult = await _sync.SyncAsync(_config.ManifestUrl, installDir, progress);
            _remoteManifest = syncResult.Manifest;
            AppendLog($"Sync: {syncResult.Downloaded} downloaded, {syncResult.Removed} removed.");

            PackVersionText.Text = syncResult.Manifest.Version is { } v
                ? $"Installed: v{v}"
                : "Installed";

            // Pre-populate the multiplayer server list so friends don't have to
            // hand-type the address. Idempotent -- match-by-IP, won't duplicate.
            if (syncResult.Manifest.DefaultServer is { } ds && !string.IsNullOrEmpty(ds.Ip))
            {
                try
                {
                    new ServerListService().EnsureServer(installDir, ds.Name, ds.Ip);
                    AppendLog($"Multiplayer list seeded: {ds.Name} ({ds.Ip}).");
                }
                catch (Exception ex)
                {
                    AppendLog($"[server-list] Couldn't update servers.dat: {ex.Message}");
                }
            }

            // Pre-enable a default shader pack on fresh installs (Iris reads
            // config/iris.properties at startup). Does nothing if the user has
            // already chosen a different shader.
            if (!string.IsNullOrEmpty(syncResult.Manifest.DefaultShader))
            {
                try
                {
                    new IrisConfigService().SetDefaultShader(installDir, syncResult.Manifest.DefaultShader);
                    AppendLog($"Default shader: {syncResult.Manifest.DefaultShader} (only set if no shader was previously chosen).");
                }
                catch (Exception ex)
                {
                    AppendLog($"[iris] Couldn't set default shader: {ex.Message}");
                }
            }

            _launch ??= new LaunchService(installDir);
            UpdateStatus("Installing Minecraft...",
                "Downloading client, libraries, assets, and mod loader. First run may take a few minutes.");
            var versionId = await _launch.InstallVersionAsync(
                syncResult.Manifest, progress, CancellationToken.None);
            AppendLog($"Version ready: {versionId}");
            ResetProgress();
        }
        catch (Exception ex)
        {
            AppendLog($"[error] {ex.GetType().Name}: {ex.Message}");
            UpdateStatus("Install failed", ex.Message);
        }
        finally
        {
            SetBusy(false);
            await RefreshStateAsync();
        }
    }

    private async Task DoLaunchAsync()
    {
        if (_session == null || _remoteManifest == null) return;

        try
        {
            SetBusy(true);

            // ─── Refresh the Microsoft session before launch. ─────────────────
            // _session was set at sign-in; the access token inside is short-lived
            // (~1 hour). If the launcher's been open longer than that, MC will
            // send stale credentials to the server and Mojang's session check
            // rejects the join ("Invalid session"). XboxAuthNet's silent flow
            // uses the cached refresh token (~14 day TTL) to mint a fresh
            // access token transparently.
            UpdateStatus("Refreshing session...", "");
            var freshSession = await _auth.TryAuthenticateSilentlyAsync();
            if (freshSession != null)
            {
                ApplySession(freshSession);
                AppendLog("[auth] Session refreshed before launch.");
            }
            else
            {
                // Refresh token also stale -- need full interactive re-login.
                // Don't try inline (player would lose context); prompt them to
                // hit Sign In again.
                AppendLog("[auth] Silent refresh returned no session; sign-in required.");
                UpdateStatus("Session expired", "Please sign in again to continue.");
                ClearSession();
                return;
            }

            var progress = new Progress<ProgressReport>(OnProgress);
            var installDir = GetInstallDir();
            _launch ??= new LaunchService(installDir);

            // Pre-launch sanity: any manifest files missing? If so, re-sync.
            var missing = _sync.FindMissingFiles(_remoteManifest, installDir);
            if (missing.Count > 0)
            {
                UpdateStatus("Repairing pack...",
                    $"{missing.Count} file(s) missing -- redownloading before launch.");
                AppendLog($"Pre-launch check: {missing.Count} files missing, re-syncing.");
                var syncResult = await _sync.SyncAsync(_config.ManifestUrl, installDir, progress);
                _remoteManifest = syncResult.Manifest;
                AppendLog($"Re-sync: {syncResult.Downloaded} downloaded, {syncResult.Removed} removed.");
            }

            UpdateStatus("Verifying installation...",
                "Checking Minecraft, mod loader, and libraries.");
            // InstallVersionAsync is idempotent + always runs the library verifier afterwards,
            // so this catches any post-install gaps (e.g. CmlLib's bootstraplauncher quirk).
            var versionId = await _launch.InstallVersionAsync(
                _remoteManifest, progress, CancellationToken.None);

            var ram = (int)(RamBox.Value ?? _config.MemoryMB);
            var minRam = ram;
            var maxRam = ram;

            UpdateStatus("Launching Minecraft...", "");
            ResetProgress();

            _ = Task.Run(async () =>
            {
                try
                {
                    await _launch.LaunchAsync(versionId, _session!, minRam, maxRam, progress, CancellationToken.None);
                }
                catch (Exception ex)
                {
                    await Dispatcher.UIThread.InvokeAsync(() =>
                    {
                        AppendLog($"[launch error] {ex.GetType().Name}: {ex.Message}");
                        UpdateStatus("Launch failed", ex.Message);
                    });
                }
                finally
                {
                    await Dispatcher.UIThread.InvokeAsync(async () =>
                    {
                        SetBusy(false);
                        await RefreshStateAsync();
                    });
                }
            });
        }
        catch (Exception ex)
        {
            AppendLog($"[error] {ex.GetType().Name}: {ex.Message}");
            UpdateStatus("Launch failed", ex.Message);
            SetBusy(false);
            await RefreshStateAsync();
        }
    }

    private string GetInstallDir() => _config.GetInstallDir(_settings.InstallDirOverride);

    private void UpdateInstallDirDisplay()
    {
        InstallDirText.Text = GetInstallDir();
    }

    private void UpdateRamWarning(int ramMb)
    {
        var totalMb = SystemInfo.TotalPhysicalMemoryMB;
        var safeMax = SystemInfo.SafeMaxAllocationMB;

        if (totalMb < 12 * 1024)
        {
            RamWarningText.Text = $"Only {totalMb / 1024} GB system RAM detected -- pack may not run smoothly (12+ GB recommended)";
            RamWarningText.Foreground = new SolidColorBrush(Color.Parse("#E8B95C"));
        }
        else if (ramMb > safeMax)
        {
            RamWarningText.Text = $"Above safe limit for {totalMb / 1024} GB system (max {safeMax} MB recommended)";
            RamWarningText.Foreground = new SolidColorBrush(Color.Parse("#DC6E28"));
        }
        else if (ramMb < 6144)
        {
            RamWarningText.Text = "Below recommended (6 GB+ for Distant Horizons)";
            RamWarningText.Foreground = new SolidColorBrush(Color.Parse("#9F8E72"));
        }
        else
        {
            RamWarningText.Text = $"OK ({totalMb / 1024} GB system)";
            RamWarningText.Foreground = new SolidColorBrush(Color.Parse("#7A8497"));
        }
    }

    private void OnRamValueChanged(object? sender, NumericUpDownValueChangedEventArgs e)
    {
        if (_suppressAutoSave) return;
        var ram = (int)(RamBox.Value ?? _config.MemoryMB);
        _settings.MemoryMB = ram;
        _settings.Save();
        UpdateRamWarning(ram);
    }

    private async void OnChangeInstallDirClick(object? sender, RoutedEventArgs e)
    {
        var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
        {
            Title = "Choose install location",
            AllowMultiple = false
        });
        if (folders.Count == 0) return;

        var picked = folders[0].TryGetLocalPath();
        if (string.IsNullOrEmpty(picked)) return;

        _settings.InstallDirOverride = picked;
        _settings.Save();
        UpdateInstallDirDisplay();
        AppendLog($"Install location changed to: {picked}");
        AppendLog("Pack will need to be re-downloaded at the new location on next Play.");

        _launch = null; // force re-create with new path next time
        _remoteManifest = null;
        await RefreshStateAsync(refetchManifest: true);
    }

    private void OnOpenFolderClick(object? sender, RoutedEventArgs e)
    {
        var dir = GetInstallDir();
        Directory.CreateDirectory(dir);
        try
        {
            Process.Start(new ProcessStartInfo
            {
                FileName = dir,
                UseShellExecute = true
            });
        }
        catch (Exception ex)
        {
            AppendLog($"[open folder] {ex.Message}");
        }
    }

    private void OnProgress(ProgressReport report)
    {
        if (Dispatcher.UIThread.CheckAccess())
        {
            ApplyProgress(report);
        }
        else
        {
            Dispatcher.UIThread.Post(() => ApplyProgress(report));
        }
    }

    private void ApplyProgress(ProgressReport report)
    {
        switch (report.Kind)
        {
            case ProgressKind.Status:
                StatusSubtext.Text = report.Message;
                break;
            case ProgressKind.Progress:
                if (report.Percent >= 0) ProgressBar.Value = Math.Clamp(report.Percent, 0, 100);
                StatusSubtext.Text = report.Message;
                break;
            case ProgressKind.Log:
                AppendLog(report.Message);
                break;
            case ProgressKind.Error:
                AppendLog($"[error] {report.Message}");
                break;
        }
    }

    private void UpdateStatus(string title, string subtitle)
    {
        StatusText.Text = title;
        StatusSubtext.Text = subtitle;
    }

    private void ResetProgress()
    {
        ProgressBar.Value = 0;
    }

    private void AppendLog(string message)
    {
        if (string.IsNullOrEmpty(message)) return;
        LogText.Text += (LogText.Text?.Length > 0 ? "\n" : "") + message;
        LogScroll.ScrollToEnd();
        FileLog.Write(message);
    }

    private void SetBusy(bool busy)
    {
        _busy = busy;
        if (busy)
        {
            PlayButton.IsEnabled = false;
            PlayButton.Content = "Working...";
        }
        else
        {
            // Restore from the cached state-derived label (avoids races with concurrent state refreshes).
            PlayButton.Content = _playButtonLabel;
            PlayButton.IsEnabled = _playButtonEnabled;
        }
        LoginButton.IsEnabled = !busy;
        LogoutButton.IsEnabled = !busy;
    }
}
