import { v4 as uuidv4 } from "uuid";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import { sep } from "node:path";
import { createWorktree, getWorktreeTerminalSpecs, listPaseoWorktrees, resolveWorktreeRuntimeEnv, runWorktreeSetupCommands, WorktreeSetupError, } from "../utils/worktree.js";
const MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES = 64 * 1024;
const WORKTREE_SETUP_TRUNCATION_MARKER = "\n...<output truncated in the middle>...\n";
const WORKTREE_BOOTSTRAP_TERMINAL_READY_TIMEOUT_MS = 1500;
const READ_ONLY_GIT_ENV = {
    ...process.env,
    GIT_OPTIONAL_LOCKS: "0",
};
const execAsync = promisify(exec);
const worktreeSetupEligibility = new WeakMap();
function byteLength(text) {
    return Buffer.byteLength(text, "utf8");
}
function sliceFirstBytes(text, maxBytes) {
    if (maxBytes <= 0 || text.length === 0) {
        return "";
    }
    const bytes = Buffer.from(text, "utf8");
    if (bytes.length <= maxBytes) {
        return text;
    }
    return bytes.subarray(0, maxBytes).toString("utf8");
}
function sliceLastBytes(text, maxBytes) {
    if (maxBytes <= 0 || text.length === 0) {
        return "";
    }
    const bytes = Buffer.from(text, "utf8");
    if (bytes.length <= maxBytes) {
        return text;
    }
    return bytes.subarray(bytes.length - maxBytes).toString("utf8");
}
function createMiddleTruncationAccumulator() {
    return {
        totalBytes: 0,
        head: "",
        tail: "",
        truncated: false,
    };
}
function getHeadTailBudgets(maxBytes) {
    const markerBytes = byteLength(WORKTREE_SETUP_TRUNCATION_MARKER);
    const availableBytes = Math.max(0, maxBytes - markerBytes);
    const headBytes = Math.floor(availableBytes / 2);
    const tailBytes = availableBytes - headBytes;
    return { headBytes, tailBytes };
}
function appendToMiddleTruncationAccumulator(accumulator, chunk) {
    if (!chunk) {
        return;
    }
    accumulator.totalBytes += byteLength(chunk);
    if (!accumulator.truncated) {
        const combined = `${accumulator.head}${chunk}`;
        if (byteLength(combined) <= MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES) {
            accumulator.head = combined;
            return;
        }
        const { headBytes, tailBytes } = getHeadTailBudgets(MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES);
        accumulator.head = sliceFirstBytes(combined, headBytes);
        accumulator.tail = sliceLastBytes(combined, tailBytes);
        accumulator.truncated = true;
        return;
    }
    const { tailBytes } = getHeadTailBudgets(MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES);
    accumulator.tail = sliceLastBytes(`${accumulator.tail}${chunk}`, tailBytes);
}
function truncateTextInMiddle(text, maxBytes) {
    if (maxBytes <= 0 || !text) {
        return { text: "", truncated: text.length > 0 };
    }
    if (byteLength(text) <= maxBytes) {
        return { text, truncated: false };
    }
    const { headBytes, tailBytes } = getHeadTailBudgets(maxBytes);
    return {
        text: `${sliceFirstBytes(text, headBytes)}${WORKTREE_SETUP_TRUNCATION_MARKER}${sliceLastBytes(text, tailBytes)}`,
        truncated: true,
    };
}
function renderMiddleTruncationAccumulator(accumulator) {
    if (!accumulator.truncated) {
        return { text: accumulator.head, truncated: false };
    }
    return {
        text: `${accumulator.head}${WORKTREE_SETUP_TRUNCATION_MARKER}${accumulator.tail}`,
        truncated: true,
    };
}
export async function createAgentWorktree(options) {
    const existingWorktree = await findExistingPaseoWorktreeBySlug(options);
    if (existingWorktree) {
        const branchName = await resolveBranchNameForWorktreePath(existingWorktree.path);
        const reusedWorktree = {
            branchName,
            worktreePath: existingWorktree.path,
        };
        worktreeSetupEligibility.set(reusedWorktree, false);
        return reusedWorktree;
    }
    const createdWorktree = await createWorktree({
        branchName: options.branchName,
        cwd: options.cwd,
        baseBranch: options.baseBranch,
        worktreeSlug: options.worktreeSlug,
        runSetup: false,
        paseoHome: options.paseoHome,
    });
    worktreeSetupEligibility.set(createdWorktree, true);
    return createdWorktree;
}
async function findExistingPaseoWorktreeBySlug(options) {
    const worktrees = await listPaseoWorktrees({
        cwd: options.cwd,
        paseoHome: options.paseoHome,
    });
    const slugSuffix = `${sep}${options.worktreeSlug}`;
    return worktrees.find((worktree) => worktree.path.endsWith(slugSuffix));
}
async function resolveBranchNameForWorktreePath(worktreePath) {
    const { stdout } = await execAsync("git branch --show-current", {
        cwd: worktreePath,
        env: READ_ONLY_GIT_ENV,
    });
    const branchName = stdout.trim();
    if (!branchName) {
        throw new Error(`Unable to resolve branch for existing worktree: ${worktreePath}`);
    }
    return branchName;
}
function formatDurationMs(durationMs) {
    return `${(durationMs / 1000).toFixed(2)}s`;
}
function commandStatusFromResult(result) {
    if (result.exitCode === null) {
        return "running";
    }
    return result.exitCode === 0 ? "completed" : "failed";
}
function buildWorktreeSetupLog(input) {
    const { results, outputAccumulatorsByIndex } = input;
    if (results.length === 0) {
        return {
            log: "",
            truncated: false,
        };
    }
    const lines = [];
    let anyTruncated = false;
    const total = results.length;
    for (const [index, result] of results.entries()) {
        lines.push(`==> [${index + 1}/${total}] Running: ${result.command}`);
        const accumulator = outputAccumulatorsByIndex?.get(index + 1);
        const output = accumulator
            ? renderMiddleTruncationAccumulator(accumulator)
            : truncateTextInMiddle(`${result.stdout ?? ""}${result.stderr ?? ""}`, MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES);
        if (output.text.length > 0) {
            lines.push(output.text.replace(/\n$/, ""));
        }
        if (output.truncated) {
            anyTruncated = true;
        }
        if (result.exitCode !== null) {
            lines.push(`<== [${index + 1}/${total}] Exit ${result.exitCode} in ${formatDurationMs(result.durationMs)}`);
        }
    }
    return {
        log: lines.join("\n"),
        truncated: anyTruncated,
    };
}
function buildSetupTimelineItem(input) {
    const commands = input.results.map((result, index) => ({
        index: index + 1,
        command: result.command,
        cwd: result.cwd,
        status: commandStatusFromResult(result),
        exitCode: result.exitCode,
        ...(result.durationMs > 0 ? { durationMs: result.durationMs } : {}),
    }));
    const renderedLog = buildWorktreeSetupLog({
        results: input.results,
        outputAccumulatorsByIndex: input.outputAccumulatorsByIndex,
    });
    const detail = {
        type: "worktree_setup",
        worktreePath: input.worktree.worktreePath,
        branchName: input.worktree.branchName,
        log: renderedLog.log,
        commands,
        ...(renderedLog.truncated ? { truncated: true } : {}),
    };
    if (input.status === "running") {
        return {
            type: "tool_call",
            name: "paseo_worktree_setup",
            callId: input.callId,
            status: "running",
            detail,
            error: null,
        };
    }
    if (input.status === "completed") {
        return {
            type: "tool_call",
            name: "paseo_worktree_setup",
            callId: input.callId,
            status: "completed",
            detail,
            error: null,
        };
    }
    return {
        type: "tool_call",
        name: "paseo_worktree_setup",
        callId: input.callId,
        status: "failed",
        detail,
        error: { message: input.errorMessage ?? "Worktree setup failed" },
    };
}
function buildTerminalTimelineItem(input) {
    const detailInput = {
        worktreePath: input.worktree.worktreePath,
        branchName: input.worktree.branchName,
    };
    const detailOutput = {
        worktreePath: input.worktree.worktreePath,
        terminals: input.results,
    };
    if (input.status === "running") {
        return {
            type: "tool_call",
            name: "paseo_worktree_terminals",
            callId: input.callId,
            status: "running",
            detail: {
                type: "unknown",
                input: detailInput,
                output: null,
            },
            error: null,
        };
    }
    if (input.status === "completed") {
        return {
            type: "tool_call",
            name: "paseo_worktree_terminals",
            callId: input.callId,
            status: "completed",
            detail: {
                type: "unknown",
                input: detailInput,
                output: detailOutput,
            },
            error: null,
        };
    }
    return {
        type: "tool_call",
        name: "paseo_worktree_terminals",
        callId: input.callId,
        status: "failed",
        detail: {
            type: "unknown",
            input: detailInput,
            output: detailOutput,
        },
        error: { message: input.errorMessage ?? "Worktree terminal bootstrap failed" },
    };
}
async function waitForTerminalBootstrapReadiness(terminal) {
    if (terminalHasOutput(terminal.getState())) {
        return;
    }
    await new Promise((resolve) => {
        let settled = false;
        let timeout = null;
        let unsubscribe = null;
        const finish = () => {
            if (settled) {
                return;
            }
            settled = true;
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            if (unsubscribe) {
                unsubscribe();
                unsubscribe = null;
            }
            resolve();
        };
        unsubscribe = terminal.subscribe((message) => {
            if (message.type !== "output") {
                return;
            }
            finish();
        });
        if (terminalHasOutput(terminal.getState())) {
            finish();
            return;
        }
        timeout = setTimeout(finish, WORKTREE_BOOTSTRAP_TERMINAL_READY_TIMEOUT_MS);
    });
}
function terminalHasOutput(state) {
    for (const row of [...state.scrollback, ...state.grid]) {
        for (const cell of row) {
            if (cell.char.trim().length > 0) {
                return true;
            }
        }
    }
    return false;
}
async function runWorktreeTerminalBootstrap(options, runtimeEnv) {
    const terminalSpecs = getWorktreeTerminalSpecs(options.worktree.worktreePath);
    if (terminalSpecs.length === 0) {
        return;
    }
    const callId = uuidv4();
    const started = await options.appendTimelineItem(buildTerminalTimelineItem({
        callId,
        status: "running",
        worktree: options.worktree,
        results: [],
        errorMessage: null,
    }));
    if (!started) {
        return;
    }
    if (!options.terminalManager) {
        await options.appendTimelineItem(buildTerminalTimelineItem({
            callId,
            status: "failed",
            worktree: options.worktree,
            results: [],
            errorMessage: "Terminal manager not available",
        }));
        return;
    }
    const results = [];
    for (const spec of terminalSpecs) {
        try {
            const terminal = await options.terminalManager.createTerminal({
                cwd: options.worktree.worktreePath,
                name: spec.name,
                env: runtimeEnv,
            });
            await waitForTerminalBootstrapReadiness(terminal);
            terminal.send({
                type: "input",
                data: `${spec.command}\r`,
            });
            results.push({
                name: terminal.name ?? spec.name ?? null,
                command: spec.command,
                status: "started",
                terminalId: terminal.id,
                error: null,
            });
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            options.logger?.warn({ agentId: options.agentId, command: spec.command, err: error }, "Failed to bootstrap worktree terminal");
            results.push({
                name: spec.name ?? null,
                command: spec.command,
                status: "failed",
                terminalId: null,
                error: message,
            });
        }
    }
    await options.appendTimelineItem(buildTerminalTimelineItem({
        callId,
        status: "completed",
        worktree: options.worktree,
        results,
        errorMessage: null,
    }));
}
export async function runAsyncWorktreeBootstrap(options) {
    if (worktreeSetupEligibility.get(options.worktree) === false) {
        return;
    }
    const setupCallId = uuidv4();
    let setupResults = [];
    let runtimeEnv = null;
    const emitLiveTimelineItem = options.emitLiveTimelineItem;
    const runningResultsByIndex = new Map();
    const outputAccumulatorsByIndex = new Map();
    let liveEmitQueue = Promise.resolve();
    const queueLiveRunningEmit = () => {
        if (!emitLiveTimelineItem) {
            return;
        }
        const runningResults = Array.from(runningResultsByIndex.entries())
            .sort((a, b) => a[0] - b[0])
            .map(([, result]) => result);
        liveEmitQueue = liveEmitQueue.then(async () => {
            try {
                await emitLiveTimelineItem(buildSetupTimelineItem({
                    callId: setupCallId,
                    status: "running",
                    worktree: options.worktree,
                    results: runningResults,
                    outputAccumulatorsByIndex,
                    errorMessage: null,
                }));
            }
            catch (error) {
                options.logger?.warn({ err: error, agentId: options.agentId }, "Failed to emit live worktree setup timeline update");
            }
        });
    };
    try {
        runtimeEnv = await resolveWorktreeRuntimeEnv({
            worktreePath: options.worktree.worktreePath,
            branchName: options.worktree.branchName,
        });
        options.terminalManager?.registerCwdEnv({
            cwd: options.worktree.worktreePath,
            env: runtimeEnv,
        });
        setupResults = await runWorktreeSetupCommands({
            worktreePath: options.worktree.worktreePath,
            branchName: options.worktree.branchName,
            cleanupOnFailure: false,
            runtimeEnv,
            onEvent: (event) => {
                const existing = runningResultsByIndex.get(event.index);
                const baseResult = existing ?? {
                    command: event.command,
                    cwd: event.cwd,
                    stdout: "",
                    stderr: "",
                    exitCode: null,
                    durationMs: 0,
                };
                if (event.type === "output") {
                    const outputAccumulator = outputAccumulatorsByIndex.get(event.index) ?? createMiddleTruncationAccumulator();
                    appendToMiddleTruncationAccumulator(outputAccumulator, event.chunk);
                    outputAccumulatorsByIndex.set(event.index, outputAccumulator);
                    runningResultsByIndex.set(event.index, {
                        ...baseResult,
                        // Keep the timeline command model lightweight; output is carried in
                        // outputAccumulatorsByIndex.
                        stdout: baseResult.stdout,
                        stderr: baseResult.stderr,
                    });
                    queueLiveRunningEmit();
                    return;
                }
                if (event.type === "command_completed") {
                    runningResultsByIndex.set(event.index, {
                        ...baseResult,
                        stdout: event.stdout,
                        stderr: event.stderr,
                        exitCode: event.exitCode,
                        durationMs: event.durationMs,
                    });
                    queueLiveRunningEmit();
                    return;
                }
                runningResultsByIndex.set(event.index, baseResult);
                queueLiveRunningEmit();
            },
        });
        await liveEmitQueue;
        const completed = await options.appendTimelineItem(buildSetupTimelineItem({
            callId: setupCallId,
            status: "completed",
            worktree: options.worktree,
            results: setupResults,
            outputAccumulatorsByIndex,
            errorMessage: null,
        }));
        if (!completed) {
            return;
        }
    }
    catch (error) {
        if (error instanceof WorktreeSetupError) {
            setupResults = error.results;
        }
        await liveEmitQueue;
        const message = error instanceof Error ? error.message : String(error);
        await options.appendTimelineItem(buildSetupTimelineItem({
            callId: setupCallId,
            status: "failed",
            worktree: options.worktree,
            results: setupResults,
            outputAccumulatorsByIndex,
            errorMessage: message,
        }));
        return;
    }
    await runWorktreeTerminalBootstrap(options, runtimeEnv);
}
//# sourceMappingURL=worktree-bootstrap.js.map