using System.Diagnostics;
using System.IO;
using ClaudeDo.Data;
using ClaudeDo.Installer.Steps;
using Microsoft.Win32;

namespace ClaudeDo.Installer.Core;

public sealed class UninstallRunner
{
    private readonly InstallContext _context;
    private readonly StopWorkerStep _stopService;

    public UninstallRunner(InstallContext context, StopWorkerStep stopService)
    {
        _context = context;
        _stopService = stopService;
    }

    public async Task<StepResult> RunAsync(bool removeAppData, IProgress<string> progress, CancellationToken ct)
    {
        // 1) Validate install dir up front — refuse obviously unsafe paths.
        //    Prevents Directory.Delete(recursive:true) from wiping C:\ or C:\Program Files\.
        if (!IsSafeInstallDir(_context.InstallDirectory, out var safeError))
            return StepResult.Fail($"Refusing to uninstall: {safeError}");

        // 2) Stop service. If stop fails we MUST abort — deleting a service whose
        //    process is still running leaves orphan locked binaries under the install dir
        //    which Directory.Delete will silently skip.
        progress.Report("Stopping worker...");
        var stopResult = await _stopService.ExecuteAsync(_context, progress, ct);
        if (!stopResult.Success)
            return StepResult.Fail(
                $"Cannot uninstall: worker did not stop cleanly. {stopResult.ErrorMessage} " +
                "Kill the worker manually and re-run uninstall.");

        // 3) Best-effort removal of the legacy scheduled task and Windows service
        //    (older installs; current installs autostart via a Startup-folder shortcut).
        progress.Report("Removing legacy autostart task...");
        await ProcessRunner.RunAsync("schtasks.exe", $"/Delete /TN \"{StopWorkerStep.LegacyTaskName}\" /F", null, progress, ct);
        await ProcessRunner.RunAsync("sc.exe", "delete ClaudeDoWorker", null, progress, ct);

        // 3b) Remove Apps & Features registry entry (best-effort).
        progress.Report("Removing Add/Remove Programs entry...");
        try
        {
            Registry.LocalMachine.DeleteSubKeyTree(WriteUninstallRegistryStep.UninstallKeyPath, throwOnMissingSubKey: false);
        }
        catch (Exception ex)
        {
            progress.Report($"Warning: could not delete uninstall registry key: {ex.Message}");
        }

        // 4) Remove shortcuts (best-effort — a stuck .lnk must not block the rest).
        progress.Report("Removing shortcuts...");
        TryDeleteFile(Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory),
            "ClaudeDo.lnk"));
        TryDeleteFile(Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
            "Programs", "ClaudeDo.lnk"));
        TryDeleteFile(AutostartShortcut.PathIn(AutostartShortcut.DefaultStartupDir));

        // 5) Delete install directory. Track success so we can report partial state.
        var failures = new List<string>();
        if (Directory.Exists(_context.InstallDirectory))
        {
            progress.Report($"Deleting {_context.InstallDirectory}...");
            if (!TryDeleteDir(_context.InstallDirectory, out var err))
                failures.Add($"install dir ({_context.InstallDirectory}): {err}");
        }

        // 6) Delete data dir (config + DB + logs) — only if user opted in.
        //    Prefer the manifest-recorded DataDir so a customised DbPath is honoured;
        //    fall back to the default app data root for older manifests.
        if (removeAppData)
        {
            var manifest = InstallManifestStore.TryRead(_context.InstallDirectory);
            var appData = manifest?.DataDir ?? Paths.AppDataRoot();
            if (Directory.Exists(appData))
            {
                progress.Report($"Deleting {appData}...");
                if (!TryDeleteDir(appData, out var err))
                    failures.Add($"app data ({appData}): {err}");
            }
        }

        // 7) If we were launched from inside the install dir (Apps & Features case),
        //    our own exe is still locked — schedule a cmd.exe trampoline to finish
        //    the deletion after this process exits. Best-effort: if this fails the
        //    user is left with an empty <uninstaller> folder which is harmless.
        var runningExe = Environment.ProcessPath;
        if (runningExe is not null
            && IsInsideDirectory(runningExe, _context.InstallDirectory)
            && Directory.Exists(_context.InstallDirectory))
        {
            progress.Report("Scheduling final cleanup after exit...");
            TryScheduleTrampolineDelete(_context.InstallDirectory);
            // The trampoline will finish the job — clear the residual failure entry for the install dir.
            failures.RemoveAll(f => f.StartsWith("install dir"));
        }

        if (failures.Count > 0)
        {
            return StepResult.Fail(
                "Uninstall partially succeeded — the following could not be removed:\n  " +
                string.Join("\n  ", failures));
        }

        progress.Report("Uninstall complete.");
        return StepResult.Ok();
    }

    private static bool IsInsideDirectory(string filePath, string directory)
    {
        try
        {
            var full = Path.GetFullPath(filePath);
            var dir = Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
            return full.StartsWith(dir, StringComparison.OrdinalIgnoreCase);
        }
        catch { return false; }
    }

    private static void TryScheduleTrampolineDelete(string installDir)
    {
        try
        {
            var pid = Environment.ProcessId;
            // Wait for this process to exit, then recursively remove the install dir.
            // /B timeout avoids a visible window; ping as a portable sleep; rmdir /S /Q is silent.
            var cmd = $"/C start \"\" /MIN cmd /C \"ping 127.0.0.1 -n 3 >nul & rmdir /S /Q \"\"{installDir}\"\"\"";
            Process.Start(new ProcessStartInfo
            {
                FileName = "cmd.exe",
                Arguments = cmd,
                UseShellExecute = false,
                CreateNoWindow = true,
                WindowStyle = ProcessWindowStyle.Hidden,
            });
        }
        catch { /* best effort */ }
    }

    /// <summary>
    /// Guards against catastrophic recursive-delete paths. The install dir must be
    /// a non-root directory with a non-empty name (e.g. reject "", "C:\\", "/").
    /// </summary>
    private static bool IsSafeInstallDir(string path, out string reason)
    {
        if (string.IsNullOrWhiteSpace(path))
        {
            reason = "install directory is empty";
            return false;
        }

        string full;
        try { full = Path.GetFullPath(path); }
        catch (Exception ex)
        {
            reason = $"install directory is not a valid path: {ex.Message}";
            return false;
        }

        var name = Path.GetFileName(full.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
        if (string.IsNullOrEmpty(name))
        {
            reason = $"install directory resolves to a drive root ({full})";
            return false;
        }

        reason = "";
        return true;
    }

    private static void TryDeleteFile(string path)
    {
        try { if (File.Exists(path)) File.Delete(path); } catch { /* best effort — single shortcut */ }
    }

    private static bool TryDeleteDir(string path, out string error)
    {
        try
        {
            Directory.Delete(path, recursive: true);
            error = "";
            return true;
        }
        catch (Exception ex)
        {
            error = ex.Message;
            return false;
        }
    }
}
