import * as fs from 'node:fs';
import * as path from 'node:path';
import { execFileSync } from 'node:child_process';
import { createLogger, isRealPathInside } from '../../shared/utils/index.js';
import { resolveConfigValue } from '../config/index.js';
import { localBranchExists, localBranchExistsAbortable, remoteBranchExists, remoteBranchExistsAbortable, resolveBaseBranch as resolveBaseBranchInternal, resolveBaseBranchAbortable, } from './clone-base-branch.js';
import { cloneAndIsolate, cloneAndIsolateAbortable, fetchRemoteBranchIntoIsolatedClone, fetchRemoteBranchIntoIsolatedCloneAbortable, resolveCloneSubmoduleOptions, runGitCommandAbortable, } from './clone-exec.js';
import { loadCloneMeta, removeCloneMeta as removeCloneMetaFile, saveCloneMeta as saveCloneMetaFile } from './clone-meta.js';
import { syncProjectLocalTaktForRetry } from './projectLocalTaktSync.js';
export { branchExists, createBaseBranchIfMissing, localBranchExists, remoteBranchExists, } from './clone-base-branch.js';
const log = createLogger('clone');
export class CloneManager {
    static generateTimestamp() {
        return new Date().toISOString().replace(/[-:.]/g, '').slice(0, 13);
    }
    static resolveCloneBaseDir(projectDir) {
        const worktreeDir = resolveConfigValue(projectDir, 'worktreeDir');
        if (worktreeDir) {
            return path.isAbsolute(worktreeDir)
                ? worktreeDir
                : path.resolve(projectDir, worktreeDir);
        }
        const defaultDir = path.join(projectDir, '..', 'takt-worktrees');
        if (!CloneManager.isParentWritable(defaultDir)) {
            log.info('Parent directory not writable, using fallback clone base dir', {
                defaultDir,
                fallback: path.join(projectDir, '.takt', 'worktrees'),
            });
            return path.join(projectDir, '.takt', 'worktrees');
        }
        return defaultDir;
    }
    static isParentWritable(targetDir) {
        const parentDir = path.dirname(targetDir);
        try {
            fs.accessSync(parentDir, fs.constants.W_OK);
            return true;
        }
        catch {
            return false;
        }
    }
    static resolveClonePath(projectDir, options) {
        const timestamp = CloneManager.generateTimestamp();
        const slug = options.taskSlug;
        let dirName;
        if (options.issueNumber !== undefined && slug) {
            dirName = `${timestamp}-${options.issueNumber}-${slug}`;
        }
        else if (slug) {
            dirName = `${timestamp}-${slug}`;
        }
        else {
            dirName = timestamp;
        }
        if (typeof options.worktree === 'string') {
            return path.isAbsolute(options.worktree)
                ? options.worktree
                : path.resolve(projectDir, options.worktree);
        }
        return CloneManager.resolveAvailableClonePath(CloneManager.resolveCloneBaseDir(projectDir), dirName);
    }
    static resolveAvailableClonePath(baseDir, dirName) {
        const firstCandidate = path.join(baseDir, dirName);
        if (!fs.existsSync(firstCandidate)) {
            return firstCandidate;
        }
        for (let suffix = 2; suffix <= 100; suffix += 1) {
            const candidate = path.join(baseDir, `${dirName}-${suffix}`);
            if (!fs.existsSync(candidate)) {
                return candidate;
            }
        }
        throw new Error(`Unable to allocate clone path in ${baseDir} for ${dirName}`);
    }
    static resolveBranchName(options) {
        if (options.branch) {
            return options.branch;
        }
        const slug = options.taskSlug;
        if (options.issueNumber !== undefined && slug) {
            return `takt/${options.issueNumber}/${slug}`;
        }
        const timestamp = CloneManager.generateTimestamp();
        return slug ? `takt/${timestamp}-${slug}` : `takt/${timestamp}`;
    }
    static resolveBaseBranch(projectDir, explicitBaseBranch) {
        return resolveBaseBranchInternal(projectDir, explicitBaseBranch);
    }
    createSharedClone(projectDir, options) {
        const clonePath = CloneManager.resolveClonePath(projectDir, options);
        const branch = CloneManager.resolveBranchName(options);
        const cloneSubmoduleOptions = resolveCloneSubmoduleOptions(projectDir);
        log.info(`Creating shared clone (${cloneSubmoduleOptions.label}, targets: ${cloneSubmoduleOptions.targets})`, { path: clonePath, branch });
        try {
            execFileSync('git', ['fetch', 'origin', branch], {
                cwd: projectDir,
                stdio: 'pipe',
            });
        }
        catch {
            log.info('Failed to prefetch branch from origin, continuing', {
                branch,
            });
        }
        if (remoteBranchExists(projectDir, branch)) {
            cloneAndIsolate(projectDir, clonePath);
            fetchRemoteBranchIntoIsolatedClone(projectDir, clonePath, branch);
            execFileSync('git', ['checkout', branch], { cwd: clonePath, stdio: 'pipe' });
        }
        else if (localBranchExists(projectDir, branch)) {
            cloneAndIsolate(projectDir, clonePath, branch);
        }
        else {
            const { branch: baseBranch, fetchedCommit } = CloneManager.resolveBaseBranch(projectDir, options.baseBranch);
            cloneAndIsolate(projectDir, clonePath, baseBranch);
            if (fetchedCommit) {
                execFileSync('git', ['reset', '--hard', fetchedCommit], { cwd: clonePath, stdio: 'pipe' });
            }
            execFileSync('git', ['checkout', '-b', branch], { cwd: clonePath, stdio: 'pipe' });
        }
        syncProjectLocalTaktForRetry(projectDir, clonePath);
        this.saveCloneMeta(projectDir, branch, clonePath);
        log.info('Clone created', { path: clonePath, branch });
        return { path: clonePath, branch };
    }
    async createSharedCloneAbortable(projectDir, options, abortSignal) {
        const clonePath = CloneManager.resolveClonePath(projectDir, options);
        const branch = CloneManager.resolveBranchName(options);
        const cloneSubmoduleOptions = resolveCloneSubmoduleOptions(projectDir);
        log.info(`Creating shared clone (${cloneSubmoduleOptions.label}, targets: ${cloneSubmoduleOptions.targets})`, { path: clonePath, branch });
        try {
            await runGitCommandAbortable(projectDir, ['fetch', 'origin', branch], abortSignal);
        }
        catch {
            log.info('Failed to prefetch branch from origin, continuing', {
                branch,
            });
        }
        if (await remoteBranchExistsAbortable(projectDir, branch, abortSignal)) {
            await cloneAndIsolateAbortable(projectDir, clonePath, undefined, abortSignal);
            await fetchRemoteBranchIntoIsolatedCloneAbortable(projectDir, clonePath, branch, abortSignal);
            await runGitCommandAbortable(clonePath, ['checkout', branch], abortSignal);
        }
        else if (await localBranchExistsAbortable(projectDir, branch, abortSignal)) {
            await cloneAndIsolateAbortable(projectDir, clonePath, branch, abortSignal);
        }
        else {
            const { branch: baseBranch, fetchedCommit } = await resolveBaseBranchAbortable(projectDir, options.baseBranch, abortSignal);
            await cloneAndIsolateAbortable(projectDir, clonePath, baseBranch, abortSignal);
            if (fetchedCommit) {
                await runGitCommandAbortable(clonePath, ['reset', '--hard', fetchedCommit], abortSignal);
            }
            await runGitCommandAbortable(clonePath, ['checkout', '-b', branch], abortSignal);
        }
        syncProjectLocalTaktForRetry(projectDir, clonePath);
        this.saveCloneMeta(projectDir, branch, clonePath);
        log.info('Clone created', { path: clonePath, branch });
        return { path: clonePath, branch };
    }
    createTempCloneForBranch(projectDir, branch) {
        CloneManager.resolveBaseBranch(projectDir);
        const timestamp = CloneManager.generateTimestamp();
        const clonePath = CloneManager.resolveAvailableClonePath(CloneManager.resolveCloneBaseDir(projectDir), `tmp-${timestamp}`);
        log.info('Creating temp clone for branch', { path: clonePath, branch });
        cloneAndIsolate(projectDir, clonePath, branch);
        syncProjectLocalTaktForRetry(projectDir, clonePath);
        this.saveCloneMeta(projectDir, branch, clonePath);
        log.info('Temp clone created', { path: clonePath, branch });
        return { path: clonePath, branch };
    }
    removeClone(clonePath) {
        log.info('Removing clone', { path: clonePath });
        try {
            fs.rmSync(clonePath, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
            log.info('Clone removed', { path: clonePath });
        }
        catch (err) {
            log.error('Failed to remove clone', { path: clonePath, error: String(err) });
        }
    }
    saveCloneMeta(projectDir, branch, clonePath) {
        saveCloneMetaFile(projectDir, branch, clonePath);
    }
    removeCloneMeta(projectDir, branch) {
        removeCloneMetaFile(projectDir, branch);
    }
    cleanupOrphanedClone(projectDir, branch) {
        const meta = loadCloneMeta(projectDir, branch);
        if (!meta) {
            this.removeCloneMeta(projectDir, branch);
            return;
        }
        const cloneBaseDir = path.resolve(CloneManager.resolveCloneBaseDir(projectDir));
        const resolvedClonePath = path.resolve(meta.clonePath);
        if (!isRealPathInside(cloneBaseDir, resolvedClonePath)) {
            log.error('Refusing to remove clone outside of clone base directory', {
                branch,
                clonePath: meta.clonePath,
                cloneBaseDir,
            });
            return;
        }
        if (fs.existsSync(resolvedClonePath)) {
            this.removeClone(resolvedClonePath);
            log.info('Orphaned clone cleaned up', { branch, clonePath: resolvedClonePath });
        }
        this.removeCloneMeta(projectDir, branch);
    }
}
const defaultManager = new CloneManager();
export function createSharedClone(projectDir, options) {
    return defaultManager.createSharedClone(projectDir, options);
}
export function createSharedCloneAbortable(projectDir, options, abortSignal) {
    return defaultManager.createSharedCloneAbortable(projectDir, options, abortSignal);
}
export function createTempCloneForBranch(projectDir, branch) {
    return defaultManager.createTempCloneForBranch(projectDir, branch);
}
export function removeClone(clonePath) {
    defaultManager.removeClone(clonePath);
}
export function saveCloneMeta(projectDir, branch, clonePath) {
    defaultManager.saveCloneMeta(projectDir, branch, clonePath);
}
export function removeCloneMeta(projectDir, branch) {
    defaultManager.removeCloneMeta(projectDir, branch);
}
export function cleanupOrphanedClone(projectDir, branch) {
    defaultManager.cleanupOrphanedClone(projectDir, branch);
}
export function resolveBaseBranch(projectDir, explicitBaseBranch) {
    return CloneManager.resolveBaseBranch(projectDir, explicitBaseBranch);
}
export function resolveCloneBaseDir(projectDir) {
    return CloneManager.resolveCloneBaseDir(projectDir);
}
//# sourceMappingURL=clone.js.map