using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;

using DodoSSH.Client.Android.Platform;
using DodoSSH.Client.Android.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.Android;

/// <summary>
/// The Avalonia application, phone side.
/// </summary>
/// <remarks>
/// Named <c>DodoSshApp</c> for the same reason the desktop head's is, and then for a second reason on top
/// of it: a type called <c>App</c> in a namespace ending <c>.Android</c> is what makes every
/// <c>Android.App</c> in this assembly ambiguous. See the note at the top of MainActivity.
/// </remarks>
public sealed partial class DodoSshApp : Avalonia.Application
{
    /// <inheritdoc />
    public override void Initialize() => AvaloniaXamlLoader.Load(this);

    /// <inheritdoc />
    public override void OnFrameworkInitializationCompleted()
    {
        // ISingleViewApplicationLifetime, not IClassicDesktopStyleApplicationLifetime: a phone has one
        // surface and no window to own. That difference is the whole reason the two heads cannot share a
        // composition root, and very nearly the only one — everything either of them composes is the same.
        if (ApplicationLifetime is ISingleViewApplicationLifetime single)
        {
            single.MainView = Compose();
        }

        base.OnFrameworkInitializationCompleted();
    }

    /// <remarks>
    /// <para>
    /// Composed by hand rather than through a container, matching the desktop head: the graph is a handful
    /// of objects deep and an indirection to read through would buy nothing at this size. Read it beside
    /// <c>DodoSSH.Client.App/App.axaml.cs</c> — the shape is deliberately identical, and the four
    /// differences are the four things docs/android-port.md said would differ.
    /// </para>
    /// <para>
    /// <b>Nothing here is disposed on a lifecycle hook</b>, and that is not an oversight either. Android
    /// does not promise to call anything before killing a process, so a teardown path would be a comfort
    /// rather than a guarantee. What actually protects the sessions is the foreground service; what
    /// protects the vault keys is that they never leave memory this process owns.
    /// </para>
    /// </remarks>
    private static PhoneShell Compose()
    {
        // Difference 1: the profile directory comes from the head. filesDir is per-app and non-roaming,
        // which is what ClientPaths asks for and what no desktop platform guarantees.
        var paths = PhoneEnvironment.Paths;
        paths.EnsureCreated();

        var caches = ClientCacheFactory.ForFile(paths.CacheFile);

        var knownHosts = new VaultKnownHostStore();
        var connections = new SshNetConnectionFactory(knownHosts);

        var workspace = new TerminalWorkspace(
            new AvaloniaTerminalAssetProvider(),
            connections,
            TimeProvider.System);

        workspace.Start();

        // Difference 2: the foreground service, which is what makes TerminalWorkspace's promise — that a
        // shell outlives a vault lock — true on a platform that stops backgrounded processes.
        // Zero transfers for now: file transfer is out of this head's first scope by decision, and this is
        // the seam it arrives through rather than a dependency taken before there is anything behind it.
        // A local rather than a field, matching the desktop head: an Avalonia Application has no disposal
        // hook, so a field holding a disposable would have nowhere honest to release it. It stays alive
        // because it is subscribed to the workspace, which lives as long as the process.
        //
        // Refresh() is called once here. Calling it again when a shell opens is what the terminal screen
        // will wire, and there is nothing to wire it to yet — the workspace announces sessions ending on
        // its own, which is the half that would otherwise leave a notification up over nothing.
        var keepAlive = new SessionKeepAlive(workspace, activeTransfers: () => 0);

        keepAlive.Refresh();

        return new PhoneShell { DataContext = ComposeShell(paths, caches, workspace, knownHosts, connections) };
    }

    /// <remarks>
    /// Split from <see cref="Compose"/> only for length. The division is a real one though: above this is
    /// the platform graph, and below it is the shell every head shares.
    /// </remarks>
    private static MainWindowViewModel ComposeShell(
        ClientPaths paths,
        ClientCacheFactory caches,
        TerminalWorkspace workspace,
        VaultKnownHostStore knownHosts,
        SshNetConnectionFactory connections)
    {
        // Difference 3: the Android keystore, with a fingerprint or the device credential releasing the
        // key. A straight implementation of the interface the session layer has always taken.
        var deviceKeys = new AndroidDeviceKeyStore(paths);

        var browser = new AndroidBrowserLauncher();

        var viewModel = new MainWindowViewModel(
            paths,
            caches,
            workspace,
            knownHosts,
            deviceKeys,

            // Difference 4: the system browser by intent, and the response by intent too rather than on a
            // loopback socket. See AndroidAuthorization for why the desktop's listener is not reused.
            async (url, cancellationToken) => await ServerConnection
                .SignInAsync(url, browser, TimeProvider.System, cancellationToken, ConfigureAndroidOidc)
                .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));

        // Started rather than awaited: framework initialisation must not block on a schema migration. The
        // view model shows its own progress and handles its own failures.
        _ = viewModel.StartAsync(CancellationToken.None);

        return viewModel;
    }

    /// <summary>
    /// Difference 4: where the authorization response comes back to.
    /// </summary>
    /// <remarks>
    /// The only thing this head changes about signing in, and it changes it for a security reason rather
    /// than a platform one. The desktop receives the response on a loopback <c>TcpListener</c>; on a phone
    /// any other installed application can bind a loopback port and race for the authorization code, which
    /// is the attack RFC 8252 §8.3 names. Android routes a registered redirect to this application
    /// instead — see <see cref="AndroidRedirectCallback"/>, including what a private-use scheme does and
    /// does not protect against.
    /// </remarks>
    private static OidcClientOptions ConfigureAndroidOidc(OidcClientOptions options) =>
        options with { CallbackFactory = path => new AndroidRedirectCallback(path) };
}
