namespace ClaudeDo.Data;

public static class Paths
{
    /// <summary>
    /// Expands a leading "~" or "%USERPROFILE%" and returns an absolute path.
    /// Relative paths are resolved against <paramref name="baseDir"/> (default: current directory).
    /// </summary>
    public static string Expand(string path, string? baseDir = null)
    {
        if (string.IsNullOrWhiteSpace(path))
            throw new ArgumentException("Path must not be empty.", nameof(path));

        var expanded = System.Environment.ExpandEnvironmentVariables(path);

        if (expanded.StartsWith("~", StringComparison.Ordinal))
        {
            var home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
            expanded = home + expanded[1..];
        }

        if (!Path.IsPathRooted(expanded))
            expanded = Path.GetFullPath(expanded, baseDir ?? System.Environment.CurrentDirectory);

        return Path.GetFullPath(expanded);
    }

    /// <summary>
    /// Strips trailing directory separators off a path bound for a Windows command line. Windows
    /// argv rules read <c>\"</c> as an escaped quote, so a quoted token ending in '\' never closes
    /// (<c>"C:\repo\"</c> parses as <c>C:\repo"</c>) and either the path itself or every argument
    /// after it is corrupted. A bare root ("C:\", "/") is all separator and is returned untouched;
    /// so is null/blank.
    /// </summary>
    public static string? TrimTrailingSeparator(string? path)
    {
        if (string.IsNullOrWhiteSpace(path))
            return path;

        var trimmed = path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
        return trimmed.Length == 0 || trimmed.EndsWith(':') ? path : trimmed;
    }

    private const string DirName = ".claudeDo";
    private const string LegacyDirName = ".todo-app";

    /// <summary>
    /// ~/.claudeDo — parent directory for db, logs, config, sandbox, worktrees.
    /// Falls back to the legacy ~/.todo-app while that one still exists and the new one
    /// doesn't, so a failed <see cref="MigrateLegacyAppDataRoot"/> degrades to "keep using
    /// the old folder" instead of silently starting over on an empty one.
    /// </summary>
    public static string AppDataRoot()
    {
        var root = Expand("~/" + DirName);
        if (!Directory.Exists(root))
        {
            var legacy = Expand("~/" + LegacyDirName);
            if (Directory.Exists(legacy)) return legacy;
        }
        return root;
    }

    /// <summary>
    /// One-time rename of ~/.todo-app to ~/.claudeDo, including the path literals stored
    /// inside worker.config.json / ui.config.json. No-op once the new folder exists.
    /// Called from the Worker at startup before anything opens a file under the root —
    /// the single mover, so the UI never races it.
    /// </summary>
    public static void MigrateLegacyAppDataRoot() => MigrateLegacyAppDataRoot(Expand("~"));

    internal static void MigrateLegacyAppDataRoot(string home)
    {
        var root = Path.Combine(home, DirName);
        var legacy = Path.Combine(home, LegacyDirName);
        if (Directory.Exists(root) || !Directory.Exists(legacy)) return;

        // A locked file (e.g. an old worker still holding todo.db) fails the move; AppDataRoot
        // then keeps returning the legacy path and the next start retries.
        try { Directory.Move(legacy, root); }
        catch (IOException) { return; }
        catch (UnauthorizedAccessException) { return; }

        foreach (var cfg in Directory.EnumerateFiles(root, "*.config.json"))
        {
            try { File.WriteAllText(cfg, File.ReadAllText(cfg).Replace(LegacyDirName, DirName)); }
            catch (IOException) { /* best effort — the default already points at the new root */ }
        }
    }
}
