import { execFile as execFileCallback } from 'node:child_process';
import { rm, writeFile } from 'node:fs/promises';
import { basename, dirname, isAbsolute, join, posix as path, relative, resolve, sep, } from 'node:path';
import { promisify } from 'node:util';
import { CliError } from '../../../errors/cli-error.js';
import { copyDirectory, copySingleFile, dirExists, ensureDir, fileExists, } from '../../../fs/io.js';
const execFileAsync = promisify(execFileCallback);
export const ARCHIVE_SNAPSHOT_METADATA_FILENAME = '.oat-archive-source.json';
/**
 * Directories excluded from S3 archive sync. These contain process artifacts
 * (reviews, PR descriptions) rather than project deliverables.
 */
export const S3_ARCHIVE_SYNC_EXCLUDES = ['reviews/*', 'pr/*'];
function normalizeS3Uri(s3Uri) {
    return s3Uri.trim().replace(/\/+$/, '');
}
/**
 * Build the env passed to every `aws` spawn in this module.
 *
 * Clobber-on-explicit-value merge: a non-empty value in `opts` overwrites the
 * parent env entry. The merge treats an explicitly supplied profile/region as
 * deliberate OAT-archive-scoped intent that wins over ambient shell state —
 * setting `archive.awsProfile` (or passing `--profile`) means "use this for
 * archive ops, regardless of what AWS_PROFILE is in the shell." Empty or
 * whitespace-only values in `opts` are treated as unset and leave the parent
 * env entry alone, so the AWS CLI's own resolution chain (incl. shell
 * AWS_PROFILE) takes over when neither config nor flag has spoken.
 *
 * Exported as a package-internal helper so the archive sync command (which
 * also spawns `aws`) can produce the same env shape without duplicating this
 * logic. This symbol is **not** part of the public package surface — keep
 * usage limited to files inside `commands/project/archive/`.
 */
export function buildAwsEnv(parentEnv, opts) {
    const env = { ...parentEnv };
    const profile = typeof opts.awsProfile === 'string' ? opts.awsProfile.trim() : '';
    if (profile.length > 0) {
        env.AWS_PROFILE = profile;
    }
    const region = typeof opts.awsRegion === 'string' ? opts.awsRegion.trim() : '';
    if (region.length > 0) {
        env.AWS_REGION = region;
    }
    return env;
}
function resolveRepoSlug(repoRoot) {
    return basename(repoRoot).trim().replace(/\s+/g, '-');
}
function buildCompletionWarning(message) {
    return {
        ok: false,
        warnings: [message],
    };
}
function buildSyncError(message) {
    return new CliError(message);
}
function requiresRemoteAccess(options) {
    return Boolean(options.s3Uri &&
        (options.mode === 'sync' || options.syncOnComplete === true));
}
export function buildRepoArchiveS3Uri(s3Uri, repoRoot) {
    return `${normalizeS3Uri(s3Uri)}/${resolveRepoSlug(repoRoot)}/projects`;
}
export function buildProjectArchiveS3Uri(s3Uri, repoRoot, projectKey) {
    return `${buildRepoArchiveS3Uri(s3Uri, repoRoot)}/${projectKey}`;
}
function normalizeArchiveDateStamp(timestamp) {
    const isoPrefix = timestamp
        .trim()
        .match(/^(\d{4})-(\d{2})-(\d{2})/)
        ?.slice(1);
    if (isoPrefix) {
        return isoPrefix.join('');
    }
    const digits = timestamp.replace(/\D/g, '');
    if (digits.length >= 8) {
        return digits.slice(0, 8);
    }
    return new Date().toISOString().slice(0, 10).replace(/-/g, '');
}
export function buildArchiveSnapshotName(projectName, timestamp) {
    return `${normalizeArchiveDateStamp(timestamp)}-${projectName}`;
}
export function parseArchiveSnapshotName(snapshotName) {
    const trimmedSnapshot = snapshotName.replace(/\/+$/, '');
    const match = trimmedSnapshot.match(/^(\d{8})-(.+)$/);
    if (!match) {
        return {
            projectName: trimmedSnapshot,
            snapshotName: trimmedSnapshot,
            dateStamp: null,
        };
    }
    const dateStamp = match[1];
    const projectName = match[2];
    if (!dateStamp || !projectName) {
        return {
            projectName: trimmedSnapshot,
            snapshotName: trimmedSnapshot,
            dateStamp: null,
        };
    }
    return {
        projectName,
        snapshotName: trimmedSnapshot,
        dateStamp,
    };
}
export function resolveLocalArchiveProjectPath(projectsRoot, projectName) {
    const normalizedProjectsRoot = projectsRoot.replace(/\/+$/, '');
    const projectsBase = path.dirname(normalizedProjectsRoot);
    return path.join(projectsBase, 'archived', projectName);
}
function normalizePathForConfig(pathValue) {
    return pathValue.replaceAll('\\', '/');
}
function isInsidePath(parentPath, childPath) {
    const relativePath = relative(parentPath, childPath);
    return (relativePath === '' ||
        (!relativePath.startsWith(`..${sep}`) &&
            relativePath !== '..' &&
            !isAbsolute(relativePath)));
}
function resolveArchiveProjectPath(repoRoot, projectsRoot, projectName) {
    const archiveProjectPath = resolveLocalArchiveProjectPath(projectsRoot, projectName);
    if (!isAbsolute(archiveProjectPath)) {
        return archiveProjectPath;
    }
    const resolvedRepoRoot = resolve(repoRoot);
    const resolvedArchiveProjectPath = resolve(archiveProjectPath);
    if (!isInsidePath(resolvedRepoRoot, resolvedArchiveProjectPath)) {
        return resolvedArchiveProjectPath;
    }
    return normalizePathForConfig(relative(resolvedRepoRoot, resolvedArchiveProjectPath));
}
function resolveCompletionArchivePath(archiveRepoRoot, archiveProjectPath) {
    return isAbsolute(archiveProjectPath)
        ? archiveProjectPath
        : join(archiveRepoRoot, archiveProjectPath);
}
function resolveGitPath(repoRoot, gitPath) {
    const normalizedPath = gitPath.trim();
    return isAbsolute(normalizedPath)
        ? normalizedPath
        : join(repoRoot, normalizedPath);
}
function isExitCode(error, code) {
    return (typeof error === 'object' &&
        error !== null &&
        'code' in error &&
        Number(error.code) === code);
}
// archiveProjectPath must be the contents-level probe path
// (.oat/projects/archived/<projectName>), not the archive directory itself
// (.oat/projects/archived). A `.oat/projects/archived/**` gitignore pattern
// leaves the directory visible in the tree while ignoring every file placed
// inside — a directory-level check reports "not ignored" and produces the
// inverse of the intended durability decision. Keep the probe inside the
// directory; the `oat-project-complete` skill documents the same invariant.
async function isGitignoredArchivePath(repoRoot, archiveProjectPath, dependencies) {
    const execFile = dependencies.gitExecFile ?? execFileAsync;
    try {
        await execFile('git', ['check-ignore', '--quiet', '--no-index', archiveProjectPath], {
            cwd: repoRoot,
            env: dependencies.env ?? process.env,
        });
        return true;
    }
    catch (error) {
        if (isExitCode(error, 1)) {
            return false;
        }
        throw error;
    }
}
async function resolvePrimaryRepoRootResolution(repoRoot, dependencies = {}) {
    const execFile = dependencies.gitExecFile ?? execFileAsync;
    try {
        const [{ stdout: commonDir }, { stdout: gitDir }] = await Promise.all([
            execFile('git', ['rev-parse', '--git-common-dir'], {
                cwd: repoRoot,
                env: dependencies.env ?? process.env,
            }),
            execFile('git', ['rev-parse', '--git-dir'], {
                cwd: repoRoot,
                env: dependencies.env ?? process.env,
            }),
        ]);
        const resolvedCommonDir = resolveGitPath(repoRoot, commonDir);
        const resolvedGitDir = resolveGitPath(repoRoot, gitDir);
        if (resolvedCommonDir === resolvedGitDir) {
            return { repoRoot, available: true };
        }
        const primaryRepoRoot = dirname(resolvedCommonDir);
        const directoryExists = dependencies.dirExists ?? dirExists;
        if (await directoryExists(primaryRepoRoot)) {
            return { repoRoot: primaryRepoRoot, available: true };
        }
        return { repoRoot: primaryRepoRoot, available: false };
    }
    catch {
        return { repoRoot, available: false };
    }
}
export async function resolvePrimaryRepoRoot(repoRoot, dependencies = {}) {
    const resolution = await resolvePrimaryRepoRootResolution(repoRoot, dependencies);
    return resolution.available ? resolution.repoRoot : repoRoot;
}
async function resolveUniqueArchivePath(archivePath, dependencies) {
    const directoryExists = dependencies.dirExists ?? dirExists;
    if (!(await directoryExists(archivePath))) {
        return archivePath;
    }
    const timestamp = dependencies.timestamp?.() ?? new Date().toISOString();
    const suffix = timestamp.replace(/[-:TZ.]/g, '').slice(0, 15);
    return `${archivePath}-${suffix}`;
}
function buildLocalOnlyArchiveWarning(projectName, archiveProjectPath, primaryRepoRoot) {
    const primaryMessage = primaryRepoRoot
        ? `the primary checkout \`${primaryRepoRoot}\` is unavailable`
        : 'the primary checkout could not be resolved';
    return `Refusing to archive project \`${projectName}\` because \`${archiveProjectPath}\` is gitignored in this worktree and ${primaryMessage}. Run \`oat project archive\` from the primary checkout or restore that checkout before retrying.`;
}
export async function resolveArchiveProjectTarget(options, dependencies = {}) {
    const archiveProjectPath = resolveArchiveProjectPath(options.repoRoot, options.projectsRoot, options.projectName);
    let archivePathIsGitignored = false;
    let archiveRepoRoot = options.repoRoot;
    let primaryRepoRoot = null;
    let primaryRepoRootAvailable = true;
    let localOnlyWarning = null;
    try {
        archivePathIsGitignored = await isGitignoredArchivePath(options.repoRoot, archiveProjectPath, dependencies);
    }
    catch {
        archivePathIsGitignored = false;
    }
    if (archivePathIsGitignored) {
        const primaryResolution = await resolvePrimaryRepoRootResolution(options.repoRoot, dependencies);
        primaryRepoRoot = primaryResolution.repoRoot;
        primaryRepoRootAvailable = primaryResolution.available;
        if (primaryResolution.available) {
            archiveRepoRoot = primaryResolution.repoRoot;
        }
        else {
            localOnlyWarning = buildLocalOnlyArchiveWarning(options.projectName, archiveProjectPath, primaryRepoRoot);
        }
    }
    const archiveBasePath = resolveCompletionArchivePath(archiveRepoRoot, archiveProjectPath);
    const archivePath = await resolveUniqueArchivePath(archiveBasePath, {
        dirExists: dependencies.dirExists,
        timestamp: dependencies.timestamp,
    });
    return {
        archiveProjectPath,
        archiveRepoRoot,
        archivePath,
        archivePathIsGitignored,
        primaryRepoRoot,
        primaryRepoRootAvailable,
        localOnlyWarning,
    };
}
export function assertDurableArchiveProjectTarget(target) {
    if (target.localOnlyWarning) {
        throw new CliError(target.localOnlyWarning);
    }
}
async function writeArchiveSnapshotMetadata(archivePath, metadata) {
    await writeFile(join(archivePath, ARCHIVE_SNAPSHOT_METADATA_FILENAME), `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
}
async function exportProjectSummary(archivePath, snapshotName, summaryExportPath, repoRoot, dependencies) {
    const summarySource = join(archivePath, 'summary.md');
    const exists = dependencies.fileExists ?? fileExists;
    if (!(await exists(summarySource))) {
        return null;
    }
    const summaryTarget = join(repoRoot, summaryExportPath, `${snapshotName}.md`);
    const copySummary = dependencies.copySingleFile ?? copySingleFile;
    await copySummary(summarySource, summaryTarget);
    return summaryTarget;
}
export async function archiveProjectOnCompletion(options, dependencies = {}) {
    const makeDir = dependencies.ensureDir ?? ensureDir;
    const copyProjectDirectory = dependencies.copyDirectory ?? copyDirectory;
    const removePath = dependencies.removePath ??
        (async (target, removeOptions) => rm(target, removeOptions));
    const ensureAccess = dependencies.ensureS3ArchiveAccess ?? ensureS3ArchiveAccess;
    const execFile = dependencies.execFile ?? execFileAsync;
    const timestamp = dependencies.timestamp?.() ?? new Date().toISOString();
    const snapshotName = buildArchiveSnapshotName(options.projectName, timestamp);
    const archiveTarget = await resolveArchiveProjectTarget({
        repoRoot: options.repoRoot,
        projectsRoot: options.projectsRoot,
        projectName: options.projectName,
    }, dependencies);
    assertDurableArchiveProjectTarget(archiveTarget);
    const archivePath = archiveTarget.archivePath;
    await makeDir(dirname(archivePath));
    await copyProjectDirectory(options.projectPath, archivePath);
    await writeArchiveSnapshotMetadata(archivePath, {
        projectName: options.projectName,
        snapshotName,
    });
    await removePath(options.projectPath, { recursive: true, force: true });
    const warnings = [];
    let summaryExportFile = null;
    if (options.summaryExportPath) {
        try {
            summaryExportFile = await exportProjectSummary(archivePath, snapshotName, options.summaryExportPath, options.repoRoot, dependencies);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            warnings.push(`Summary export to \`${options.summaryExportPath}\` failed: ${message}`);
        }
    }
    let s3Path = null;
    if (options.s3Uri && options.s3SyncOnComplete) {
        const access = await ensureAccess({
            mode: 'completion',
            s3Uri: options.s3Uri,
            syncOnComplete: options.s3SyncOnComplete,
            awsProfile: options.awsProfile,
            awsRegion: options.awsRegion,
        }, {
            execFile,
            env: dependencies.env,
        });
        warnings.push(...access.warnings);
        if (access.ok) {
            const remoteRepoRoot = await resolvePrimaryRepoRoot(options.repoRoot, dependencies);
            s3Path = buildProjectArchiveS3Uri(options.s3Uri, remoteRepoRoot, snapshotName);
            try {
                const syncArgs = ['s3', 'sync', archivePath, s3Path];
                for (const pattern of S3_ARCHIVE_SYNC_EXCLUDES) {
                    syncArgs.push('--exclude', pattern);
                }
                await execFile('aws', syncArgs, {
                    cwd: options.repoRoot,
                    env: buildAwsEnv(dependencies.env ?? process.env, {
                        awsProfile: options.awsProfile,
                        awsRegion: options.awsRegion,
                    }),
                });
            }
            catch (error) {
                const message = error instanceof Error ? error.message : String(error);
                warnings.push(`Archive S3 sync to \`${s3Path}\` failed: ${message}`);
                s3Path = null;
            }
        }
    }
    return {
        archivePath,
        s3Path,
        summaryExportFile,
        warnings,
    };
}
export async function ensureS3ArchiveAccess(options, dependencies = {}) {
    if (!requiresRemoteAccess(options)) {
        return { ok: true, warnings: [] };
    }
    const execFile = dependencies.execFile ?? execFileAsync;
    const execOptions = {
        env: buildAwsEnv(dependencies.env ?? process.env, {
            awsProfile: options.awsProfile,
            awsRegion: options.awsRegion,
        }),
    };
    try {
        await execFile('aws', ['--version'], execOptions);
    }
    catch (error) {
        if (typeof error === 'object' &&
            error !== null &&
            'code' in error &&
            error.code === 'ENOENT') {
            if (options.mode === 'completion') {
                return buildCompletionWarning('Archive S3 sync is enabled via `archive.s3SyncOnComplete`, but AWS CLI was not found on PATH. Skipping S3 archive sync.');
            }
            throw buildSyncError('AWS CLI is required for `oat repo archive sync`, but it was not found on PATH. Install `aws` and retry.');
        }
        throw error;
    }
    try {
        await execFile('aws', ['sts', 'get-caller-identity'], execOptions);
        return { ok: true, warnings: [] };
    }
    catch {
        if (options.mode === 'completion') {
            return buildCompletionWarning('Archive S3 sync is enabled via `archive.s3SyncOnComplete` and `archive.s3Uri`, but AWS CLI is not configured for access. Skipping S3 archive sync.');
        }
        throw buildSyncError('AWS CLI is required for `oat repo archive sync`, but it is not configured for access to `archive.s3Uri`. Configure AWS credentials or profile settings and retry.');
    }
}
