using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input.Platform;
using Avalonia.Markup.Xaml;
using DodoSSH.Client.App.Platform;
using DodoSSH.Client.App.Views;
using DodoSSH.Client.Auth;
using DodoSSH.Client.Session;
using DodoSSH.Client.Shell.Terminal;
using DodoSSH.Client.Shell.ViewModels;
using DodoSSH.Client.Ssh;
using DodoSSH.Client.Storage;
using DodoSSH.Client.Terminal;

namespace DodoSSH.Client.App;

/// <summary>
/// The Avalonia application.
/// </summary>
/// <remarks>
/// Named <c>DodoSshApp</c> rather than the conventional <c>App</c> only because the assembly's root
/// namespace already ends in <c>App</c>, and a type whose name matches its namespace forces every
/// ambiguous reference to be fully qualified.
/// </remarks>
internal sealed partial class DodoSshApp : Application
{
    /// <inheritdoc />
    public override void Initialize() => AvaloniaXamlLoader.Load(this);

    /// <inheritdoc />
    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            Compose(desktop);
        }

        base.OnFrameworkInitializationCompleted();
    }

    /// <remarks>
    /// <para>
    /// Composed by hand rather than through a container. The graph is a handful of objects deep and an
    /// indirection to read through would buy nothing at this size.
    /// </para>
    /// <para>
    /// Everything disposable is a local captured by the closures below rather than a field, because an
    /// Avalonia <c>Application</c> has no disposal hook of its own and a type that owned them would have
    /// nowhere honest to release them.
    /// </para>
    /// </remarks>
    /// <summary>
    /// Puts one line of text on the system clipboard.
    /// </summary>
    /// <remarks>
    /// The clipboard is reached through the window, and at composition time there is no window yet — hence
    /// a closure that looks it up on each call rather than a reference captured now. A machine with no
    /// clipboard falls through silently here; the view model is the one that decides what to say, and it
    /// distinguishes "no clipboard on this machine" from "copied" because they are different answers.
    /// <para>
    /// A delegate rather than handing the view model an <c>IClipboard</c>, so that nothing in the view
    /// models needs a visual and every test that drives them stays window-free.
    /// </para>
    /// </remarks>
    private static Func<string, Task> ClipboardWriter(IClassicDesktopStyleApplicationLifetime desktop) =>
        async text =>
        {
            if (TopLevel.GetTopLevel(desktop.MainWindow) is { Clipboard: { } clipboard })
            {
                await clipboard.SetTextAsync(text).ConfigureAwait(false);
            }
        };

    /// <summary>The terminal workspace, with its loopback listener already up.</summary>
    /// <remarks>
    /// Extracted so that constructing it and starting it cannot drift apart: the data plane's socket has to
    /// be listening before the renderer attaches, and a workspace handed out un-started is one whose first
    /// connect fails for a reason nothing on screen would explain.
    /// </remarks>
    private static TerminalWorkspace StartedWorkspace(SshNetConnectionFactory connections)
    {
        var workspace = new TerminalWorkspace(
            new AvaloniaTerminalAssetProvider(),
            connections,
            TimeProvider.System);

        workspace.Start();

        return workspace;
    }

    private static void Compose(IClassicDesktopStyleApplicationLifetime desktop)
    {
        // ForChannel rather than Default, so a nightly keeps its cache, outbox and device key somewhere
        // the release build never opens. See ClientPaths.ForChannel for why sharing them is the failure
        // worth spending a directory on.
        var paths = ClientPaths.ForChannel(DesktopChannel.Name);
        var caches = ClientCacheFactory.ForFile(paths.CacheFile);

        // Known hosts live in the vault, so trust survives a restart and follows the user to every device.
        // Composed here, once, because the connection factory below needs it now and outlives every unlock;
        // the vault behind it is attached and detached as one is opened and locked. See VaultKnownHostStore
        // for why the handshake is answered from a snapshot rather than by reading the vault per lookup.
        var knownHosts = new VaultKnownHostStore();

        // One factory for both kinds of connection. Shells and file transfers start with the same handshake
        // and the same host key decision, and composing two would mean two snapshots of the pins.
        var connections = new SshNetConnectionFactory(knownHosts);

        var workspace = StartedWorkspace(connections);

        var browser = new SystemBrowserLauncher();

        // Both chosen once, here, because each is a property of the machine rather than of any session. A
        // computer with a usable TPM gets the store that keeps a device key behind a Windows consent prompt;
        // anything else gets one that reports itself unavailable, so unlock keeps asking for the passphrase
        // (ADR 0007). The update channel answers the same shape of question about how this copy was
        // installed, and a build run from a checkout likewise gets one that says so. See ADR 0013.
        var deviceKeys = DesktopDeviceKeyStores.ForThisMachine(paths);
        var updates = UpdateChannels.ForThisMachine();

        var viewModel = new MainWindowViewModel(
            paths,
            caches,
            workspace,
            knownHosts,
            deviceKeys,
            async (url, cancellationToken) => await ServerConnection
                .SignInAsync(url, browser, TimeProvider.System, cancellationToken)
                .ConfigureAwait(false),
            TimeProvider.System,
            connections,
            passphraseProfile: null,

            // The other half of signing in: a refresh grant, no browser, and nobody present. It is what
            // makes a launch after the first one arrive online rather than merely enrolled.
            resume: async (url, refreshToken, cancellationToken) => await ServerConnection
                .ResumeAsync(url, refreshToken, TimeProvider.System, cancellationToken)
                .ConfigureAwait(false),

            copyToClipboard: ClipboardWriter(desktop),
            updates: updates);

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

        // Started rather than awaited: the framework's initialisation must not block on a schema
        // migration. The view model shows its own progress and handles its own failures, which is why
        // discarding the task here is safe rather than merely convenient.
        _ = viewModel.StartAsync(CancellationToken.None);

        WireShutdown(desktop, viewModel, workspace, caches);
    }

    /// <remarks>
    /// Shutdown is deferred rather than blocked on. Sessions hold SSH connections and a listening socket, and
    /// blocking the UI thread on their disposal is how an application comes to take several seconds to close —
    /// or deadlocks, if any of that disposal needs the UI thread.
    /// </remarks>
    private static void WireShutdown(
        IClassicDesktopStyleApplicationLifetime desktop,
        MainWindowViewModel viewModel,
        TerminalWorkspace workspace,
        ClientCacheFactory caches)
    {
        var shuttingDown = false;

        desktop.ShutdownRequested += async (_, e) =>
        {
            if (shuttingDown)
            {
                return;
            }

            shuttingDown = true;
            e.Cancel = true;

            // The view model first: it holds the vault session, and disposing that is what zeroes the
            // identity keys, the vault keys and the cache key.
            await viewModel.DisposeAsync().ConfigureAwait(true);
            await workspace.DisposeAsync().ConfigureAwait(true);

            caches.Dispose();

            desktop.Shutdown();
        };
    }
}
