using SilverOS.Welcome.Core.Apply;

namespace SilverOS.Welcome.Core.Apps;

public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppInstaller
{
    public async Task<IReadOnlyList<AppInstallResult>> InstallAsync(
        IReadOnlyList<AppCatalogEntry> apps, IProgress<ApplyProgress> progress, CancellationToken ct = default)
    {
        var results = new List<AppInstallResult>();
        if (apps.Count == 0) return results;

        // App installs are non-critical: a missing/broken winget (e.g. offline IoT LTSC) must
        // NEVER fail onboarding. Resolve winget defensively; if it can't be found, skip installs.
        var winget = await ResolveWingetAsync(progress, ct);
        if (winget is null)
        {
            progress.Report(new($"App installer unavailable - skipping {apps.Count} app(s)", 80));
            foreach (var app in apps) results.Add(new AppInstallResult(app.Id, false));
            return results;
        }

        var i = 0;
        foreach (var app in apps)
        {
            i++;
            progress.Report(new($"Installing {app.Name} ({i}/{apps.Count})", 80));
            var ok = false;
            var id = app.Source.Winget;
            if (!string.IsNullOrWhiteSpace(id))
            {
                var r = await TryRunAsync(winget,
                    $"install --id {id} --silent --accept-package-agreements --accept-source-agreements --disable-interactivity",
                    ct);
                ok = r.ExitCode == 0;
                if (ok && !string.IsNullOrWhiteSpace(app.Configure))
                {
                    var script = Path.Combine(appsDir, "configure", app.Configure);
                    // best-effort: configuration failure does not mark the install as failed
                    await TryRunAsync("powershell.exe",
                        $"-NoProfile -ExecutionPolicy Bypass -File \"{script}\"", ct);
                }
            }
            results.Add(new AppInstallResult(app.Id, ok));
        }
        return results;
    }

    // Find a usable winget. winget (App Installer) is absent from IoT Enterprise LTSC, and even
    // when present it ships as a WindowsApps execution alias that Process.Start can't always launch
    // by bare name. Returns the invocation target ("winget" or a full path), or null if unavailable.
    private async Task<string?> ResolveWingetAsync(IProgress<ApplyProgress> progress, CancellationToken ct)
    {
        // 1) Already launchable by name (on PATH for this process)?
        if ((await TryRunAsync("winget", "--version", ct)).ExitCode == 0) return "winget";

        // 2) Provision App Installer via the bundled bootstrap (or registered package), then re-probe.
        progress.Report(new("Preparing app installer", 68));
        var bootstrap = Path.Combine(appsDir, "bootstrap-winget.ps1");
        await TryRunAsync("powershell.exe",
            $"-NoProfile -ExecutionPolicy Bypass -Command \"if (Test-Path '{bootstrap}') {{ & '{bootstrap}' }} else {{ " +
            "Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -EA SilentlyContinue }}\"",
            ct);
        if ((await TryRunAsync("winget", "--version", ct)).ExitCode == 0) return "winget";

        // 3) Fall back to the WindowsApps execution-alias path (bare-name launch can fail under
        //    UseShellExecute=false even when winget is installed).
        var local = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        var aliased = Path.Combine(local, "Microsoft", "WindowsApps", "winget.exe");
        if (File.Exists(aliased) && (await TryRunAsync(aliased, "--version", ct)).ExitCode == 0) return aliased;

        return null;
    }

    // Launching a missing executable throws Win32Exception ("cannot find the file specified")
    // rather than returning a non-zero code. Treat any launch failure as a failed run so the
    // installer's continue-on-failure logic covers it and onboarding never crashes.
    private async Task<ProcessResult> TryRunAsync(string file, string args, CancellationToken ct)
    {
        try { return await runner.RunAsync(file, args, ct); }
        catch (Exception ex) { return new ProcessResult(-1, "", ex.Message); }
    }
}
