/**
 * NERF External State Migration
 *
 * Migrates legacy in-project `.nerf/` directories to the external
 * `~/.nerf/projects/<hash>/` state directory. After migration, a
 * symlink replaces the original directory so all paths remain valid.
 */
import { execFileSync } from "node:child_process";
import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
import { join } from "node:path";
import { externalNerfRoot } from "./repo-identity.js";
import { getErrorMessage } from "./error-utils.js";
import { hasGitTrackedNerfFiles } from "./gitignore.js";
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
/**
 * Migrate a legacy in-project `.nerf/` directory to external storage.
 *
 * Algorithm:
 * 1. If `<project>/.nerf` is a symlink or doesn't exist -> skip
 * 2. If `<project>/.nerf` is a real directory:
 *    a. Compute external path from repoIdentity
 *    b. mkdir -p external dir
 *    c. Rename `.nerf` -> `.nerf.migrating` (atomic on same FS, acts as lock)
 *    d. Copy contents to external dir (skip `worktrees/` subdirectory)
 *    e. Create symlink `.nerf -> external path`
 *    f. Remove `.nerf.migrating`
 * 3. On failure: rename `.nerf.migrating` back to `.nerf` (rollback)
 */
export function migrateToExternalState(basePath) {
    const localNerf = join(basePath, ".nerf");
    // Skip if doesn't exist
    if (!existsSync(localNerf)) {
        return { migrated: false };
    }
    // Skip if already a symlink
    try {
        const stat = lstatSync(localNerf);
        if (stat.isSymbolicLink()) {
            return { migrated: false };
        }
        if (!stat.isDirectory()) {
            return { migrated: false, error: ".nerf exists but is not a directory or symlink" };
        }
    }
    catch (err) {
        return { migrated: false, error: `Cannot stat .nerf: ${getErrorMessage(err)}` };
    }
    // Skip if .nerf/ contains git-tracked files — the project intentionally
    // keeps .nerf/ in version control and migration would destroy that.
    if (hasGitTrackedNerfFiles(basePath)) {
        return { migrated: false };
    }
    // Skip if .nerf/worktrees/ has active worktree directories (#1337).
    // On Windows, active git worktrees hold OS-level directory handles that
    // prevent rename/delete. Attempting migration causes EBUSY and data loss.
    const worktreesDir = join(localNerf, "worktrees");
    if (existsSync(worktreesDir)) {
        try {
            const entries = readdirSync(worktreesDir, { withFileTypes: true });
            if (entries.some(e => e.isDirectory())) {
                return { migrated: false };
            }
        }
        catch {
            // Can't read worktrees dir — skip migration to be safe
            return { migrated: false };
        }
    }
    const externalPath = externalNerfRoot(basePath);
    const migratingPath = join(basePath, ".nerf.migrating");
    try {
        // mkdir -p the external dir
        mkdirSync(externalPath, { recursive: true });
        // Rename .nerf -> .nerf.migrating (atomic lock).
        // On Windows, NTFS may reject rename with EPERM if file descriptors are
        // open (VS Code watchers, antivirus on-access scan). Fall back to
        // copy+delete (#1292).
        try {
            renameSync(localNerf, migratingPath);
        }
        catch (renameErr) {
            if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
                try {
                    cpSync(localNerf, migratingPath, { recursive: true, force: true });
                    rmSync(localNerf, { recursive: true, force: true });
                }
                catch (copyErr) {
                    return { migrated: false, error: `Migration rename/copy failed: ${copyErr instanceof Error ? copyErr.message : String(copyErr)}` };
                }
            }
            else {
                throw renameErr;
            }
        }
        // Copy contents to external dir, skipping worktrees/
        const entries = readdirSync(migratingPath, { withFileTypes: true });
        for (const entry of entries) {
            if (entry.name === "worktrees")
                continue; // worktrees stay local
            const src = join(migratingPath, entry.name);
            const dst = join(externalPath, entry.name);
            try {
                if (entry.isDirectory()) {
                    cpSync(src, dst, { recursive: true, force: true });
                }
                else {
                    cpSync(src, dst, { force: true });
                }
            }
            catch {
                // Non-fatal: continue with other files
            }
        }
        // Create symlink .nerf -> external path
        symlinkSync(externalPath, localNerf, "junction");
        // Verify the symlink resolves correctly before removing the backup (#1377).
        // On Windows, junction creation can silently succeed but resolve to the wrong
        // target, or the external dir may not be accessible. If verification fails,
        // restore from the backup.
        try {
            const resolved = realpathSync(localNerf);
            const resolvedExternal = realpathSync(externalPath);
            if (resolved !== resolvedExternal) {
                // Symlink points to wrong target — restore backup
                try {
                    rmSync(localNerf, { force: true });
                }
                catch { /* may not exist */ }
                renameSync(migratingPath, localNerf);
                return { migrated: false, error: `Migration verification failed: symlink resolves to ${resolved}, expected ${resolvedExternal}` };
            }
            // Verify we can read through the symlink
            readdirSync(localNerf);
        }
        catch (verifyErr) {
            // Symlink broken or unreadable — restore backup
            try {
                rmSync(localNerf, { force: true });
            }
            catch { /* may not exist */ }
            try {
                renameSync(migratingPath, localNerf);
            }
            catch { /* best-effort restore */ }
            return { migrated: false, error: `Migration verification failed: ${getErrorMessage(verifyErr)}` };
        }
        // Clean the git index — any .nerf/* files tracked before migration now
        // sit behind the symlink and git can't follow it, causing them to show
        // as deleted. Remove them from the index so the working tree stays clean.
        // --ignore-unmatch makes this a no-op on fresh projects with no tracked .nerf/.
        try {
            execFileSync("git", ["rm", "-r", "--cached", "--ignore-unmatch", ".nerf"], {
                cwd: basePath,
                stdio: ["ignore", "pipe", "ignore"],
                env: GIT_NO_PROMPT_ENV,
                timeout: 10_000,
            });
        }
        catch {
            // Non-fatal — git may be unavailable or nothing was tracked
        }
        // Remove .nerf.migrating only after symlink is verified and index is clean
        rmSync(migratingPath, { recursive: true, force: true });
        return { migrated: true };
    }
    catch (err) {
        // Rollback: rename .nerf.migrating back to .nerf-rollback
        try {
            if (existsSync(migratingPath) && !existsSync(localNerf)) {
                renameSync(migratingPath, localNerf);
            }
        }
        catch {
            // Rollback failed -- leave .nerf.migrating for doctor to detect
        }
        return {
            migrated: false,
            error: `Migration failed: ${getErrorMessage(err)}`,
        };
    }
}
/**
 * Recover from a failed migration (`.nerf.migrating` exists).
 * Moves `.nerf.migrating` back to `.nerf` if `.nerf` doesn't exist.
 */
export function recoverFailedMigration(basePath) {
    const localNerf = join(basePath, ".nerf");
    const migratingPath = join(basePath, ".nerf.migrating");
    if (!existsSync(migratingPath))
        return false;
    if (existsSync(localNerf))
        return false; // both exist -- ambiguous, don't touch
    try {
        renameSync(migratingPath, localNerf);
        return true;
    }
    catch {
        return false;
    }
}
