using System.Diagnostics;
using System.IO;
using ClaudeDo.Installer.Core;

namespace ClaudeDo.Installer.Steps;

public sealed class StopWorkerStep : IInstallStep
{
    public const string LegacyTaskName = "ClaudeDoWorker";

    // Both must be stopped before the install dir is touched: a running app/worker
    // exe locks its directory, so Directory.Move during extraction would otherwise
    // fail with "Access to the path '...\app' is denied".
    private static readonly string[] ProcessNames = { "ClaudeDo.Worker", "ClaudeDo.App" };

    public string Name => "Stop Worker";

    public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
    {
        progress.Report("Stopping ClaudeDo processes (if running)...");
        var installDir = ctx.InstallDirectory;
        var killedAny = false;
        foreach (var name in ProcessNames)
        {
            foreach (var p in Process.GetProcessesByName(name))
            {
                try
                {
                    // Scope to THIS install when the module path is readable; if it
                    // can't be read (access race / exiting process), fall through and
                    // kill anyway — a survivor would lock the install dir during
                    // extraction. Reading MainModule must not skip the Kill.
                    string? path = null;
                    try { path = p.MainModule?.FileName; } catch { /* unreadable — kill anyway */ }
                    if (path is not null && !IsUnder(path, installDir)) continue;
                    p.Kill(entireProcessTree: true);
                    p.WaitForExit(10000);
                    killedAny = true;
                }
                catch { /* process may have exited or be inaccessible */ }
                finally { p.Dispose(); }
            }
        }

        // WaitForExit returns before the OS releases the process's file handles.
        // Give it a moment so DownloadAndExtractStep's Directory.Move doesn't race
        // a still-open handle. (That step also retries, this just avoids the churn.)
        if (killedAny) await Task.Delay(1500, ct);
        return StepResult.Ok();
    }

    private static bool IsUnder(string filePath, string dir)
    {
        try
        {
            if (string.IsNullOrWhiteSpace(dir)) return true; // can't scope — be permissive
            var full = Path.GetFullPath(filePath);
            var root = Path.GetFullPath(dir).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
            return full.StartsWith(root, StringComparison.OrdinalIgnoreCase);
        }
        catch { return false; }
    }
}
