using SilverOS.Welcome.Core.Apply;

namespace SilverOS.Welcome.Core.Apps;

public sealed class AppInstaller(IProcessRunner runner, string appsDir) : IAppInstaller
{
    // Best-effort diagnostic log (winget resolution, bootstrap output, per-app results).
    // Lives under ProgramData so it survives + is readable post-install. Never throws.
    private static readonly string LogPath = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "SilverMetal", "appinstall.log");

    private static void Log(string msg)
    {
        try
        {
            Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
            File.AppendAllText(LogPath, $"{DateTime.Now:HH:mm:ss.fff} {msg}{Environment.NewLine}");
        }
        catch { /* logging is best-effort */ }
    }

    private static string Snip(string? s) =>
        string.IsNullOrWhiteSpace(s) ? "" : s.Trim().Replace("\r", " ").Replace("\n", " ") is var t && t.Length > 300 ? t[..300] : t;

    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;

        Log($"InstallAsync: {apps.Count} app(s) requested: {string.Join(", ", apps.Select(a => a.Id))}");

        // 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)
        {
            Log($"winget UNAVAILABLE -> skipping all {apps.Count} app(s)");
            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;
                Log($"install {id}: exit={r.ExitCode} ok={ok} err={Snip(r.StdErr)}");
                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));
        }
        Log($"InstallAsync done: {results.Count(r => r.Installed)}/{results.Count} installed");
        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)?
        var p1 = await TryRunAsync("winget", "--version", ct);
        Log($"winget probe (PATH): exit={p1.ExitCode} out={Snip(p1.StdOut)}");
        if (p1.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");
        var b = await TryRunAsync("powershell.exe",
            $"-NoProfile -ExecutionPolicy Bypass -Command \"if (Test-Path '{bootstrap}') {{ & '{bootstrap}' }} else {{ " +
            "Add-AppxPackage -RegisterByFamilyName -MainPackage Microsoft.DesktopAppInstaller_8wekyb3d8bbwe -EA SilentlyContinue }}\"",
            ct);
        Log($"bootstrap-winget: exit={b.ExitCode} out={Snip(b.StdOut)} err={Snip(b.StdErr)}");
        var p2 = await TryRunAsync("winget", "--version", ct);
        Log($"winget probe (post-bootstrap): exit={p2.ExitCode} out={Snip(p2.StdOut)}");
        if (p2.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");
        var aliasExists = File.Exists(aliased);
        var p3Exit = aliasExists ? (await TryRunAsync(aliased, "--version", ct)).ExitCode : -1;
        Log($"winget alias path '{aliased}': exists={aliasExists} probe={p3Exit}");
        if (aliasExists && p3Exit == 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); }
    }
}
