import { v4 as uuidv4 } from "uuid";
import { getScriptConfigs, getWorktreeTerminalSpecs, isServiceScript, paseoConfigParseError, processCarriageReturns, readPaseoConfig, resolveWorktreeRuntimeEnv, runWorktreeSetupCommands, WorktreeSetupError, } from "../utils/worktree.js";
import { findFreePort } from "./service-proxy.js";
import { assertNoServiceEnvNameCollisions, buildWorkspaceServiceEnv, } from "./workspace-service-env.js";
import { ensureWorkspaceServicePortPlan, requirePlannedWorkspaceServicePort, refreshWorkspaceServicePort, } from "./workspace-service-port-registry.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;
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");
}
export function createWorktreeSetupOutputAccumulator() {
    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 };
}
export function appendWorktreeSetupOutputAccumulator(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,
    };
}
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 output = buildWorktreeSetupCommandLog({
            index: index + 1,
            result,
            outputAccumulatorsByIndex,
        });
        if (output.log.length > 0) {
            lines.push(output.log.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 buildWorktreeSetupCommandLog(input) {
    const { index, result, outputAccumulatorsByIndex } = input;
    const accumulator = outputAccumulatorsByIndex?.get(index);
    const rendered = accumulator
        ? renderMiddleTruncationAccumulator(accumulator)
        : truncateTextInMiddle(`${result.stdout ?? ""}${result.stderr ?? ""}`, MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES);
    return {
        log: processCarriageReturns(rendered.text),
        truncated: rendered.truncated,
    };
}
export function createWorktreeSetupProgressAccumulator() {
    return {
        resultsByIndex: new Map(),
        outputAccumulatorsByIndex: new Map(),
    };
}
export function applyWorktreeSetupProgressEvent(accumulator, event) {
    const existing = accumulator.resultsByIndex.get(event.index);
    const baseResult = existing ?? {
        command: event.command,
        cwd: event.cwd,
        stdout: "",
        stderr: "",
        exitCode: null,
        durationMs: 0,
    };
    if (event.type === "output") {
        const outputAccumulator = accumulator.outputAccumulatorsByIndex.get(event.index) ??
            createWorktreeSetupOutputAccumulator();
        appendWorktreeSetupOutputAccumulator(outputAccumulator, event.chunk);
        accumulator.outputAccumulatorsByIndex.set(event.index, outputAccumulator);
        accumulator.resultsByIndex.set(event.index, {
            ...baseResult,
            stdout: baseResult.stdout,
            stderr: baseResult.stderr,
        });
        return;
    }
    if (event.type === "command_completed") {
        accumulator.resultsByIndex.set(event.index, {
            ...baseResult,
            stdout: event.stdout,
            stderr: event.stderr,
            exitCode: event.exitCode,
            durationMs: event.durationMs,
        });
        return;
    }
    accumulator.resultsByIndex.set(event.index, baseResult);
}
export function getWorktreeSetupProgressResults(accumulator) {
    return Array.from(accumulator.resultsByIndex.entries())
        .sort((a, b) => a[0] - b[0])
        .map(([, result]) => result);
}
export function buildWorktreeSetupDetail(input) {
    let anyCommandTruncated = false;
    const commands = input.results.map((result, index) => {
        const renderedLog = buildWorktreeSetupCommandLog({
            index: index + 1,
            result,
            outputAccumulatorsByIndex: input.outputAccumulatorsByIndex,
        });
        if (renderedLog.truncated) {
            anyCommandTruncated = true;
        }
        return {
            index: index + 1,
            command: result.command,
            cwd: result.cwd,
            log: renderedLog.log,
            status: commandStatusFromResult(result),
            exitCode: result.exitCode,
            ...(result.durationMs > 0 ? { durationMs: result.durationMs } : {}),
        };
    });
    const renderedLog = buildWorktreeSetupLog({
        results: input.results,
        outputAccumulatorsByIndex: input.outputAccumulatorsByIndex,
    });
    return {
        type: "worktree_setup",
        worktreePath: input.worktree.worktreePath,
        branchName: input.worktree.branchName,
        log: renderedLog.log,
        commands,
        ...(renderedLog.truncated || anyCommandTruncated ? { truncated: true } : {}),
    };
}
function buildSetupTimelineItem(input) {
    const detail = buildWorktreeSetupDetail({
        worktree: input.worktree,
        results: input.results,
        outputAccumulatorsByIndex: input.outputAccumulatorsByIndex,
    });
    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 pendingResolve = resolve;
        let timeout = null;
        let unsubscribe = null;
        const finish = () => {
            if (!pendingResolve) {
                return;
            }
            const fn = pendingResolve;
            pendingResolve = null;
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            if (unsubscribe) {
                unsubscribe();
                unsubscribe = null;
            }
            fn();
        };
        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 terminalManager = options.terminalManager;
    const results = await Promise.all(terminalSpecs.map(async (spec) => {
        try {
            const terminal = await terminalManager.createTerminal({
                cwd: options.worktree.worktreePath,
                name: spec.name,
                env: runtimeEnv,
            });
            await waitForTerminalBootstrapReadiness(terminal);
            terminal.send({
                type: "input",
                data: `${spec.command}\r`,
            });
            return {
                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");
            return {
                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 (options.shouldBootstrap === false) {
        return;
    }
    const setupCallId = uuidv4();
    let setupResults = [];
    let runtimeEnv = null;
    const emitLiveTimelineItem = options.emitLiveTimelineItem;
    const progressAccumulator = createWorktreeSetupProgressAccumulator();
    let liveEmitQueue = Promise.resolve();
    const queueLiveRunningEmit = () => {
        if (!emitLiveTimelineItem) {
            return;
        }
        const runningResults = getWorktreeSetupProgressResults(progressAccumulator);
        liveEmitQueue = liveEmitQueue.then(async () => {
            try {
                await emitLiveTimelineItem(buildSetupTimelineItem({
                    callId: setupCallId,
                    status: "running",
                    worktree: options.worktree,
                    results: runningResults,
                    outputAccumulatorsByIndex: progressAccumulator.outputAccumulatorsByIndex,
                    errorMessage: null,
                }));
            }
            catch (error) {
                options.logger?.warn({ err: error, agentId: options.agentId }, "Failed to emit live worktree setup timeline update");
            }
            return;
        });
    };
    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) => {
                applyWorktreeSetupProgressEvent(progressAccumulator, event);
                queueLiveRunningEmit();
            },
        });
        await liveEmitQueue;
        const completed = await options.appendTimelineItem(buildSetupTimelineItem({
            callId: setupCallId,
            status: "completed",
            worktree: options.worktree,
            results: setupResults,
            outputAccumulatorsByIndex: progressAccumulator.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: progressAccumulator.outputAccumulatorsByIndex,
            errorMessage: message,
        }));
        return;
    }
    await runWorktreeTerminalBootstrap(options, runtimeEnv);
}
async function setupServiceScriptRoute(params) {
    const { scriptConfigs, config, scriptName, projectSlug, branchName, workspaceId, daemonPort, daemonListenHost, serviceProxyPublicBaseUrl, existingRuntimeEntry, serviceProxy, } = params;
    const serviceDeclarations = [];
    for (const [configuredScriptName, scriptConfig] of scriptConfigs) {
        if (isServiceScript(scriptConfig)) {
            serviceDeclarations.push({
                scriptName: configuredScriptName,
                port: scriptConfig.port,
            });
        }
    }
    assertNoServiceEnvNameCollisions(serviceDeclarations.map((serviceDeclaration) => serviceDeclaration.scriptName));
    const plannedPorts = await ensureWorkspaceServicePortPlan({
        workspaceId,
        services: serviceDeclarations,
        allocatePort: findFreePort,
    });
    const port = existingRuntimeEntry?.lifecycle === "stopped"
        ? await refreshWorkspaceServicePort({
            workspaceId,
            service: { scriptName, port: config.port },
            allocatePort: findFreePort,
        })
        : requirePlannedWorkspaceServicePort(plannedPorts, scriptName);
    const peers = [];
    for (const [peerScriptName, peerPort] of plannedPorts) {
        peers.push({
            scriptName: peerScriptName,
            port: peerScriptName === scriptName ? port : peerPort,
        });
    }
    const env = buildWorkspaceServiceEnv({
        scriptName,
        projectSlug,
        branchName,
        daemonPort,
        daemonListenHost,
        serviceProxyPublicBaseUrl,
        peers,
    });
    const registeredRoute = serviceProxy.registerWorkspaceService({
        port,
        workspaceId,
        projectSlug,
        branchName,
        scriptName,
        publicBaseUrl: serviceProxyPublicBaseUrl ?? null,
    });
    return { hostname: registeredRoute.hostname, port, env };
}
async function acquireWorkspaceScriptTerminal(params) {
    const { serviceScript, existingRuntimeEntry, terminalManager, repoRoot, scriptName, env } = params;
    let reusableTerminal = null;
    if (!serviceScript && existingRuntimeEntry?.terminalId) {
        reusableTerminal = terminalManager.getTerminal(existingRuntimeEntry.terminalId) ?? null;
    }
    const terminal = reusableTerminal ??
        (await terminalManager.createTerminal({
            cwd: repoRoot,
            name: scriptName,
            title: scriptName,
            env,
        }));
    return { terminal, reusableTerminal };
}
export async function spawnWorkspaceScript(options) {
    const { repoRoot, workspaceId, projectSlug, branchName, scriptName, daemonPort, daemonListenHost, serviceProxyPublicBaseUrl, serviceProxy, runtimeStore, terminalManager, logger, onLifecycleChanged, } = options;
    const configResult = readPaseoConfig(repoRoot);
    if (!configResult.ok) {
        throw paseoConfigParseError(configResult);
    }
    const scriptConfigs = getScriptConfigs(configResult.config);
    const config = scriptConfigs.get(scriptName);
    if (!config) {
        throw new Error(`Script '${scriptName}' is not configured in paseo.json`);
    }
    const serviceScript = isServiceScript(config);
    const scriptType = serviceScript ? "service" : "script";
    let hostname = null;
    let port = null;
    let runtimeRegistered = false;
    let routeRegistered = false;
    let disposeLifecycleListeners = null;
    try {
        if (runtimeStore.isRunning({ workspaceId, scriptName })) {
            throw new Error(`Script '${scriptName}' is already running`);
        }
        const existingRuntimeEntry = runtimeStore.get({ workspaceId, scriptName });
        let env;
        if (serviceScript) {
            const serviceSetup = await setupServiceScriptRoute({
                scriptConfigs,
                config,
                scriptName,
                projectSlug,
                branchName,
                workspaceId,
                daemonPort,
                daemonListenHost,
                serviceProxyPublicBaseUrl,
                existingRuntimeEntry,
                serviceProxy,
            });
            hostname = serviceSetup.hostname;
            port = serviceSetup.port;
            env = serviceSetup.env;
            routeRegistered = true;
        }
        const { terminal, reusableTerminal } = await acquireWorkspaceScriptTerminal({
            serviceScript,
            existingRuntimeEntry,
            terminalManager,
            repoRoot,
            scriptName,
            env,
        });
        runtimeStore.set({
            workspaceId,
            scriptName,
            type: scriptType,
            lifecycle: "running",
            terminalId: terminal.id,
            exitCode: null,
        });
        runtimeRegistered = true;
        const stopRuntimeIfCurrent = (input) => {
            const current = runtimeStore.get({ workspaceId, scriptName });
            if (current?.terminalId !== terminal.id || current.lifecycle !== "running") {
                return;
            }
            disposeLifecycleListeners?.();
            disposeLifecycleListeners = null;
            if (input.removeRoute && hostname) {
                serviceProxy.removeWorkspaceService({ workspaceId, scriptName });
            }
            runtimeStore.set({
                workspaceId,
                scriptName,
                type: scriptType,
                lifecycle: "stopped",
                terminalId: terminal.id,
                exitCode: input.exitCode,
            });
            onLifecycleChanged?.();
            logger?.info({
                scriptName,
                hostname,
                exitCode: input.exitCode,
                terminalId: terminal.id,
            }, "Stopped worktree script");
        };
        const unsubscribeExit = terminal.onExit((info) => {
            stopRuntimeIfCurrent({
                exitCode: info.exitCode,
                removeRoute: true,
            });
        });
        let unsubscribeCommandFinished = null;
        if (!serviceScript) {
            unsubscribeCommandFinished = terminal.onCommandFinished((info) => {
                stopRuntimeIfCurrent({ exitCode: info.exitCode, removeRoute: false });
            });
        }
        disposeLifecycleListeners = () => {
            unsubscribeExit();
            unsubscribeCommandFinished?.();
        };
        if (!reusableTerminal) {
            await waitForTerminalBootstrapReadiness(terminal);
        }
        terminal.send({ type: "input", data: `${config.command}\r` });
        logger?.info({
            scriptName,
            hostname,
            port,
            terminalId: terminal.id,
            type: scriptType,
        }, serviceScript
            ? `Registered script proxy: ${hostname} -> 127.0.0.1:${port}`
            : "Started workspace script");
        onLifecycleChanged?.();
        return {
            scriptName,
            hostname,
            port,
            terminalId: terminal.id,
        };
    }
    catch (error) {
        disposeLifecycleListeners?.();
        if (routeRegistered && hostname) {
            serviceProxy.removeServiceRoutesByHostnames([hostname]);
        }
        if (runtimeRegistered) {
            runtimeStore.remove({ workspaceId, scriptName });
        }
        logger?.error({
            err: error,
            scriptName,
            repoRoot,
            branchName,
            hostname,
            port,
            command: config.command,
        }, "Failed to spawn worktree script");
        throw error;
    }
}
export function teardownWorktreeScripts(options) {
    const { hostnames, serviceProxy, logger } = options;
    serviceProxy.removeServiceRoutesByHostnames(hostnames);
    for (const hostname of hostnames) {
        logger.info({ hostname }, "Removed script proxy route");
    }
}
//# sourceMappingURL=worktree-bootstrap.js.map