import { existsSync } from "node:fs";
import { extractTodoResultItems } from "../providers/todo-result.js";
import { GitService, cleanupWorktreeRemoteAware } from "../services/git.js";
import { resolveThreadSelectionDefaults } from "../services/thread-defaults.js";
import { buildThreadHistoryReplayPrompt } from "../services/thread-history.js";
import { generateTitleViaApi, generateTitleViaTurn, normalizeGeneratedThreadTitle } from "../services/thread-title.js";
import { ToolName } from "./tool-names.js";
import { routeThread, formatRoutingPlanForPrompt } from "../services/thread-router.js";
const THREAD_TERMINAL_STATUSES = new Set(["completed", "error", "interrupted"]);
const THREAD_WAIT_POLL_MS = 100;
const THREAD_RESULT_ACTIVITY_LIMIT = 25;
const THREAD_RESULT_TEXT_LIMIT = 4_000;
function normalizeThreadProviderId(value) {
    if (typeof value !== "string")
        return null;
    const normalized = value.trim().toLowerCase();
    switch (normalized) {
        case "openai":
            return "codex";
        case "anthropic":
            return "claude-code";
        default:
            return normalized || null;
    }
}
function normalizePrompt(message, prompt) {
    const value = typeof prompt === "string" && prompt.trim() ? prompt : message;
    if (typeof value !== "string")
        return undefined;
    const trimmed = value.trim();
    return trimmed ? trimmed : undefined;
}
function normalizeTimeoutMs(timeoutMinutes, defaultMinutes) {
    const minutes = typeof timeoutMinutes === "number" && Number.isFinite(timeoutMinutes)
        ? timeoutMinutes
        : defaultMinutes;
    if (minutes === undefined || minutes <= 0)
        return undefined;
    return Math.max(1, Math.floor(minutes * 60_000));
}
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
function shouldGenerateSchedulerThreadTitle(title) {
    if (typeof title !== "string")
        return true;
    const normalized = title.trim().toLowerCase();
    return !normalized || normalized === "jait code quality rotation";
}
function truncateThreadResultText(value, limit = THREAD_RESULT_TEXT_LIMIT) {
    return value.length > limit
        ? `${value.slice(0, limit)}\n\n[truncated — ${value.length} chars total]`
        : value;
}
function normalizeSchedulerDeliveryPrompt(message, thread, context) {
    const worktreePath = thread.workingDirectory?.trim();
    const projectRoot = context.projectRoot?.trim();
    if (context.requestedBy !== "scheduler" || thread.kind !== "delivery" || !worktreePath) {
        return message;
    }
    const rewrittenMessage = projectRoot && projectRoot !== worktreePath
        ? message.split(projectRoot).join(worktreePath)
        : message;
    const originalPathLine = projectRoot && projectRoot !== worktreePath
        ? `Do not modify the scheduler source project directly: ${projectRoot}`
        : "Do not modify any other checkout unless explicitly asked.";
    return [
        "<jaitManagedWorktree>",
        "Jait created this scheduled delivery thread in an isolated git worktree.",
        `Use this working directory for all file reads, edits, commands, tests, commits, and relative paths: ${worktreePath}`,
        originalPathLine,
        "If any earlier instruction mentions a different project path, treat this managed worktree as the active project.",
        "</jaitManagedWorktree>",
        "",
        rewrittenMessage,
    ].join("\n");
}
export function createThreadControlTool(deps) {
    const gitService = deps.gitService ?? new GitService();
    const broadcastThreadEvent = (threadId, event, data) => {
        if (!deps.ws)
            return;
        deps.ws.broadcastAll({
            type: `thread.${event}`,
            sessionId: "",
            timestamp: new Date().toISOString(),
            payload: { threadId, ...data },
        });
    };
    const broadcastThreadStatus = (threadId, status, data = {}) => {
        const thread = deps.threadService.getById(threadId);
        broadcastThreadEvent(threadId, "status", {
            status,
            ...(thread ? { thread } : {}),
            ...data,
        });
    };
    const ensureUserId = (context) => {
        const userId = context.userId?.trim();
        return userId || "system";
    };
    const createManagedDeliveryWorktree = async (thread) => {
        if (thread.kind !== "delivery")
            return thread;
        if (thread.branch)
            return thread;
        const cwd = thread.workingDirectory?.trim();
        if (!cwd)
            return thread;
        if (!existsSync(cwd))
            return thread;
        if (!gitService.isRepo || !gitService.createWorktree)
            return thread;
        const isRepo = await gitService.isRepo(cwd).catch(() => false);
        if (!isRepo)
            return thread;
        let remoteUrl = null;
        if (gitService.getPreferredRemote && gitService.getRemoteUrl) {
            const remote = await gitService.getPreferredRemote(cwd).catch(() => null);
            if (remote) {
                remoteUrl = await gitService.getRemoteUrl(cwd, remote).catch(() => null);
            }
        }
        const defaultBranch = gitService.resolveDefaultBranch
            ? await gitService.resolveDefaultBranch(cwd, remoteUrl).catch(() => "main")
            : "main";
        const branch = `jait/${thread.id.slice(-8)}`;
        const worktree = await gitService.createWorktree(cwd, defaultBranch, branch);
        return deps.threadService.update(thread.id, {
            workingDirectory: worktree.path,
            branch: worktree.branch,
            prBaseBranch: defaultBranch,
        }) ?? thread;
    };
    const resolveSelectedThreadDefaults = (context) => {
        const defaults = resolveThreadSelectionDefaults({
            userId: context.userId,
            sessionId: context.sessionId,
            userService: deps.userService,
            sessionState: deps.sessionState,
        });
        const contextProviderId = normalizeThreadProviderId(context.providerId);
        return {
            providerId: contextProviderId ?? (defaults.providerId ? normalizeThreadProviderId(defaults.providerId) : null),
            model: (typeof context.model === "string" && context.model.trim() ? context.model.trim() : undefined) ?? defaults.model,
            runtimeMode: (context.runtimeMode === "full-access" || context.runtimeMode === "supervised" ? context.runtimeMode : undefined)
                ?? defaults.runtimeMode,
        };
    };
    const resolveProviderId = (context, requestedProviderId) => {
        const resolveRegisteredProvider = (providerId, label) => deps.providerRegistry.get(providerId)
            ? { providerId }
            : { error: `${label} '${providerId}' is not registered on this gateway.` };
        const requestedProvider = normalizeThreadProviderId(requestedProviderId);
        if (requestedProvider) {
            return resolveRegisteredProvider(requestedProvider, "The requested provider");
        }
        const selectedProvider = resolveSelectedThreadDefaults(context).providerId;
        if (!selectedProvider) {
            return {
                error: "No provider could be resolved for this thread. A selected user provider must be configured and supported.",
            };
        }
        return resolveRegisteredProvider(selectedProvider, "The selected user provider");
    };
    const preflightScheduledProvider = async (providerId) => {
        const provider = deps.providerRegistry.get(providerId);
        if (!provider) {
            return { ok: false, message: `Provider '${providerId}' is not registered on this gateway.` };
        }
        const available = await provider.checkAvailability();
        if (!available) {
            return {
                ok: false,
                message: `Provider '${providerId}' is not available: ${provider.info.unavailableReason ?? "unknown reason"}`,
            };
        }
        if (!provider.listModels) {
            return { ok: true };
        }
        try {
            await provider.listModels();
            return { ok: true };
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            return {
                ok: false,
                message: `Provider '${providerId}' did not pass scheduled job preflight before thread creation: ${message}`,
            };
        }
    };
    const preflightScheduledProviders = async (providerIds) => {
        for (const providerId of [...new Set(providerIds)]) {
            const result = await preflightScheduledProvider(providerId);
            if (!result.ok)
                return result;
        }
        return { ok: true };
    };
    const resolveStoredThreadProvider = (threadProviderId, context) => {
        const storedProvider = normalizeThreadProviderId(threadProviderId);
        if (!storedProvider) {
            return {
                error: `Thread provider '${String(threadProviderId)}' is not supported for agent threads.`,
            };
        }
        const contextProvider = normalizeThreadProviderId(context.providerId);
        if (contextProvider && contextProvider !== storedProvider) {
            return {
                error: `Thread provider '${storedProvider}' does not match the calling agent provider '${contextProvider}'.`,
            };
        }
        return deps.providerRegistry.get(storedProvider)
            ? { providerId: storedProvider }
            : { error: `Provider '${storedProvider}' is not registered on this gateway.` };
    };
    const getAccessibleThread = (threadId, userId) => {
        const thread = deps.threadService.getById(threadId);
        if (!thread)
            return undefined;
        if (thread.userId && thread.userId !== userId)
            return undefined;
        return thread;
    };
    const waitForThreadCompletion = async (threadId, context) => {
        while (!context.signal?.aborted) {
            const thread = deps.threadService.getById(threadId);
            if (!thread)
                return undefined;
            if (THREAD_TERMINAL_STATUSES.has(thread.status))
                return thread;
            await sleep(THREAD_WAIT_POLL_MS);
        }
        return deps.threadService.getById(threadId);
    };
    const summarizeThreadForToolResult = (thread) => {
        const activities = deps.threadService
            .getActivities(thread.id, THREAD_RESULT_ACTIVITY_LIMIT)
            .reverse();
        const assistantMessages = activities.flatMap((activity) => {
            const payload = activity.payload;
            if (payload?.role !== "assistant" || typeof payload.content !== "string") {
                return [];
            }
            const content = payload.content.trim();
            return content ? [truncateThreadResultText(content)] : [];
        });
        const noteworthyActivities = activities
            .filter((activity) => (activity.kind === "error" ||
            activity.kind === "tool.error" ||
            activity.kind === "session"))
            .map((activity) => ({
            kind: activity.kind,
            summary: truncateThreadResultText(activity.summary, 1_000),
            createdAt: activity.createdAt,
        }));
        return {
            threadId: thread.id,
            title: thread.title,
            status: thread.status,
            completedAt: thread.completedAt,
            error: thread.error,
            finalMessage: assistantMessages.at(-1) ?? null,
            assistantMessages,
            noteworthyActivities,
        };
    };
    const generateAndApplyThreadTitle = async (context, thread, provider, providerId, providerSessionId, task) => {
        const titleTask = task.trim();
        if (!titleTask)
            return;
        try {
            let generatedTitle = "";
            if (providerId === "codex" || providerId === "claude-code") {
                const raw = await generateTitleViaTurn(provider, providerSessionId, titleTask);
                generatedTitle = normalizeGeneratedThreadTitle(raw, "");
            }
            else if (deps.config) {
                const settings = context.userId ? deps.userService?.getSettings(context.userId) : undefined;
                generatedTitle = await generateTitleViaApi({
                    task: titleTask,
                    config: deps.config,
                    apiKeys: settings?.apiKeys,
                    model: thread.model ?? undefined,
                    jaitBackend: providerId === "jait" ? settings?.jaitBackend : undefined,
                });
            }
            if (!generatedTitle)
                return;
            const updated = deps.threadService.update(thread.id, { title: generatedTitle });
            if (updated)
                broadcastThreadEvent(thread.id, "updated", { thread: updated });
        }
        catch {
            // Scheduled title generation is best-effort; the actual job must still run.
        }
    };
    const startThread = async (context, thread, options = {}) => {
        const rawMessage = normalizePrompt(options.message);
        if (thread.status === "running" && thread.providerSessionId) {
            return { ok: false, message: "Thread is already running" };
        }
        if (!rawMessage) {
            return { ok: false, message: "start requires non-empty `prompt`." };
        }
        const resolvedProvider = resolveStoredThreadProvider(thread.providerId, context);
        if (!resolvedProvider.providerId) {
            return { ok: false, message: resolvedProvider.error ?? "Unable to resolve a provider for this thread." };
        }
        const effectiveProviderId = resolvedProvider.providerId;
        const effectiveThread = thread;
        const autoFinishAfterFirstTurn = effectiveThread.kind === "delegation" ||
            options.autoStopAfterTurn === true ||
            context.requestedBy === "scheduler";
        const detach = options.detach === true || context.requestedBy === "scheduler";
        const timeoutMs = normalizeTimeoutMs(options.timeoutMinutes, context.requestedBy === "scheduler" ? 240 : undefined);
        const provider = deps.providerRegistry.get(effectiveProviderId);
        if (!provider) {
            return { ok: false, message: `Provider '${effectiveProviderId}' not found` };
        }
        const message = normalizeSchedulerDeliveryPrompt(rawMessage, effectiveThread, context);
        const available = await provider.checkAvailability();
        if (!available) {
            return { ok: false, message: `Provider '${effectiveProviderId}' is not available: ${provider.info.unavailableReason ?? "unknown reason"}` };
        }
        const mcpServers = deps.mcpConfig
            ? [deps.providerRegistry.buildJaitMcpServerRef(deps.mcpConfig)]
            : undefined;
        try {
            const session = await provider.startSession({
                threadId: effectiveThread.id,
                workingDirectory: effectiveThread.workingDirectory ?? process.cwd(),
                mode: effectiveThread.runtimeMode ?? "full-access",
                model: effectiveThread.model ?? context.model ?? undefined,
                mcpServers,
            });
            if (options.generateTitleFromPrompt) {
                await generateAndApplyThreadTitle(context, effectiveThread, provider, effectiveProviderId, session.id, rawMessage);
            }
            let timeout;
            let unsubscribed = false;
            let unsubscribe;
            let pendingAssistantContent = "";
            const cleanup = () => {
                if (timeout) {
                    clearTimeout(timeout);
                    timeout = undefined;
                }
                if (unsubscribe && !unsubscribed) {
                    unsubscribed = true;
                    unsubscribe();
                }
            };
            const flushPendingAssistantContent = () => {
                if (!pendingAssistantContent.trim()) {
                    pendingAssistantContent = "";
                    return;
                }
                const content = pendingAssistantContent;
                pendingAssistantContent = "";
                const activity = deps.threadService.addActivity(effectiveThread.id, "message", content.trim().slice(0, 500), {
                    role: "assistant",
                    content,
                });
                broadcastThreadEvent(effectiveThread.id, "activity", {
                    event: { type: "message", sessionId: session.id, role: "assistant", content },
                    activity,
                });
            };
            unsubscribe = provider.onEvent((event) => {
                if (event.sessionId !== session.id) {
                    return;
                }
                if (event.type === "token") {
                    pendingAssistantContent += event.content;
                    return;
                }
                if (event.type === "message" && event.role === "assistant" && pendingAssistantContent) {
                    if (event.content === pendingAssistantContent || event.content.startsWith(pendingAssistantContent)) {
                        pendingAssistantContent = "";
                    }
                    else {
                        flushPendingAssistantContent();
                    }
                }
                else if (event.type !== "turn.started") {
                    flushPendingAssistantContent();
                }
                const activity = deps.threadService.logProviderEvent(effectiveThread.id, event);
                if (activity) {
                    broadcastThreadEvent(effectiveThread.id, "activity", { event, activity });
                }
                // Emit dedicated todo activity so the frontend can track the list in real time
                const todoItems = event.type === "tool.result" && event.ok
                    ? extractTodoResultItems(event.tool, event.data)
                    : null;
                if (todoItems) {
                    const todoActivity = deps.threadService.addActivity(effectiveThread.id, "todo", "Todo list updated", { items: todoItems });
                    broadcastThreadEvent(effectiveThread.id, "activity", { activity: todoActivity });
                }
                if (event.type === "session.completed") {
                    if (autoFinishAfterFirstTurn) {
                        deps.threadService.markCompletedAndClearSession(effectiveThread.id);
                    }
                    else {
                        deps.threadService.markCompleted(effectiveThread.id);
                    }
                    broadcastThreadStatus(effectiveThread.id, "completed");
                    cleanup();
                }
                else if (event.type === "session.error") {
                    deps.threadService.markError(effectiveThread.id, event.error);
                    broadcastThreadStatus(effectiveThread.id, "error", { error: event.error });
                    cleanup();
                }
                else if (event.type === "turn.started") {
                    // Re-assert running when a new turn begins
                    const cur = deps.threadService.getById(effectiveThread.id);
                    if (cur && cur.status !== "running") {
                        deps.threadService.update(effectiveThread.id, { status: "running", error: null, completedAt: null });
                        broadcastThreadStatus(effectiveThread.id, "running");
                    }
                }
                else if (event.type === "turn.completed") {
                    if (autoFinishAfterFirstTurn) {
                        void provider.stopSession(session.id).catch(() => {
                            // Best effort: one-shot threads should not linger if the provider
                            // leaves the session open after answering the first task.
                        });
                        deps.threadService.markCompletedAndClearSession(effectiveThread.id);
                        cleanup();
                    }
                    else {
                        deps.threadService.markCompleted(effectiveThread.id);
                    }
                    broadcastThreadStatus(effectiveThread.id, "completed");
                }
            });
            if (timeoutMs) {
                timeout = setTimeout(() => {
                    void provider.stopSession(session.id).catch(() => { });
                    const activity = deps.threadService.addActivity(effectiveThread.id, "session", `Thread stopped after ${Math.round(timeoutMs / 60_000)} minute timeout`, { timeoutMs });
                    broadcastThreadEvent(effectiveThread.id, "activity", { activity });
                    deps.threadService.markInterrupted(effectiveThread.id);
                    broadcastThreadStatus(effectiveThread.id, "interrupted");
                    cleanup();
                }, timeoutMs);
                if (typeof timeout === "object" && "unref" in timeout && typeof timeout.unref === "function") {
                    timeout.unref();
                }
            }
            deps.threadService.markRunning(effectiveThread.id, session.id);
            broadcastThreadStatus(effectiveThread.id, "running");
            const historyReplayPrompt = buildThreadHistoryReplayPrompt(deps.threadService, effectiveThread.id);
            const userActivity = deps.threadService.addActivity(effectiveThread.id, "message", message.slice(0, 500), { role: "user" });
            broadcastThreadEvent(effectiveThread.id, "activity", { activity: userActivity });
            // Run thread router for the first turn
            let turnMessage = message;
            if (historyReplayPrompt) {
                turnMessage = `${historyReplayPrompt}\n\n${turnMessage}`;
            }
            if (deps.skillRegistry) {
                const availableSkills = deps.skillRegistry.listEnabled();
                const routingPlan = routeThread({
                    message,
                    availableSkills,
                    pinnedSkillIds: effectiveThread.skillIds ? (typeof effectiveThread.skillIds === "string" ? JSON.parse(effectiveThread.skillIds) : effectiveThread.skillIds) : null,
                    kind: effectiveThread.kind,
                });
                deps.threadService.update(effectiveThread.id, { routingPlan });
                broadcastThreadEvent(effectiveThread.id, "updated", { thread: deps.threadService.getById(effectiveThread.id) });
                turnMessage = `${formatRoutingPlanForPrompt(routingPlan, message.length)}\n\n${message}`;
            }
            const sendTurn = async () => {
                try {
                    await provider.sendTurn(session.id, turnMessage, options.attachments);
                }
                catch (err) {
                    const errorMessage = err instanceof Error ? err.message : String(err);
                    deps.threadService.markError(effectiveThread.id, errorMessage);
                    broadcastThreadStatus(effectiveThread.id, "error", { error: errorMessage });
                    cleanup();
                    if (!detach)
                        throw err;
                }
            };
            if (detach) {
                void sendTurn();
            }
            else {
                await sendTurn();
            }
            const updated = deps.threadService.getById(effectiveThread.id) ?? effectiveThread;
            return { ok: true, message: detach ? "Thread started in background" : "Thread started", thread: updated };
        }
        catch (err) {
            const errorMessage = err instanceof Error ? err.message : String(err);
            deps.threadService.markError(effectiveThread.id, errorMessage);
            broadcastThreadStatus(effectiveThread.id, "error", { error: errorMessage });
            return { ok: false, message: errorMessage };
        }
    };
    return {
        name: ToolName.ThreadControl,
        description: "Control agent threads end-to-end: create/list/update/delete threads, run them in parallel, " +
            "send turns, stop/interrupt, and create pull requests with direct links. " +
            "Threads have two kinds: delivery threads are user-owned work items that can be completed, resumed, reviewed, and used to create PRs; " +
            "delegation threads are helper/sub-agent tasks that auto-finish after one turn and cannot create PRs. " +
            "Creating or starting a thread requires a non-empty prompt. Scheduled jobs should create delivery threads with detach=true so the scheduler returns after dispatching the turn.",
        tier: "standard",
        category: "agent",
        source: "builtin",
        parameters: {
            type: "object",
            properties: {
                action: {
                    type: "string",
                    description: "Action: list, get, create, create_many, update, delete, start, send, stop, interrupt, approve, activities, create_pr.",
                    enum: [
                        "list",
                        "get",
                        "create",
                        "create_many",
                        "update",
                        "delete",
                        "start",
                        "send",
                        "stop",
                        "interrupt",
                        "approve",
                        "activities",
                        "create_pr",
                    ],
                },
                threadId: { type: "string", description: "Thread ID for thread-specific actions." },
                sessionId: { type: "string", description: "Optional session filter or session assignment on create." },
                title: { type: "string", description: "Thread title (create/update)." },
                providerId: { type: "string", description: "Optional provider override for create/create_many." },
                model: { type: "string", description: "Optional provider model." },
                runtimeMode: { type: "string", enum: ["full-access", "supervised"], description: "Execution mode for thread runs." },
                kind: {
                    type: "string",
                    enum: ["delivery", "delegation"],
                    description: "Thread kind. Use delivery for user-owned work that can be reviewed, resumed, merged, or turned into a PR. " +
                        "Use delegation for helper/sub-agent work; delegation threads auto-finish after one turn and cannot create PRs.",
                },
                workingDirectory: { type: "string", description: "Working directory for the thread." },
                branch: { type: "string", description: "Git branch metadata for the thread." },
                prompt: {
                    type: "string",
                    description: "Required prompt for create/start/send. Prefer this over `message`; `message` is kept as a backward-compatible alias.",
                },
                message: { type: "string", description: "Backward-compatible alias for `prompt`." },
                attachments: {
                    type: "array",
                    description: "Optional attachments for provider sendTurn.",
                    items: { type: "string" },
                },
                start: { type: "boolean", description: "For create/create_many: auto-start thread(s) after creation. Requires `prompt`." },
                detach: {
                    type: "boolean",
                    description: "Return after dispatching the first turn instead of waiting for provider.sendTurn. " +
                        "Use true for scheduled/cron-created delivery threads so the job creates a normal user-visible thread without blocking the scheduler. Scheduler-triggered starts detach automatically.",
                },
                autoStopAfterTurn: {
                    type: "boolean",
                    description: "Stop the provider session and clear resumability after the first turn completes. " +
                        "Delegation threads always do this. Leave false for normal delivery threads that the user may review, resume, merge, or delete later.",
                },
                timeoutMinutes: {
                    type: "number",
                    description: "Maximum runtime before stopping the provider session. Scheduler-triggered starts default to 240 minutes.",
                },
                threads: {
                    type: "array",
                    description: "For create_many: array of thread specs to create in one call.",
                    items: {
                        type: "object",
                        properties: {
                            title: { type: "string" },
                            providerId: { type: "string" },
                            model: { type: "string" },
                            runtimeMode: { type: "string", enum: ["full-access", "supervised"] },
                            kind: { type: "string", enum: ["delivery", "delegation"] },
                            workingDirectory: { type: "string" },
                            branch: { type: "string" },
                            sessionId: { type: "string" },
                            start: { type: "boolean" },
                            prompt: { type: "string" },
                            message: { type: "string" },
                            attachments: { type: "array", items: { type: "string" } },
                            detach: { type: "boolean" },
                            autoStopAfterTurn: { type: "boolean" },
                            timeoutMinutes: { type: "number" },
                        },
                        required: ["title"],
                    },
                },
                requestId: { type: "string", description: "Approval request ID for approve action." },
                approved: { type: "boolean", description: "Approval decision (default true)." },
                limit: { type: "number", description: "Max activities to return (default 100, max 500)." },
                prUrl: { type: "string", description: "PR URL metadata for update." },
                prNumber: { type: "number", description: "PR number metadata for update." },
                prTitle: { type: "string", description: "PR title metadata for update." },
                prBaseBranch: { type: "string", description: "PR base branch metadata for update." },
                prState: { type: "string", enum: ["creating", "open", "closed", "merged"], description: "PR state metadata for update." },
                cwd: { type: "string", description: "Repo path for create_pr action. Defaults to thread working directory." },
                gitAction: { type: "string", enum: ["commit", "commit_push", "commit_push_pr"], description: "Git stacked action for create_pr (default commit_push_pr)." },
                commitMessage: { type: "string", description: "Commit/PR title override for create_pr." },
                baseBranch: { type: "string", description: "Base branch for PR creation." },
                featureBranch: { type: "boolean", description: "Whether to create a feature branch before committing." },
            },
            required: ["action"],
        },
        async execute(input, context) {
            const userId = ensureUserId(context);
            try {
                switch (input.action) {
                    case "list": {
                        const threads = (input.sessionId
                            ? deps.threadService.listBySession(input.sessionId)
                            : deps.threadService.list(userId))
                            .filter((t) => !t.userId || t.userId === userId);
                        return {
                            ok: true,
                            message: `${threads.length} thread(s)`,
                            data: { threads },
                        };
                    }
                    case "get": {
                        if (!input.threadId)
                            return { ok: false, message: "get requires `threadId`." };
                        const thread = getAccessibleThread(input.threadId, userId);
                        if (!thread)
                            return { ok: false, message: "Thread not found." };
                        return { ok: true, message: "Thread loaded", data: { thread } };
                    }
                    case "create": {
                        const prompt = normalizePrompt(input.message, input.prompt);
                        if (!prompt) {
                            return { ok: false, message: "create requires non-empty `prompt`." };
                        }
                        const resolvedProvider = resolveProviderId(context, input.providerId);
                        if (!resolvedProvider.providerId) {
                            return { ok: false, message: resolvedProvider.error ?? "Unable to resolve a provider for this thread." };
                        }
                        const selectedProviderId = resolvedProvider.providerId;
                        const selectedDefaults = resolveSelectedThreadDefaults(context);
                        if (context.requestedBy === "scheduler") {
                            const preflight = await preflightScheduledProvider(selectedProviderId);
                            if (!preflight.ok)
                                return preflight;
                        }
                        let thread = deps.threadService.create({
                            userId,
                            sessionId: input.sessionId,
                            title: input.title?.trim() || normalizeGeneratedThreadTitle(prompt, "New Thread"),
                            providerId: selectedProviderId,
                            model: input.model ?? selectedDefaults.model,
                            runtimeMode: input.runtimeMode ?? selectedDefaults.runtimeMode ?? "full-access",
                            kind: input.kind === "delivery" ? "delivery" : "delegation",
                            workingDirectory: input.workingDirectory,
                            branch: input.branch,
                        });
                        broadcastThreadEvent(thread.id, "created", { thread });
                        try {
                            const isolatedThread = await createManagedDeliveryWorktree(thread);
                            if (isolatedThread.id === thread.id && isolatedThread.workingDirectory !== thread.workingDirectory) {
                                thread = isolatedThread;
                                broadcastThreadEvent(thread.id, "updated", { thread });
                            }
                        }
                        catch (err) {
                            const message = err instanceof Error ? err.message : String(err);
                            deps.threadService.markError(thread.id, message);
                            broadcastThreadStatus(thread.id, "error", { error: message });
                            return {
                                ok: false,
                                message: `Thread created but failed to create delivery worktree: ${message}`,
                                data: { thread: deps.threadService.getById(thread.id) ?? thread },
                            };
                        }
                        if (!input.start) {
                            const activity = deps.threadService.addActivity(thread.id, "message", prompt.slice(0, 500), { role: "user" });
                            broadcastThreadEvent(thread.id, "activity", { activity });
                            return { ok: true, message: "Thread created", data: { thread } };
                        }
                        const started = await startThread(context, thread, {
                            message: prompt,
                            attachments: input.attachments,
                            detach: input.detach,
                            autoStopAfterTurn: input.autoStopAfterTurn,
                            timeoutMinutes: input.timeoutMinutes,
                            generateTitleFromPrompt: context.requestedBy === "scheduler" &&
                                input.kind === "delivery" &&
                                shouldGenerateSchedulerThreadTitle(input.title),
                        });
                        if (!started.ok) {
                            return {
                                ok: false,
                                message: `Thread created but failed to start: ${started.message}`,
                                data: { thread },
                            };
                        }
                        return {
                            ok: true,
                            message: "Thread created and started",
                            data: { thread: started.thread ?? thread },
                        };
                    }
                    case "create_many": {
                        if (!input.threads || input.threads.length === 0) {
                            return { ok: false, message: "create_many requires non-empty `threads`." };
                        }
                        const missingPrompt = input.threads.find((spec) => {
                            const prompt = normalizePrompt(spec.message, spec.prompt) ?? normalizePrompt(input.message, input.prompt);
                            return !prompt;
                        });
                        if (missingPrompt) {
                            return { ok: false, message: `create_many requires non-empty prompt for thread '${missingPrompt.title}'.` };
                        }
                        const validatedSpecs = input.threads.map((spec) => ({
                            spec,
                            resolvedProvider: resolveProviderId(context, spec.providerId ?? input.providerId),
                        }));
                        const firstResolutionError = validatedSpecs.find(({ resolvedProvider }) => !resolvedProvider.providerId);
                        if (firstResolutionError) {
                            return {
                                ok: false,
                                message: firstResolutionError.resolvedProvider.error ?? `Unable to resolve a provider for thread '${firstResolutionError.spec.title}'.`,
                            };
                        }
                        if (context.requestedBy === "scheduler") {
                            const preflight = await preflightScheduledProviders(validatedSpecs.flatMap(({ resolvedProvider }) => resolvedProvider.providerId ? [resolvedProvider.providerId] : []));
                            if (!preflight.ok)
                                return preflight;
                        }
                        const created = await Promise.all(validatedSpecs.map(async ({ spec, resolvedProvider }) => {
                            const selectedProviderId = resolvedProvider.providerId;
                            const selectedDefaults = resolveSelectedThreadDefaults(context);
                            const prompt = (normalizePrompt(spec.message, spec.prompt) ?? normalizePrompt(input.message, input.prompt));
                            let thread = deps.threadService.create({
                                userId,
                                sessionId: spec.sessionId ?? input.sessionId,
                                title: spec.title?.trim() || "New Thread",
                                providerId: selectedProviderId,
                                model: spec.model ?? input.model ?? selectedDefaults.model,
                                runtimeMode: spec.runtimeMode ?? input.runtimeMode ?? selectedDefaults.runtimeMode ?? "full-access",
                                kind: spec.kind === "delivery" ? "delivery" : input.kind === "delivery" ? "delivery" : "delegation",
                                workingDirectory: spec.workingDirectory ?? input.workingDirectory ?? context.projectRoot,
                                branch: spec.branch ?? input.branch,
                            });
                            broadcastThreadEvent(thread.id, "created", { thread });
                            try {
                                const isolatedThread = await createManagedDeliveryWorktree(thread);
                                if (isolatedThread.id === thread.id && isolatedThread.workingDirectory !== thread.workingDirectory) {
                                    thread = isolatedThread;
                                    broadcastThreadEvent(thread.id, "updated", { thread });
                                }
                            }
                            catch (err) {
                                const message = err instanceof Error ? err.message : String(err);
                                deps.threadService.markError(thread.id, message);
                                broadcastThreadStatus(thread.id, "error", { error: message });
                                return { thread: deps.threadService.getById(thread.id) ?? thread, spec, prompt, worktreeError: message };
                            }
                            return { thread, spec, prompt };
                        }));
                        const worktreeFailures = created.filter((entry) => entry.worktreeError);
                        if (worktreeFailures.length > 0) {
                            return {
                                ok: false,
                                message: `Created ${created.length} thread(s), ${worktreeFailures.length} failed to create delivery worktrees.`,
                                data: {
                                    threads: created.map((entry) => entry.thread),
                                    failedWorktrees: worktreeFailures.map((entry) => ({
                                        threadId: entry.thread.id,
                                        message: entry.worktreeError,
                                    })),
                                },
                            };
                        }
                        const startTargets = created.filter(({ spec }) => spec.start === true || input.start === true);
                        const startResults = await Promise.all(startTargets.map(async ({ thread, spec, prompt }) => {
                            const started = await startThread(context, thread, {
                                message: prompt,
                                attachments: spec.attachments ?? input.attachments,
                                detach: spec.detach ?? input.detach,
                                autoStopAfterTurn: spec.autoStopAfterTurn ?? input.autoStopAfterTurn,
                                timeoutMinutes: spec.timeoutMinutes ?? input.timeoutMinutes,
                                generateTitleFromPrompt: context.requestedBy === "scheduler" &&
                                    (spec.kind === "delivery" || input.kind === "delivery") &&
                                    shouldGenerateSchedulerThreadTitle(spec.title ?? input.title),
                            });
                            return {
                                threadId: thread.id,
                                ok: started.ok,
                                message: started.message,
                                thread: started.thread ?? thread,
                            };
                        }));
                        const failedStarts = startResults.filter((r) => !r.ok);
                        const threadById = new Map(created.map((entry) => [entry.thread.id, entry.thread]));
                        for (const started of startResults) {
                            threadById.set(started.threadId, started.thread);
                        }
                        const waitTargets = startResults
                            .filter((result) => result.ok)
                            .flatMap((result) => {
                            const source = created.find((entry) => entry.thread.id === result.threadId);
                            const shouldDetach = source?.spec.detach ?? input.detach;
                            return shouldDetach === true ? [] : [result.threadId];
                        });
                        if (waitTargets.length > 0) {
                            context.onOutputChunk?.(`Waiting for ${waitTargets.length} thread(s) to finish...\n`);
                            const completedThreads = await Promise.all(waitTargets.map((threadId) => waitForThreadCompletion(threadId, context)));
                            for (const thread of completedThreads) {
                                if (thread)
                                    threadById.set(thread.id, thread);
                            }
                        }
                        for (const { thread, prompt, spec } of created) {
                            if (spec.start === true || input.start === true)
                                continue;
                            const activity = deps.threadService.addActivity(thread.id, "message", prompt.slice(0, 500), { role: "user" });
                            broadcastThreadEvent(thread.id, "activity", { activity });
                        }
                        const threads = [...threadById.values()];
                        const terminalFailures = threads.filter((thread) => thread.status === "error" || thread.status === "interrupted");
                        const threadSummaries = threads.map((thread) => summarizeThreadForToolResult(thread));
                        return {
                            ok: failedStarts.length === 0 && terminalFailures.length === 0,
                            message: failedStarts.length > 0
                                ? `Created ${threads.length} thread(s), ${failedStarts.length} failed to start.`
                                : terminalFailures.length > 0
                                    ? `Created ${threads.length} thread(s), ${terminalFailures.length} did not complete successfully.`
                                    : waitTargets.length > 0
                                        ? `Created ${threads.length} thread(s) and waited for ${waitTargets.length} to finish.`
                                        : `Created ${threads.length} thread(s).`,
                            data: {
                                threads,
                                startedCount: startResults.length - failedStarts.length,
                                failedStarts,
                                waitedForCompletionCount: waitTargets.length,
                                threadSummaries,
                            },
                        };
                    }
                    case "update": {
                        if (!input.threadId)
                            return { ok: false, message: "update requires `threadId`." };
                        const existing = getAccessibleThread(input.threadId, userId);
                        if (!existing)
                            return { ok: false, message: "Thread not found." };
                        const thread = deps.threadService.update(input.threadId, {
                            title: input.title,
                            model: input.model,
                            runtimeMode: input.runtimeMode,
                            kind: input.kind === "delegation" ? "delegation" : input.kind === "delivery" ? "delivery" : undefined,
                            workingDirectory: input.workingDirectory,
                            branch: input.branch,
                            prUrl: typeof input.prUrl === "string" ? input.prUrl : input.prUrl === null ? null : undefined,
                            prNumber: typeof input.prNumber === "number" ? input.prNumber : input.prNumber === null ? null : undefined,
                            prTitle: typeof input.prTitle === "string" ? input.prTitle : input.prTitle === null ? null : undefined,
                            prBaseBranch: typeof input.prBaseBranch === "string" ? input.prBaseBranch : input.prBaseBranch === null ? null : undefined,
                            prState: input.prState === "creating" || input.prState === "open" || input.prState === "closed" || input.prState === "merged"
                                ? input.prState
                                : input.prState === null
                                    ? null
                                    : undefined,
                        });
                        if (!thread)
                            return { ok: false, message: "Thread not found after update." };
                        broadcastThreadEvent(thread.id, "updated", { thread });
                        return { ok: true, message: "Thread updated", data: { thread } };
                    }
                    case "delete": {
                        if (!input.threadId)
                            return { ok: false, message: "delete requires `threadId`." };
                        const thread = getAccessibleThread(input.threadId, userId);
                        if (!thread)
                            return { ok: false, message: "Thread not found." };
                        if (thread.status === "running" && thread.providerSessionId) {
                            const provider = deps.providerRegistry.get(thread.providerId);
                            if (provider) {
                                try {
                                    await provider.stopSession(thread.providerSessionId);
                                }
                                catch {
                                    // Best effort stop before delete.
                                }
                            }
                        }
                        // Clean up the worktree and branch on disk (best effort, don't block delete)
                        if (thread.workingDirectory) {
                            cleanupWorktreeRemoteAware(thread.workingDirectory, deps.ws, thread.branch).catch(() => { });
                        }
                        deps.threadService.delete(thread.id);
                        broadcastThreadEvent(thread.id, "deleted", {});
                        return { ok: true, message: "Thread deleted", data: { threadId: thread.id } };
                    }
                    case "start": {
                        if (!input.threadId)
                            return { ok: false, message: "start requires `threadId`." };
                        const prompt = normalizePrompt(input.message, input.prompt);
                        if (!prompt)
                            return { ok: false, message: "start requires non-empty `prompt`." };
                        const thread = getAccessibleThread(input.threadId, userId);
                        if (!thread)
                            return { ok: false, message: "Thread not found." };
                        const started = await startThread(context, thread, {
                            message: prompt,
                            attachments: input.attachments,
                            detach: input.detach,
                            autoStopAfterTurn: input.autoStopAfterTurn,
                            timeoutMinutes: input.timeoutMinutes,
                        });
                        return {
                            ok: started.ok,
                            message: started.message,
                            data: started.thread ? { thread: started.thread } : undefined,
                        };
                    }
                    case "send": {
                        if (!input.threadId)
                            return { ok: false, message: "send requires `threadId`." };
                        const prompt = normalizePrompt(input.message, input.prompt);
                        if (!prompt)
                            return { ok: false, message: "send requires non-empty `prompt`." };
                        const thread = getAccessibleThread(input.threadId, userId);
                        if (!thread)
                            return { ok: false, message: "Thread not found." };
                        if (!thread.providerSessionId) {
                            return { ok: false, message: "Thread has no active session." };
                        }
                        const provider = deps.providerRegistry.get(thread.providerId);
                        if (!provider)
                            return { ok: false, message: `Provider '${thread.providerId}' not found` };
                        deps.threadService.update(thread.id, { status: "running", error: null, completedAt: null });
                        broadcastThreadStatus(thread.id, "running");
                        await provider.sendTurn(thread.providerSessionId, prompt, input.attachments);
                        const activity = deps.threadService.addActivity(thread.id, "message", prompt.slice(0, 500), { role: "user" });
                        broadcastThreadEvent(thread.id, "activity", { activity });
                        return { ok: true, message: "Turn sent", data: { threadId: thread.id } };
                    }
                    case "stop": {
                        if (!input.threadId)
                            return { ok: false, message: "stop requires `threadId`." };
                        const thread = getAccessibleThread(input.threadId, userId);
                        if (!thread)
                            return { ok: false, message: "Thread not found." };
                        if (thread.providerSessionId) {
                            const provider = deps.providerRegistry.get(thread.providerId);
                            if (provider) {
                                try {
                                    await provider.stopSession(thread.providerSessionId);
                                }
                                catch {
                                    // Best effort stop.
                                }
                            }
                        }
                        deps.threadService.markInterrupted(thread.id);
                        broadcastThreadStatus(thread.id, "interrupted");
                        const updated = deps.threadService.getById(thread.id) ?? thread;
                        return { ok: true, message: "Thread stopped", data: { thread: updated } };
                    }
                    case "interrupt": {
                        if (!input.threadId)
                            return { ok: false, message: "interrupt requires `threadId`." };
                        const thread = getAccessibleThread(input.threadId, userId);
                        if (!thread)
                            return { ok: false, message: "Thread not found." };
                        if (!thread.providerSessionId)
                            return { ok: false, message: "Thread has no active session." };
                        const provider = deps.providerRegistry.get(thread.providerId);
                        if (!provider)
                            return { ok: false, message: `Provider '${thread.providerId}' not found` };
                        await provider.interruptTurn(thread.providerSessionId);
                        return { ok: true, message: "Thread turn interrupted", data: { threadId: thread.id } };
                    }
                    case "approve": {
                        if (!input.threadId)
                            return { ok: false, message: "approve requires `threadId`." };
                        if (!input.requestId?.trim())
                            return { ok: false, message: "approve requires `requestId`." };
                        const thread = getAccessibleThread(input.threadId, userId);
                        if (!thread)
                            return { ok: false, message: "Thread not found." };
                        if (!thread.providerSessionId)
                            return { ok: false, message: "Thread has no active session." };
                        const provider = deps.providerRegistry.get(thread.providerId);
                        if (!provider)
                            return { ok: false, message: `Provider '${thread.providerId}' not found` };
                        await provider.respondToApproval(thread.providerSessionId, input.requestId, input.approved !== false);
                        return { ok: true, message: "Approval response sent", data: { threadId: thread.id } };
                    }
                    case "activities": {
                        if (!input.threadId)
                            return { ok: false, message: "activities requires `threadId`." };
                        const thread = getAccessibleThread(input.threadId, userId);
                        if (!thread)
                            return { ok: false, message: "Thread not found." };
                        const limit = Math.min(Math.max(input.limit ?? 100, 1), 500);
                        const activities = deps.threadService.getActivities(thread.id, limit);
                        return {
                            ok: true,
                            message: `${activities.length} activit${activities.length === 1 ? "y" : "ies"}`,
                            data: { activities, threadId: thread.id },
                        };
                    }
                    case "create_pr": {
                        const thread = input.threadId ? getAccessibleThread(input.threadId, userId) : undefined;
                        if (input.threadId && !thread) {
                            return { ok: false, message: "Thread not found." };
                        }
                        if (thread && !thread.completedAt) {
                            return {
                                ok: false,
                                message: "Thread must be completed before creating a pull request.",
                            };
                        }
                        if (thread?.kind === "delegation") {
                            return {
                                ok: false,
                                message: "Delegation threads do not support PR creation.",
                            };
                        }
                        const cwd = input.cwd || thread?.workingDirectory || context.projectRoot;
                        if (!cwd)
                            return { ok: false, message: "create_pr requires `cwd` or a thread with `workingDirectory`." };
                        const action = input.gitAction ?? "commit_push_pr";
                        const result = await gitService.runStackedAction(cwd, action, input.commitMessage?.trim() || undefined, input.featureBranch === true, input.baseBranch);
                        const prUrl = result.pr.url ?? result.push.createPrUrl;
                        let updatedThread;
                        if (thread) {
                            updatedThread = deps.threadService.update(thread.id, {
                                branch: result.push.branch ?? undefined,
                                prUrl: result.pr.url ?? undefined,
                                prNumber: result.pr.number ?? undefined,
                                prTitle: result.pr.title ?? undefined,
                                prBaseBranch: result.pr.baseBranch ?? input.baseBranch ?? undefined,
                                prState: result.pr.status === "created" || result.pr.status === "opened_existing" ? "open" : undefined,
                            });
                            if (updatedThread) {
                                broadcastThreadEvent(thread.id, "updated", { thread: updatedThread });
                            }
                            if (prUrl) {
                                const activity = deps.threadService.addActivity(thread.id, "activity", result.pr.url ? `Pull request created: ${prUrl}` : `Open pull request: ${prUrl}`, { action, prUrl, result });
                                broadcastThreadEvent(thread.id, "activity", { activity });
                            }
                        }
                        const message = result.pr.url
                            ? `Pull request ready: ${result.pr.url}`
                            : prUrl
                                ? `Create pull request here: ${prUrl}`
                                : "Git action completed, but no pull request link is available.";
                        return {
                            ok: true,
                            message,
                            data: {
                                action,
                                prUrl: prUrl ?? null,
                                result,
                                thread: updatedThread,
                            },
                        };
                    }
                    default:
                        return { ok: false, message: `Unsupported action: ${input.action ?? "(unknown)"}` };
                }
            }
            catch (err) {
                return {
                    ok: false,
                    message: err instanceof Error ? err.message : "thread.control failed",
                };
            }
        },
    };
}
//# sourceMappingURL=thread-tools.js.map