import { randomBytes } from "node:crypto";
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { SessionWorktreeStore } from "./worktree-store.js";
const MAX_GIT_BUFFER = 10 * 1024 * 1024;
const DEFAULT_DIFF_LIMIT_BYTES = 256 * 1024;
const MAX_FILE_PREVIEW_BYTES = 64 * 1024;
export class SessionWorktreeService {
    config;
    store;
    constructor(config, store) {
        this.config = config;
        this.store = store;
    }
    static defaultRoot() {
        return path.join(os.homedir(), ".nordrelay", "worktrees");
    }
    listDashboardSnapshot(sharedSessions = []) {
        const records = this.store.list().map((record) => this.snapshot(record));
        return {
            defaultMode: this.config.sessionWorkspaceMode,
            worktreeRoot: this.config.sessionWorktreeRoot,
            records,
            integrations: this.store.listIntegrations(),
            sharedWarnings: this.sharedWarnings(sharedSessions),
            updatedAt: new Date().toISOString(),
        };
    }
    create(options) {
        const repo = this.inspectRepo(options.sourceWorkspace);
        const id = shortId();
        const repoName = safeSegment(path.basename(repo.root) || "repo");
        const branchName = `${this.config.sessionWorktreeBranchPrefix}/${repoName}/${id}`;
        const worktreePath = path.join(this.config.sessionWorktreeRoot, repoName, id);
        mkdirSync(path.dirname(worktreePath), { recursive: true });
        runGit(["worktree", "add", "-b", branchName, worktreePath, repo.headSha], repo.root);
        const now = new Date().toISOString();
        const record = {
            id,
            mode: "worktree",
            status: "active",
            agentId: options.agentId,
            contextKey: options.contextKey,
            threadId: options.threadId ?? null,
            sourceWorkspace: path.resolve(options.sourceWorkspace),
            repoRoot: repo.root,
            repoName,
            baseSha: repo.headSha,
            baseBranch: repo.branch,
            branchName,
            worktreePath,
            createdAt: now,
            updatedAt: now,
        };
        return this.store.upsert(record);
    }
    fork(options) {
        const record = this.create(options);
        let patchApplied = false;
        let copiedUntrackedFiles = [];
        let skippedUntrackedFiles = [];
        try {
            if (options.includeUncommitted) {
                patchApplied = this.applyTrackedDiff(record);
                const copied = this.copyUntrackedFiles(record);
                copiedUntrackedFiles = copied.copied;
                skippedUntrackedFiles = copied.skipped;
                this.store.patch(record.id, { copiedUntrackedFiles, skippedUntrackedFiles });
            }
            return {
                record: this.store.get(record.id) ?? record,
                copiedUntrackedFiles,
                skippedUntrackedFiles,
                patchApplied,
            };
        }
        catch (error) {
            this.store.patch(record.id, {
                status: "failed",
                lastError: error instanceof Error ? error.message : String(error),
            });
            throw error;
        }
    }
    linkThread(id, threadId, agentId, contextKey) {
        return this.store.patch(id, {
            threadId: threadId ?? null,
            agentId,
            contextKey,
        });
    }
    getByThreadId(threadId) {
        return this.store.findByThreadId(threadId);
    }
    getByWorkspace(workspace) {
        return this.store.findByWorkspace(workspace);
    }
    commit(id, message) {
        const record = this.requireRecord(id);
        const status = gitStatus(record.worktreePath);
        if (status.length === 0) {
            const headSha = gitOutput(["rev-parse", "HEAD"], record.worktreePath);
            return {
                record: this.store.patch(id, {
                    status: record.status === "merged" ? "merged" : "committed",
                    commitSha: headSha,
                    committedAt: record.committedAt ?? new Date().toISOString(),
                    commitMessage: record.commitMessage ?? message,
                    lastError: undefined,
                }),
                clean: true,
                status,
            };
        }
        runGit(["add", "-A"], record.worktreePath);
        const commitMessage = message?.trim() || defaultCommitMessage(record);
        runGit(["commit", "-m", commitMessage], record.worktreePath);
        const commitSha = gitOutput(["rev-parse", "HEAD"], record.worktreePath);
        return {
            record: this.store.patch(id, {
                status: "committed",
                commitSha,
                committedAt: new Date().toISOString(),
                commitMessage,
                lastError: undefined,
            }),
            clean: false,
            status,
        };
    }
    diff(id, limitBytes = DEFAULT_DIFF_LIMIT_BYTES) {
        const record = this.requireRecord(id);
        const exists = existsSync(record.worktreePath);
        if (!exists) {
            throw new Error(`Worktree path does not exist: ${record.worktreePath}`);
        }
        const diff = this.diffText(record);
        const limited = truncateUtf8(diff, Math.max(1_024, limitBytes));
        const files = this.changedFiles(record);
        return {
            record,
            files,
            diff: limited.text,
            structuredFiles: structuredDiffFiles(limited.text, files),
            truncated: limited.truncated,
            byteLength: Buffer.byteLength(diff, "utf8"),
            generatedAt: new Date().toISOString(),
        };
    }
    previewIntegration(ids) {
        const generatedAt = new Date().toISOString();
        const records = ids.map((id) => this.requireRecord(id));
        const warnings = [];
        if (records.length === 0) {
            return { ids, canIntegrate: false, sourceWorktrees: [], files: [], conflictCandidates: [], conflictReview: [], warnings: ["Select at least one worktree."], generatedAt };
        }
        const repoRoot = records[0].repoRoot;
        const repoName = records[0].repoName;
        const baseSha = records[0].baseSha;
        for (const record of records) {
            if (record.repoRoot !== repoRoot || record.baseSha !== baseSha) {
                warnings.push("All selected worktrees must use the same repository and base commit.");
                break;
            }
            if (!record.commitSha) {
                warnings.push(`Worktree ${record.id} has no session commit yet.`);
            }
            if (!existsSync(record.worktreePath)) {
                warnings.push(`Worktree ${record.id} is missing on disk.`);
            }
        }
        const fileMap = new Map();
        for (const record of records) {
            for (const file of this.changedFiles(record)) {
                const existing = fileMap.get(file.path);
                if (existing) {
                    existing.sourceWorktreeIds = [...new Set([...existing.sourceWorktreeIds, record.id])];
                    if (existing.status !== file.status)
                        existing.status = "conflict";
                }
                else {
                    fileMap.set(file.path, { ...file, sourceWorktreeIds: [record.id] });
                }
            }
        }
        const files = [...fileMap.values()].sort((left, right) => left.path.localeCompare(right.path));
        const conflictCandidates = files.filter((file) => file.sourceWorktreeIds.length > 1 || file.status === "conflict");
        if (conflictCandidates.length) {
            warnings.push(`${conflictCandidates.length} file(s) are changed by more than one selected worktree.`);
        }
        const sourceWorktrees = records.map(previewSource);
        const conflictReview = files.map((file) => conflictReviewItem(file, sourceWorktrees, records, repoRoot, baseSha));
        const riskSummary = summarizeWorktreeRisks(conflictReview, warnings);
        return {
            ids,
            canIntegrate: warnings.length === 0,
            repoRoot,
            repoName,
            baseSha,
            sourceWorktrees,
            files,
            conflictCandidates,
            conflictReview,
            riskSummary,
            warnings,
            generatedAt,
        };
    }
    compare(ids) {
        return {
            ids,
            preview: this.previewIntegration(ids),
            diffs: ids.map((id) => this.diff(id)),
            generatedAt: new Date().toISOString(),
        };
    }
    exportIntegrationPatch(ids) {
        const records = ids.map((id) => this.requireRecord(id));
        if (records.length === 0) {
            throw new Error("Select at least one worktree to export patches.");
        }
        const repoRoot = records[0].repoRoot;
        const baseSha = records[0].baseSha;
        const incompatible = records.find((record) => record.repoRoot !== repoRoot || record.baseSha !== baseSha);
        if (incompatible) {
            throw new Error("All selected worktrees must use the same repository and base commit.");
        }
        const missingCommit = records.find((record) => !record.commitSha);
        if (missingCommit) {
            throw new Error(`Worktree ${missingCommit.id} has no session commit yet.`);
        }
        const generatedAt = new Date().toISOString();
        const preview = this.previewIntegration(ids);
        const content = records.map((record, index) => {
            const patch = gitRawOutput(["format-patch", "--stdout", `${record.baseSha}..${record.commitSha}`], record.repoRoot, true).trim();
            const fallback = patch || gitRawOutput(["diff", "--binary", `${record.baseSha}..${record.commitSha}`, "--"], record.repoRoot, true).trim();
            return [
                `# NordRelay worktree patch ${index + 1}/${records.length}`,
                `# Worktree: ${record.id}`,
                `# Branch: ${record.branchName}`,
                `# Commit: ${record.commitSha}`,
                "",
                fallback,
            ].join("\n").trimEnd();
        }).join("\n\n");
        const stamp = generatedAt.replace(/[:.]/g, "-");
        const patchFileName = `nordrelay-worktree-patches-${records[0].repoName}-${stamp}.patch`;
        const summaryFileName = `nordrelay-worktree-summary-${records[0].repoName}-${stamp}.md`;
        const reviewBranch = `${this.config.sessionWorktreeBranchPrefix}/review/${records[0].repoName}/${shortId()}`;
        const summary = integrationExportSummary(preview, records);
        const prTitle = `Integrate NordRelay worktrees for ${records[0].repoName}`;
        const prBody = integrationPrBody(preview, records);
        return {
            fileName: patchFileName,
            content: `${content}\n`,
            worktreeIds: records.map((record) => record.id),
            summaryFileName,
            summary,
            riskReportFileName: `nordrelay-worktree-risk-${records[0].repoName}-${stamp}.json`,
            riskReportJson: `${JSON.stringify({ preview, worktreeIds: records.map((record) => record.id) }, null, 2)}\n`,
            prTitle,
            prBody,
            prCommands: [
                `git checkout -b ${shellQuote(reviewBranch)} ${shellQuote(baseSha)}`,
                `git am ${shellQuote(patchFileName)} || git apply --3way ${shellQuote(patchFileName)}`,
                `git push -u origin ${shellQuote(reviewBranch)}`,
                `gh pr create --draft --title ${shellQuote(prTitle)} --body-file ${shellQuote(summaryFileName)}`,
            ],
            generatedAt,
        };
    }
    updateFromBase(id) {
        const record = this.requireRecord(id);
        if (!existsSync(record.worktreePath)) {
            throw new Error(`Worktree path does not exist: ${record.worktreePath}`);
        }
        const status = gitStatus(record.worktreePath);
        if (status.length > 0) {
            throw new Error("Commit or clean the worktree before updating it from the base branch.");
        }
        const previousBaseSha = record.baseSha;
        const baseRef = record.baseBranch || "HEAD";
        const newBaseSha = gitOutput(["rev-parse", baseRef], record.repoRoot);
        if (newBaseSha === previousBaseSha) {
            return { record, previousBaseSha, newBaseSha, rebased: false, status };
        }
        try {
            runGit(["rebase", newBaseSha], record.worktreePath, undefined, true);
            const headSha = gitOutput(["rev-parse", "HEAD"], record.worktreePath);
            const next = this.store.patch(id, {
                baseSha: newBaseSha,
                commitSha: headSha === newBaseSha ? undefined : headSha,
                status: headSha === newBaseSha ? "active" : "committed",
                lastError: undefined,
            });
            return { record: next, previousBaseSha, newBaseSha, rebased: true, status: gitStatus(record.worktreePath) };
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            const next = this.store.patch(id, { status: "conflict", lastError: message });
            return { record: next, previousBaseSha, newBaseSha, rebased: false, status: gitStatus(record.worktreePath) };
        }
    }
    integrate(ids, options = {}) {
        const records = ids.map((id) => this.requireRecord(id));
        if (records.length === 0) {
            throw new Error("Select at least one worktree to integrate.");
        }
        const repoRoot = records[0].repoRoot;
        const baseSha = records[0].baseSha;
        const incompatible = records.find((record) => record.repoRoot !== repoRoot || record.baseSha !== baseSha);
        if (incompatible) {
            throw new Error("All selected worktrees must use the same repository and base commit.");
        }
        const missingCommit = records.find((record) => !record.commitSha);
        if (missingCommit) {
            throw new Error(`Worktree ${missingCommit.id} has no session commit yet.`);
        }
        const id = `int-${shortId()}`;
        const repoName = records[0].repoName;
        const branchName = `${this.config.sessionWorktreeBranchPrefix}/integration/${repoName}/${id}`;
        const worktreePath = path.join(this.config.sessionWorktreeRoot, repoName, "integrations", id);
        mkdirSync(path.dirname(worktreePath), { recursive: true });
        const now = new Date().toISOString();
        const run = {
            id,
            status: "running",
            repoRoot,
            repoName,
            baseSha,
            baseBranch: records[0].baseBranch,
            branchName,
            worktreePath,
            worktreeIds: records.map((record) => record.id),
            mergedCommitShas: [],
            createdAt: now,
            updatedAt: now,
        };
        this.store.upsertIntegration(run);
        try {
            runGit(["worktree", "add", "-b", branchName, worktreePath, baseSha], repoRoot);
            const mergedCommitShas = [];
            const resolvedConflicts = [];
            for (const record of records) {
                try {
                    runGit(["merge", "--no-ff", "--no-edit", record.commitSha], worktreePath);
                }
                catch (error) {
                    const resolved = this.applyIntegrationResolutions(worktreePath, records, options.resolutions ?? []);
                    if (!resolved.length) {
                        throw error;
                    }
                    resolvedConflicts.push(...resolved);
                    const remaining = unmergedFiles(worktreePath);
                    if (remaining.length > 0) {
                        throw new Error(`Integration has unresolved conflicts: ${remaining.join(", ")}`);
                    }
                    runGit(["commit", "--no-edit"], worktreePath);
                }
                mergedCommitShas.push(record.commitSha);
                this.store.patch(record.id, {
                    status: "merged",
                    mergedAt: new Date().toISOString(),
                    integrationRunId: id,
                    lastError: undefined,
                });
            }
            return this.store.patchIntegration(id, {
                status: "merged",
                mergedCommitShas,
                resolvedConflicts: [...new Set(resolvedConflicts)],
                finishedAt: new Date().toISOString(),
            });
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            const status = gitStatus(worktreePath);
            const conflict = status.some((line) => /^([A-Z?U]{2})\s/.test(line) && line.includes("U"));
            const nextStatus = conflict ? "conflict" : "failed";
            for (const record of records) {
                this.store.patch(record.id, {
                    status: nextStatus,
                    integrationRunId: id,
                    lastError: message,
                });
            }
            return this.store.patchIntegration(id, {
                status: nextStatus,
                lastError: message,
                finishedAt: new Date().toISOString(),
            });
        }
    }
    finalizeIntegration(id, options = {}) {
        const run = this.requireIntegration(id);
        if (run.status === "applied") {
            return {
                run,
                removedIntegrationWorktree: Boolean(run.cleanup?.removedIntegrationWorktree),
                deletedIntegrationBranch: Boolean(run.cleanup?.deletedIntegrationBranch),
                removedSourceWorktrees: [],
            };
        }
        if (run.status !== "merged") {
            throw new Error(`Integration ${id} cannot be finalized while it is ${run.status}. Resolve or recreate it first.`);
        }
        if (!existsSync(run.repoRoot)) {
            throw new Error(`Repository root does not exist: ${run.repoRoot}`);
        }
        const currentBranch = gitOutput(["rev-parse", "--abbrev-ref", "HEAD"], run.repoRoot);
        if (!currentBranch || currentBranch === "HEAD") {
            throw new Error("Open the target repository on a named branch before finalizing the integration.");
        }
        const targetBranch = (options.targetBranch?.trim() || run.targetBranch || run.baseBranch || currentBranch).trim();
        if (currentBranch !== targetBranch) {
            throw new Error(`Finalize requires ${run.repoRoot} to be on ${targetBranch}, but it is on ${currentBranch}. Switch branches and retry.`);
        }
        const status = gitStatus(run.repoRoot);
        if (status.length > 0) {
            throw new Error(`Commit or stash local changes before finalizing the integration. Dirty files: ${status.slice(0, 8).join(", ")}`);
        }
        if (!branchExists(run.repoRoot, run.branchName)) {
            throw new Error(`Integration branch no longer exists: ${run.branchName}`);
        }
        if (!isAncestor(run.repoRoot, run.branchName, "HEAD")) {
            runGit(["merge", "--no-ff", "--no-edit", run.branchName], run.repoRoot);
        }
        const appliedCommitSha = gitOutput(["rev-parse", "HEAD"], run.repoRoot);
        let nextRun = this.store.patchIntegration(id, {
            status: "applied",
            targetBranch,
            appliedCommitSha,
            appliedAt: new Date().toISOString(),
            lastError: undefined,
        });
        let removedIntegrationWorktree = false;
        let deletedIntegrationBranch = false;
        const removedSourceWorktrees = [];
        const warnings = [];
        if (options.removeIntegrationWorktree) {
            try {
                this.removeIntegrationWorktree(nextRun);
                removedIntegrationWorktree = true;
            }
            catch (error) {
                warnings.push(`Failed to remove integration worktree: ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        if (options.removeSourceWorktrees) {
            for (const worktreeId of nextRun.worktreeIds) {
                try {
                    removedSourceWorktrees.push(this.remove(worktreeId, { force: true }));
                }
                catch (error) {
                    warnings.push(`Failed to remove source worktree ${worktreeId}: ${error instanceof Error ? error.message : String(error)}`);
                }
            }
        }
        if (options.deleteIntegrationBranch) {
            try {
                runGit(["branch", "-d", nextRun.branchName], nextRun.repoRoot);
                deletedIntegrationBranch = true;
            }
            catch (error) {
                warnings.push(`Failed to delete integration branch ${nextRun.branchName}: ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        nextRun = this.store.patchIntegration(id, {
            cleanup: {
                ...(nextRun.cleanup ?? {}),
                removedIntegrationWorktree,
                deletedIntegrationBranch,
                removedSourceWorktrees: removedSourceWorktrees.map((record) => record.id),
                warnings,
            },
        });
        return { run: nextRun, removedIntegrationWorktree, deletedIntegrationBranch, removedSourceWorktrees };
    }
    applyIntegrationResolutions(worktreePath, records, resolutions) {
        const unmerged = unmergedFiles(worktreePath);
        if (!unmerged.length || !resolutions.length)
            return [];
        const byPath = new Map(resolutions
            .filter((resolution) => isSafeRelativePath(resolution.path) && resolution.choice !== "auto")
            .map((resolution) => [resolution.path, resolution]));
        const resolved = [];
        for (const relativePath of unmerged) {
            const resolution = byPath.get(relativePath);
            if (!resolution)
                continue;
            this.applyIntegrationResolution(worktreePath, records, relativePath, resolution);
            resolved.push(relativePath);
        }
        return resolved;
    }
    applyIntegrationResolution(worktreePath, records, relativePath, resolution) {
        if (resolution.choice === "manual") {
            const target = path.join(worktreePath, relativePath);
            mkdirSync(path.dirname(target), { recursive: true });
            writeFileSync(target, resolution.content ?? "", "utf8");
            runGit(["add", "--", relativePath], worktreePath);
            return;
        }
        if (resolution.choice === "both") {
            const versions = records
                .filter((record) => record.commitSha)
                .map((record) => ({
                label: record.branchName,
                text: gitOutputSafe(["show", `${record.commitSha}:${relativePath}`], worktreePath, true),
            }))
                .filter((entry) => entry.text !== undefined);
            if (!versions.length) {
                throw new Error(`Cannot build combined conflict resolution for ${relativePath}; no source versions were readable.`);
            }
            const target = path.join(worktreePath, relativePath);
            mkdirSync(path.dirname(target), { recursive: true });
            writeFileSync(target, versions.map((version) => `<<<<<<< ${version.label}\n${version.text.replace(/\s+$/g, "")}\n>>>>>>> ${version.label}`).join("\n\n"), "utf8");
            runGit(["add", "--", relativePath], worktreePath);
            return;
        }
        if (resolution.choice === "theirs" && resolution.sourceWorktreeId) {
            const source = records.find((record) => record.id === resolution.sourceWorktreeId);
            if (!source?.commitSha) {
                throw new Error(`Cannot resolve ${relativePath}; source worktree ${resolution.sourceWorktreeId} has no commit.`);
            }
            runGit(["checkout", source.commitSha, "--", relativePath], worktreePath);
            runGit(["add", "--", relativePath], worktreePath);
            return;
        }
        runGit(["checkout", resolution.choice === "ours" ? "--ours" : "--theirs", "--", relativePath], worktreePath);
        runGit(["add", "--", relativePath], worktreePath);
    }
    remove(id, options = {}) {
        const record = this.requireRecord(id);
        try {
            runGit(["worktree", "remove", options.force ? "--force" : "", record.worktreePath].filter(Boolean), record.repoRoot);
        }
        catch (error) {
            if (!options.force) {
                throw error;
            }
            rmSync(record.worktreePath, { recursive: true, force: true });
        }
        return this.store.patch(id, { status: "removed" });
    }
    removeIntegrationWorktree(run) {
        if (!existsSync(run.worktreePath)) {
            return;
        }
        try {
            runGit(["worktree", "remove", "--force", run.worktreePath], run.repoRoot);
        }
        catch {
            rmSync(run.worktreePath, { recursive: true, force: true });
        }
    }
    cleanup() {
        const removedRecords = [];
        const removedIntegrations = [];
        const prunedRepositories = [];
        const warnings = [];
        const repositories = new Set();
        for (const record of this.store.list()) {
            repositories.add(record.repoRoot);
            const missing = !existsSync(record.worktreePath);
            if (record.status === "removed" || missing) {
                if (missing && record.status !== "removed") {
                    warnings.push(`Removed stale record for missing worktree ${record.id}.`);
                }
                if (this.store.delete(record.id))
                    removedRecords.push(record.id);
            }
        }
        for (const run of this.store.listIntegrations()) {
            repositories.add(run.repoRoot);
            const terminal = run.status === "merged" || run.status === "applied" || run.status === "failed";
            if (terminal && !existsSync(run.worktreePath)) {
                if (this.store.deleteIntegration(run.id))
                    removedIntegrations.push(run.id);
            }
        }
        for (const repoRoot of repositories) {
            try {
                runGit(["worktree", "prune"], repoRoot);
                prunedRepositories.push(repoRoot);
            }
            catch (error) {
                warnings.push(`Failed to prune ${repoRoot}: ${error instanceof Error ? error.message : String(error)}`);
            }
        }
        return {
            removedRecords,
            removedIntegrations,
            prunedRepositories,
            warnings,
            cleanedAt: new Date().toISOString(),
        };
    }
    snapshot(record) {
        const exists = existsSync(record.worktreePath);
        const status = exists ? gitStatus(record.worktreePath) : [];
        const headSha = exists ? gitOutputSafe(["rev-parse", "HEAD"], record.worktreePath) : undefined;
        const dirty = status.length > 0;
        return {
            ...record,
            exists,
            dirty,
            headSha,
            gitStatus: status,
            statusText: statusText(record, exists, dirty),
        };
    }
    requireRecord(id) {
        const record = this.store.get(id);
        if (!record) {
            throw new Error(`Unknown session worktree: ${id}`);
        }
        return record;
    }
    requireIntegration(id) {
        const run = this.store.getIntegration(id);
        if (!run) {
            throw new Error(`Unknown worktree integration run: ${id}`);
        }
        return run;
    }
    inspectRepo(workspace) {
        const cwd = path.resolve(workspace);
        const root = gitOutput(["rev-parse", "--show-toplevel"], cwd);
        const headSha = gitOutput(["rev-parse", "HEAD"], root);
        const branch = gitOutputSafe(["rev-parse", "--abbrev-ref", "HEAD"], root);
        return {
            root,
            headSha,
            branch: branch && branch !== "HEAD" ? branch : undefined,
        };
    }
    applyTrackedDiff(record) {
        const patch = gitRawOutput(["diff", "--binary", "HEAD", "--"], record.repoRoot, true);
        if (!patch.trim()) {
            return false;
        }
        runGit(["apply", "--3way", "--whitespace=nowarn"], record.worktreePath, patch);
        return true;
    }
    diffText(record) {
        const baseDiff = gitRawOutput(["diff", "--stat", "--patch", `${record.baseSha}...HEAD`, "--"], record.worktreePath, true);
        const workingDiff = gitRawOutput(["diff", "--stat", "--patch", "HEAD", "--"], record.worktreePath, true);
        const untracked = gitRawOutput(["ls-files", "--others", "--exclude-standard"], record.worktreePath, true).trim();
        return [
            baseDiff.trim() ? `# Branch diff (${record.baseSha.slice(0, 12)}...HEAD)\n${baseDiff.trimEnd()}` : "",
            workingDiff.trim() ? `# Uncommitted diff (HEAD)\n${workingDiff.trimEnd()}` : "",
            untracked ? `# Untracked files\n${untracked}` : "",
        ].filter(Boolean).join("\n\n");
    }
    changedFiles(record) {
        if (!existsSync(record.worktreePath)) {
            return [];
        }
        const byPath = new Map();
        const add = (relativePath, statusCode) => {
            if (!relativePath || !isSafeRelativePath(relativePath))
                return;
            const status = gitStatusCodeToChangedStatus(statusCode);
            const existing = byPath.get(relativePath);
            if (existing) {
                if (existing.status !== status)
                    existing.status = "conflict";
                return;
            }
            byPath.set(relativePath, { path: relativePath, status, sourceWorktreeIds: [record.id] });
        };
        const committed = gitRawOutput(["diff", "--name-status", `${record.baseSha}...HEAD`, "--"], record.worktreePath, true);
        for (const line of committed.split(/\r?\n/).filter(Boolean)) {
            const [statusCode = "", firstPath = "", secondPath = ""] = line.split(/\t+/);
            add(secondPath || firstPath, statusCode);
        }
        const status = gitRawOutput(["status", "--porcelain"], record.worktreePath, true);
        for (const line of status.split(/\r?\n/).filter(Boolean)) {
            const code = line.slice(0, 2);
            const rawPath = line.slice(3).trim();
            const renamedPath = rawPath.includes(" -> ") ? rawPath.split(" -> ").pop() ?? rawPath : rawPath;
            add(renamedPath.replace(/^"|"$/g, ""), code);
        }
        return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
    }
    copyUntrackedFiles(record) {
        const raw = gitRawOutput(["ls-files", "--others", "--exclude-standard", "-z"], record.repoRoot, true);
        const copied = [];
        const skipped = [];
        for (const relative of raw.split("\0").filter(Boolean)) {
            if (!isSafeRelativePath(relative)) {
                skipped.push(relative);
                continue;
            }
            const source = path.join(record.repoRoot, relative);
            const target = path.join(record.worktreePath, relative);
            const stat = safeStat(source);
            if (!stat || !stat.isFile()) {
                skipped.push(relative);
                continue;
            }
            mkdirSync(path.dirname(target), { recursive: true });
            copyFileSync(source, target);
            copied.push(relative);
        }
        return { copied, skipped };
    }
    sharedWarnings(sessions) {
        const byRepo = new Map();
        for (const session of sessions) {
            const workspace = session.workspace;
            if (!workspace || this.getByWorkspace(workspace)) {
                continue;
            }
            const repoRoot = gitOutputSafe(["rev-parse", "--show-toplevel"], workspace);
            if (!repoRoot) {
                continue;
            }
            const warning = byRepo.get(repoRoot) ?? {
                repoRoot,
                sourceWorkspace: workspace,
                sessions: [],
            };
            warning.sessions.push(session);
            byRepo.set(repoRoot, warning);
        }
        return [...byRepo.values()].filter((warning) => warning.sessions.length > 1);
    }
}
function previewSource(record) {
    return {
        id: record.id,
        branchName: record.branchName,
        status: record.status,
        threadId: record.threadId,
        agentId: record.agentId,
        worktreePath: record.worktreePath,
        commitSha: record.commitSha,
    };
}
function conflictReviewItem(file, sources, records, repoRoot, baseSha) {
    const sourceWorktrees = sources.filter((source) => file.sourceWorktreeIds.includes(source.id));
    const sameFile = sourceWorktrees.length > 1;
    const risk = file.status === "conflict" ? "status-mismatch" : sameFile ? "same-file" : "none";
    const changedRanges = sourceWorktrees.map((source) => {
        const record = records.find((candidate) => candidate.id === source.id);
        return {
            worktreeId: source.id,
            branchName: source.branchName,
            ranges: record?.commitSha ? changedRangesForFile(record, file.path) : [],
        };
    });
    const hasLineOverlap = changedRangesHaveOverlap(changedRanges);
    const riskReasons = worktreeRiskReasons(file, sameFile, hasLineOverlap);
    const riskLevel = worktreeRiskLevel(file, sameFile, hasLineOverlap);
    const recommendation = worktreeRiskRecommendation(riskLevel, risk, hasLineOverlap);
    const item = {
        path: file.path,
        status: file.status,
        sourceWorktrees,
        risk,
        riskLevel,
        riskReasons,
        hasLineOverlap,
        changedRanges,
        recommendation,
    };
    if (risk !== "none") {
        item.baseContent = gitContentPreview("Base", repoRoot, baseSha, file.path);
        item.sourceVersions = sourceWorktrees.map((source) => {
            const record = records.find((candidate) => candidate.id === source.id);
            const preview = record?.commitSha
                ? gitContentPreview(source.branchName, record.repoRoot, record.commitSha, file.path, record.worktreePath)
                : worktreeFilePreview(source.branchName, record?.worktreePath, file.path);
            return {
                ...preview,
                worktreeId: source.id,
                branchName: source.branchName,
                commitSha: source.commitSha,
            };
        });
    }
    return item;
}
function gitContentPreview(label, cwd, ref, relativePath, fallbackRoot) {
    const content = gitOutputSafe(["show", `${ref}:${relativePath}`], cwd, true);
    if (content !== undefined) {
        return previewContent(label, content);
    }
    return worktreeFilePreview(label, fallbackRoot, relativePath);
}
function worktreeFilePreview(label, root, relativePath) {
    if (!root || !isSafeRelativePath(relativePath)) {
        return { label, unavailable: "File version is not available." };
    }
    const filePath = path.join(root, relativePath);
    const stat = safeStat(filePath);
    if (!stat?.isFile()) {
        return { label, unavailable: "File version is not available." };
    }
    try {
        return previewContent(label, readFileSync(filePath, "utf8"));
    }
    catch (error) {
        return { label, unavailable: error instanceof Error ? error.message : String(error) };
    }
}
function previewContent(label, content) {
    const limited = truncateUtf8(content, MAX_FILE_PREVIEW_BYTES);
    return {
        label,
        content: limited.text,
        truncated: limited.truncated,
    };
}
function structuredDiffFiles(diff, files) {
    const byPath = new Map(files.map((file) => [file.path, file]));
    const structured = [];
    let current = null;
    let oldLine = 0;
    let newLine = 0;
    const pushMeta = (text) => {
        if (current)
            current.lines.push({ kind: "meta", text });
    };
    for (const line of diff.split(/\r?\n/)) {
        const diffHeader = line.match(/^diff --git a\/(.+) b\/(.+)$/);
        if (diffHeader) {
            const filePath = normalizeDiffPath(diffHeader[2] ?? diffHeader[1] ?? "unknown");
            const changed = byPath.get(filePath);
            current = {
                path: filePath,
                oldPath: normalizeDiffPath(diffHeader[1] ?? filePath),
                status: changed?.status ?? "unknown",
                additions: 0,
                deletions: 0,
                lines: [],
            };
            structured.push(current);
            oldLine = 0;
            newLine = 0;
            pushMeta(line);
            continue;
        }
        if (!current)
            continue;
        if (line.startsWith("new file mode")) {
            current.status = "added";
            pushMeta(line);
            continue;
        }
        if (line.startsWith("deleted file mode")) {
            current.status = "deleted";
            pushMeta(line);
            continue;
        }
        if (line.startsWith("rename from ")) {
            current.oldPath = normalizeDiffPath(line.slice("rename from ".length));
            current.status = "renamed";
            pushMeta(line);
            continue;
        }
        if (line.startsWith("rename to ")) {
            current.path = normalizeDiffPath(line.slice("rename to ".length));
            current.status = "renamed";
            pushMeta(line);
            continue;
        }
        if (line.startsWith("Binary files ")) {
            current.binary = true;
            pushMeta(line);
            continue;
        }
        const hunk = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
        if (hunk) {
            oldLine = Number(hunk[1]) - 1;
            newLine = Number(hunk[2]) - 1;
            current.lines.push({ kind: "hunk", text: line });
            continue;
        }
        if (line.startsWith("--- ") || line.startsWith("+++ ") || line.startsWith("index ")) {
            pushMeta(line);
            continue;
        }
        if (line.startsWith("-")) {
            oldLine += 1;
            current.deletions += 1;
            current.lines.push({ kind: "delete", oldLine, text: line.slice(1) });
            continue;
        }
        if (line.startsWith("+")) {
            newLine += 1;
            current.additions += 1;
            current.lines.push({ kind: "add", newLine, text: line.slice(1) });
            continue;
        }
        if (line.startsWith(" ")) {
            oldLine += 1;
            newLine += 1;
            current.lines.push({ kind: "context", oldLine, newLine, text: line.slice(1) });
            continue;
        }
        pushMeta(line);
    }
    return structured.filter((file) => file.lines.length > 0 || file.binary);
}
function normalizeDiffPath(value) {
    return value.replace(/^"|"$/g, "").replace(/^a\//, "").replace(/^b\//, "");
}
function changedRangesForFile(record, relativePath) {
    if (!record.commitSha || !isSafeRelativePath(relativePath))
        return [];
    const raw = gitRawOutput(["diff", "--unified=0", `${record.baseSha}..${record.commitSha}`, "--", relativePath], record.repoRoot, true);
    const ranges = [];
    for (const line of raw.split(/\r?\n/)) {
        const match = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
        if (!match)
            continue;
        const start = Number(match[1]);
        const length = Number(match[2] ?? 1);
        ranges.push({ start, end: Math.max(start, start + Math.max(length, 1) - 1) });
    }
    return ranges;
}
function changedRangesHaveOverlap(sources) {
    for (let i = 0; i < sources.length; i += 1) {
        for (let j = i + 1; j < sources.length; j += 1) {
            for (const left of sources[i]?.ranges ?? []) {
                for (const right of sources[j]?.ranges ?? []) {
                    if (left.start <= right.end && right.start <= left.end)
                        return true;
                }
            }
        }
    }
    return false;
}
function worktreeRiskLevel(file, sameFile, hasLineOverlap) {
    if (file.status === "conflict")
        return "blocked";
    if (hasLineOverlap)
        return "high";
    if (sameFile)
        return "medium";
    if (file.status === "deleted" || file.status === "renamed")
        return "medium";
    return "low";
}
function worktreeRiskReasons(file, sameFile, hasLineOverlap) {
    const reasons = [];
    if (file.status === "conflict")
        reasons.push("Selected worktrees report different change types.");
    if (sameFile)
        reasons.push("The file is changed by multiple selected worktrees.");
    if (hasLineOverlap)
        reasons.push("The changed line ranges overlap.");
    if (file.status === "deleted" || file.status === "renamed")
        reasons.push(`The file is ${file.status}; verify the final path/content before merging.`);
    if (!reasons.length)
        reasons.push("Single-source file change.");
    return reasons;
}
function worktreeRiskRecommendation(level, risk, hasLineOverlap) {
    if (level === "blocked")
        return "Resolve the change-type conflict before creating the integration branch.";
    if (level === "high" || hasLineOverlap)
        return "Use the resolver preview or manual content because selected worktrees touched overlapping lines.";
    if (risk === "same-file")
        return "Review side-by-side diffs before merging because multiple worktrees changed this file.";
    if (level === "medium")
        return "Review the file before merging because the change can affect paths or file lifecycle.";
    return "No conflict candidate detected for this file.";
}
function summarizeWorktreeRisks(items, warnings) {
    const summary = {
        label: "low",
        low: 0,
        medium: 0,
        high: 0,
        blocked: 0,
        totalFiles: items.length,
        riskyFiles: 0,
        canMerge: warnings.length === 0,
    };
    for (const item of items) {
        const level = item.riskLevel ?? "low";
        summary[level] += 1;
        if (level !== "low")
            summary.riskyFiles += 1;
    }
    if (warnings.length > 0 && items.length === 0)
        summary.blocked += 1;
    summary.canMerge = warnings.length === 0 && summary.blocked === 0;
    summary.label = summary.blocked > 0 ? "blocked" : summary.high > 0 ? "high" : summary.medium > 0 ? "medium" : "low";
    return summary;
}
function integrationExportSummary(preview, records) {
    const risk = preview.riskSummary;
    const files = preview.files.map((file) => `- ${file.status} ${file.path} (${file.sourceWorktreeIds.length} source${file.sourceWorktreeIds.length === 1 ? "" : "s"})`).join("\n") || "- No file changes.";
    const warnings = preview.warnings.length ? preview.warnings.map((warning) => `- ${warning}`).join("\n") : "- None.";
    const sources = records.map((record) => `- ${record.branchName} (${record.commitSha ?? "uncommitted"})`).join("\n");
    return [
        "# NordRelay Worktree Integration Export",
        "",
        `Repository: ${preview.repoName ?? records[0]?.repoName ?? "-"}`,
        `Base: ${preview.baseSha ?? records[0]?.baseSha ?? "-"}`,
        `Generated: ${preview.generatedAt}`,
        "",
        "## Risk",
        "",
        `Overall: ${risk?.label ?? "unknown"}`,
        `Files: ${risk?.totalFiles ?? preview.files.length}, risky: ${risk?.riskyFiles ?? preview.conflictCandidates.length}, blocked: ${risk?.blocked ?? 0}, high: ${risk?.high ?? 0}, medium: ${risk?.medium ?? 0}, low: ${risk?.low ?? 0}`,
        "",
        "## Sources",
        "",
        sources,
        "",
        "## Warnings",
        "",
        warnings,
        "",
        "## Files",
        "",
        files,
        "",
    ].join("\n");
}
function integrationPrBody(preview, records) {
    return [
        "Generated by NordRelay worktree review.",
        "",
        `Selected worktrees: ${records.map((record) => record.id).join(", ")}`,
        `Risk: ${preview.riskSummary?.label ?? "unknown"}`,
        `Changed files: ${preview.files.length}`,
        preview.warnings.length ? `Warnings:\n${preview.warnings.map((warning) => `- ${warning}`).join("\n")}` : "Warnings: none",
    ].join("\n");
}
function shellQuote(value) {
    return `'${value.replace(/'/g, "'\\''")}'`;
}
export function createSessionWorktreeStore(config) {
    return new SessionWorktreeStore(config.workspace, config.stateBackend);
}
function defaultCommitMessage(record) {
    const suffix = record.threadId ? ` ${record.threadId}` : ` ${record.id}`;
    return `NordRelay session${suffix}`;
}
function statusText(record, exists, dirty) {
    if (!exists) {
        return "missing";
    }
    if (record.status === "merged") {
        return "merged";
    }
    if (record.status === "conflict") {
        return "merge conflict";
    }
    if (dirty) {
        return "dirty";
    }
    if (record.commitSha) {
        return "committed";
    }
    return record.status;
}
function gitStatus(cwd) {
    if (!existsSync(cwd)) {
        return [];
    }
    return gitOutputSafe(["status", "--porcelain"], cwd, true)?.split(/\r?\n/).filter(Boolean) ?? [];
}
function unmergedFiles(cwd) {
    return gitOutputSafe(["diff", "--name-only", "--diff-filter=U", "--"], cwd, true)?.split(/\r?\n/).filter(Boolean).filter(isSafeRelativePath) ?? [];
}
function gitOutput(args, cwd, allowLarge = false) {
    return gitRawOutput(args, cwd, allowLarge).trim();
}
function gitRawOutput(args, cwd, allowLarge = false) {
    const { stdout } = runGit(args, cwd, undefined, allowLarge);
    return stdout;
}
function gitOutputSafe(args, cwd, allowLarge = false) {
    try {
        return gitOutput(args, cwd, allowLarge);
    }
    catch {
        return undefined;
    }
}
function branchExists(cwd, branchName) {
    try {
        runGit(["rev-parse", "--verify", branchName], cwd);
        return true;
    }
    catch {
        return false;
    }
}
function isAncestor(cwd, ancestor, descendant) {
    try {
        runGit(["merge-base", "--is-ancestor", ancestor, descendant], cwd);
        return true;
    }
    catch {
        return false;
    }
}
function runGit(args, cwd, input, allowLarge = false) {
    try {
        const stdout = execFileSync("git", args, {
            cwd,
            input,
            encoding: "utf8",
            maxBuffer: allowLarge ? MAX_GIT_BUFFER : 1024 * 1024,
            stdio: input === undefined ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
        });
        return { stdout, stderr: "" };
    }
    catch (error) {
        const err = error;
        const detail = [err.stderr, err.stdout, err.message].filter(Boolean).join("\n").trim();
        throw new Error(detail || "git command failed");
    }
}
function shortId() {
    return randomBytes(4).toString("hex");
}
function safeSegment(value) {
    return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "repo";
}
function isSafeRelativePath(relative) {
    return Boolean(relative && !path.isAbsolute(relative) && !relative.split(/[\\/]+/).includes(".."));
}
function safeStat(filePath) {
    try {
        return statSync(filePath);
    }
    catch {
        return null;
    }
}
function truncateUtf8(text, limitBytes) {
    const bytes = Buffer.byteLength(text, "utf8");
    if (bytes <= limitBytes) {
        return { text, truncated: false };
    }
    let end = Math.min(text.length, limitBytes);
    while (Buffer.byteLength(text.slice(0, end), "utf8") > limitBytes && end > 0) {
        end -= 1;
    }
    return { text: `${text.slice(0, end)}\n\n[diff truncated]`, truncated: true };
}
function gitStatusCodeToChangedStatus(code) {
    const normalized = code.trim();
    if (normalized === "??")
        return "untracked";
    if (normalized.includes("U"))
        return "conflict";
    const first = normalized[0] ?? "";
    if (first === "A")
        return "added";
    if (first === "D")
        return "deleted";
    if (first === "R")
        return "renamed";
    if (first === "C")
        return "copied";
    if (first === "M")
        return "modified";
    return "unknown";
}
