using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using ClawdDotNet.App;
using ClawdDotNet.App.Services;
using ClawdDotNet.App.Settings;
using ClawdDotNet.Desktop.Services;
using ClawdDotNet.Desktop.ViewModels;
using ClawdDotNet.Desktop.Views;

namespace ClawdDotNet.Desktop;

public partial class App : Application
{
    /// <summary>
    /// Schalter der Befehlszeile, mit dem sich ClawdDotNet auf eine bestimmte Instanz
    /// starten laesst. Gesetzt wird er nur von der Anwendung selbst, wenn der
    /// Instanzdialog einen Wechsel verlangt — deshalb ist er nirgends dokumentiert.
    /// </summary>
    private const string InstanceArgument = "--instance";

    private AppHost? _host;

    /// <summary>Vorgewaehlte Instanz aus der Befehlszeile; wird genau einmal verbraucht.</summary>
    private string? _preselectedInstancePath;

    public override void Initialize() => AvaloniaXamlLoader.Load(this);

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            // Erst beenden, wenn wir es sagen: Zwischen Instanzauswahl und Hauptfenster
            // ist kurz gar kein Fenster offen. Mit OnLastWindowClose würde die Anwendung
            // in genau dieser Lücke aussteigen.
            desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;

            _preselectedInstancePath = ReadInstanceArgument(desktop.Args);

            ApplySavedThemeEarly();

            desktop.ShutdownRequested += async (_, _) =>
            {
                if (_host is not null) await _host.DisposeAsync();
            };

            // Nicht abwarten: OnFrameworkInitializationCompleted muss zurückkehren,
            // damit die Nachrichtenschleife anläuft — sonst gäbe es keinen Faden, auf
            // dem die Fenster des Startvorgangs überhaupt erscheinen könnten.
            _ = StartAsync(desktop);
        }

        base.OnFrameworkInitializationCompleted();
    }

    /// <summary>
    /// Setzt das gespeicherte Farbschema, bevor das erste Fenster erscheint.
    ///
    /// <para>Das Ansichtsmodell des Hauptfensters tut dasselbe — aber erst, wenn der
    /// Aufbau steht. Dazwischen liegt die Instanzauswahl, und die waere sonst bei einem
    /// Benutzer im Dunkelmodus hell und der Rest der Anwendung dunkel.</para>
    ///
    /// <para>Ein zweites Laden der Einstellungen ist unbedenklich: Load liest nur und
    /// schreibt allenfalls beim allerersten Start die Vorgaben fest.</para>
    /// </summary>
    private static void ApplySavedThemeEarly()
    {
        try
        {
            var settings = new SettingsManager();
            settings.Load();
            ThemeManager.Apply(ThemeManager.IsDarkName(settings.AppSettings.Theme));
        }
        catch (Exception)
        {
            // Ohne lesbare Einstellungen bleibt es beim hellen Schema.
        }
    }

    private async Task StartAsync(IClassicDesktopStyleApplicationLifetime desktop)
    {
        var result = await AppHost.StartAsync(new AppHost.Callbacks
        {
            SelectInstance = SelectInstanceAsync,
            License = new AvaloniaLicensePrompt(),
            TelegramLogin = prompt => AskAsync("Telegram Verifizierung", prompt),
            Telegram2FA = () => AskAsync("Telegram 2FA", "Bitte 2FA-Passwort eingeben:")
        });

        if (result.Error is { } error)
        {
            await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Fehler", error);
            desktop.Shutdown(1);
            return;
        }

        if (result.Host is null)
        {
            // Abbruch durch den Benutzer oder fehlende Lizenz — beides ist bereits
            // erklärt worden, hier kommt keine weitere Meldung hinterher.
            desktop.Shutdown();
            return;
        }

        _host = result.Host;

        HookErrorReporting(_host);
        HookLicenseWatch(_host, desktop);

        var mainViewModel = new MainWindowViewModel(_host);

        // Der Wechsel gehoert hierher und nicht ins Fenster: Er braucht den Aufbau und
        // die Lebensdauer der Anwendung, und beides kennt nur diese Klasse.
        mainViewModel.RequestInstanceManagerDialog += () => _ = SwitchInstanceAsync(desktop);

        desktop.MainWindow = new MainWindow { DataContext = mainViewModel };

        desktop.MainWindow.Show();
        desktop.MainWindow.Closed += (_, _) => desktop.Shutdown();

        await OfferUpdateAsync(_host, desktop);
    }

    /// <summary>
    /// Meldet ungefangene Ausnahmen an den Fehler-Stream des Deploymentcenters.
    ///
    /// <para>Erst hier verdrahtet, nicht in <c>Main</c>: Vor dem Aufbau gibt es weder
    /// Einstellungen noch Token, und ohne die wäre der Meldeweg ohnehin der Leerlauf.
    /// Die Kehrseite ist bewusst in Kauf genommen — ein Absturz <em>während</em> des
    /// Starts erreicht das Deploymentcenter nicht, steht aber im Protokoll.</para>
    /// </summary>
    private static void HookErrorReporting(AppHost host)
    {
        AppDomain.CurrentDomain.UnhandledException += (_, args) =>
        {
            if (args.ExceptionObject is Exception ex)
            {
                // Der Prozess endet gleich: kurze Frist, dann weiterlaufen lassen.
                host.Errors.ReportAsync(ex, fatal: args.IsTerminating)
                    .Wait(TimeSpan.FromSeconds(3));
            }
        };

        TaskScheduler.UnobservedTaskException += (_, args) =>
        {
            _ = host.Errors.ReportAsync(args.Exception, fatal: false);

            // Ohne Observe reißt eine unbeobachtete Ausnahme in manchen Konfigurationen
            // den Prozess mit — und das wäre eine Nebenwirkung des Meldens.
            args.SetObserved();
        };

        Dispatcher.UIThread.UnhandledException += (_, args) =>
        {
            _ = host.Errors.ReportAsync(args.Exception, fatal: false);

            // Ein Fehler in einem Ereignisbehandler soll die Oberfläche nicht beenden.
            args.Handled = true;
        };
    }

    /// <summary>Ein Widerruf beendet die Anwendung, ohne Beenden-Rückfrage.</summary>
    private static void HookLicenseWatch(AppHost host, IClassicDesktopStyleApplicationLifetime desktop)
    {
        if (host.LicenseWatch is null) return;

        host.LicenseWatch.Revoked += async message =>
        {
            await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Lizenz", message);
            await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(2));
        };
    }

    /// <summary>
    /// Fragt, ob ein verfügbares Update jetzt eingespielt werden soll.
    ///
    /// <para>Bewusst eine Frage und kein Selbstlauf: Wann aktualisiert wird, entscheidet
    /// der Benutzer — eine Anwendung, die sich beim Start selbst beendet, um sich zu
    /// erneuern, ist genau dann im Weg, wenn man sie braucht.</para>
    /// </summary>
    private static async Task OfferUpdateAsync(AppHost host, IClassicDesktopStyleApplicationLifetime desktop)
    {
        if (host.Deploymentcenter is null) return;

        // Die Prüfung läuft nebenher; kurz Zeit geben, dann aufgeben.
        for (var waited = 0; host.Deploymentcenter.Update is null && waited < 10; waited++)
            await Task.Delay(TimeSpan.FromSeconds(1));

        if (host.Deploymentcenter.Update is not { IsAvailable: true } update) return;

        var notes = string.IsNullOrWhiteSpace(update.ReleaseNotes) ? "" : $"\n\n{update.ReleaseNotes}";

        var install = await ConfirmAsync(
            update.IsCritical ? "ClawdDotNet – Wichtiges Update" : "ClawdDotNet – Update",
            $"Version {update.LatestVersion} ist verfügbar.{notes}\n\n"
            + "Zum Einspielen wird ClawdDotNet beendet und danach neu gestartet.",
            confirmText: "Jetzt installieren",
            cancelText: "Später");

        if (!install) return;

        await StartUpdateAsync(host, desktop, update.LatestVersion);
    }

    /// <summary>
    /// Die Reihenfolge ist der ganze Punkt dieser Methode.
    ///
    /// <list type="number">
    /// <item>Dem Watchdog sagen, dass gleich ein Update kommt — sonst sieht das
    ///       Verschwinden der Instanz aus wie ein Ausfall.</item>
    /// <item>Geordnet herunterfahren: Datenbank, Scanner, Telegram, Abmeldung.</item>
    /// <item>Erst dann den Agenten starten. Er bekommt unsere Prozesskennung und wartet,
    ///       bis wir weg sind, statt über gesperrte Dateien zu kopieren.</item>
    /// <item>Selbst beenden.</item>
    /// </list>
    ///
    /// <para>Die Verlockung wäre, <c>LaunchUpdateAgent</c> das Beenden zu überlassen —
    /// das tut es aber über <c>Environment.Exit</c> und übergeht damit Schritt 2
    /// vollständig.</para>
    /// </summary>
    private static async Task StartUpdateAsync(
        AppHost host, IClassicDesktopStyleApplicationLifetime desktop, string version)
    {
        host.Deploymentcenter!.AnnounceUpdate(version);

        var settings = host.Settings.AppSettings;
        await host.DisposeAsync();

        var problem = host.Deploymentcenter.StartUpdate(settings, AppHost.AppVersion);

        if (problem is not null)
        {
            await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Update", problem);

            // Der Aufbau ist bereits abgebaut — weiterarbeiten wäre nicht ehrlich.
            await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(1));
            return;
        }

        await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown());
    }

    /// <summary>Ja/Nein-Rückfrage. Avalonia bringt kein MessageBox mit.</summary>
    private static async Task<bool> ConfirmAsync(
        string title, string message, string confirmText, string cancelText)
        => await Dispatcher.UIThread.InvokeAsync(async () =>
        {
            var completion = new TaskCompletionSource<bool>();

            var confirm = new Button { Content = confirmText, IsDefault = true };
            var cancel = new Button { Content = cancelText, IsCancel = true };

            var window = new Window
            {
                Title = title,
                Width = 480,
                SizeToContent = SizeToContent.Height,
                CanResize = false,
                WindowStartupLocation = WindowStartupLocation.CenterScreen,
                Content = new StackPanel
                {
                    Margin = new Thickness(20),
                    Spacing = 16,
                    Children =
                    {
                        new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
                        new StackPanel
                        {
                            Orientation = Avalonia.Layout.Orientation.Horizontal,
                            HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
                            Spacing = 8,
                            Children = { cancel, confirm }
                        }
                    }
                }
            };

            var answer = false;
            confirm.Click += (_, _) => { answer = true; window.Close(); };
            cancel.Click += (_, _) => window.Close();

            // Schließt der Benutzer das Fenster, gilt das als „Später".
            window.Closed += (_, _) => completion.TrySetResult(answer);

            window.Show();
            return await completion.Task;
        });

    /// <summary>
    /// Instanzwechsel aus der laufenden Anwendung.
    ///
    /// <para>Ein Wechsel im laufenden Prozess ist nicht vorgesehen und waere auch keine
    /// kleine Sache: Engine, Scanner, Telegram-Anbindung und Datenbank haengen alle an
    /// <em>einer</em> Instanz. Statt das umzubauen startet sich die Anwendung mit
    /// <c>--instance</c> neu — derselbe Weg, den auch das Update geht.</para>
    ///
    /// <para>Reihenfolge wie dort: erst geordnet herunterfahren, dann den neuen Prozess
    /// starten. Andersherum liefen kurz zwei Prozesse auf derselben Datenbank.</para>
    /// </summary>
    private async Task SwitchInstanceAsync(IClassicDesktopStyleApplicationLifetime desktop)
    {
        if (_host is null) return;

        var viewModel = new InstancePickerViewModel(_host.Directories, _host.InstancePath);
        var window = new InstancePickerWindow { DataContext = viewModel };

        // Schliesst der Benutzer das Fenster, gilt das als Abbruch.
        window.Closed += (_, _) => viewModel.CancelCommand.Execute(null);

        // Umgekehrt: Sobald eine Wahl feststeht, schliesst sich das Fenster von selbst.
        _ = viewModel.Result.ContinueWith(
            _ => Dispatcher.UIThread.Post(window.Close), TaskScheduler.Default);

        if (desktop.MainWindow is { } owner) await window.ShowDialog(owner);
        else window.Show();

        var path = await viewModel.Result;

        if (path is null) return;
        if (string.Equals(path, _host.InstancePath, StringComparison.OrdinalIgnoreCase)) return;

        var executable = Environment.ProcessPath;

        if (executable is null)
        {
            await new AvaloniaLicensePrompt().ShowErrorAsync(
                "ClawdDotNet – Instanzwechsel",
                "Der eigene Programmpfad ist nicht ermittelbar; ein Neustart ist von hier "
                + "aus nicht möglich. Bitte ClawdDotNet von Hand neu starten.");
            return;
        }

        await _host.DisposeAsync();

        try
        {
            Process.Start(new ProcessStartInfo(executable)
            {
                ArgumentList = { InstanceArgument, path },
                UseShellExecute = false
            });
        }
        catch (Exception ex)
        {
            await new AvaloniaLicensePrompt().ShowErrorAsync(
                "ClawdDotNet – Instanzwechsel",
                $"Neustart fehlgeschlagen: {ex.Message}");
        }

        await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown());
    }

    private static string? ReadInstanceArgument(IReadOnlyList<string>? args)
    {
        if (args is null) return null;

        for (var i = 0; i < args.Count - 1; i++)
        {
            if (string.Equals(args[i], InstanceArgument, StringComparison.OrdinalIgnoreCase))
                return args[i + 1];
        }

        return null;
    }

    /// <summary>
    /// Instanzauswahl beim Start.
    ///
    /// <para>Kam die Anwendung mit <c>--instance</c> hoch, wird nicht gefragt: Dann hat
    /// der Benutzer die Instanz im vorigen Prozess bereits gewaehlt, und ein zweiter
    /// Dialog waere nur die Frage nach der schon gegebenen Antwort. Der Wert wird einmal
    /// verbraucht — bleibt der Pfad ungueltig, erscheint doch der Dialog, statt dass der
    /// Start wortlos scheitert.</para>
    /// </summary>
    private async Task<string?> SelectInstanceAsync(InstanceDirectoryManager directories)
    {
        var preselected = _preselectedInstancePath;
        _preselectedInstancePath = null;

        if (preselected is not null && Directory.Exists(preselected))
            return preselected;

        return await ShowInstancePickerAsync(directories);
    }

    private static async Task<string?> ShowInstancePickerAsync(InstanceDirectoryManager directories)
        => await Dispatcher.UIThread.InvokeAsync(async () =>
        {
            var viewModel = new InstancePickerViewModel(directories);
            var window = new InstancePickerWindow { DataContext = viewModel };

            // Schließt der Benutzer das Fenster, gilt das als Abbruch — sonst wartete
            // der Start für immer auf eine Auswahl, die nie kommt.
            window.Closed += (_, _) => viewModel.CancelCommand.Execute(null);

            window.Show();

            var path = await viewModel.Result;
            window.Close();

            return path;
        });

    /// <summary>
    /// Einzeilige Abfrage — ersetzt <c>Microsoft.VisualBasic.Interaction.InputBox</c>,
    /// das die Telegram-Anmeldung an Windows band.
    /// </summary>
    private static async Task<string> AskAsync(string title, string prompt)
        => await Dispatcher.UIThread.InvokeAsync(async () =>
        {
            var completion = new TaskCompletionSource<string>();

            var input = new TextBox();
            var ok = new Button { Content = "OK", IsDefault = true };

            var window = new Window
            {
                Title = title,
                Width = 420,
                SizeToContent = SizeToContent.Height,
                CanResize = false,
                WindowStartupLocation = WindowStartupLocation.CenterScreen,
                Content = new StackPanel
                {
                    Margin = new Thickness(20),
                    Spacing = 12,
                    Children =
                    {
                        new TextBlock { Text = prompt, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
                        input,
                        ok
                    }
                }
            };

            ok.Click += (_, _) => window.Close();
            window.Closed += (_, _) => completion.TrySetResult(input.Text?.Trim() ?? "");

            window.Show();
            input.Focus();

            return await completion.Task;
        });
}
