/**
 * Autoresearch task executor — bridges TaskManager with the
 * autoresearch experiment loop.
 *
 * Each tick:
 * 1. Loads/creates a ResearchWorker for the task's tag
 * 2. Gets the phase-appropriate tool filter
 * 3. Calls NeuroLink.generate() with research tools + prompt
 * 4. Advances phase based on which tools the AI called
 * 5. Returns a TaskRunResult
 *
 * Workers are cached by tag to avoid re-initialization on each tick.
 * Forwards the NeuroLink emitter to each worker for lifecycle events.
 */
import { ResearchWorker } from "../autoresearch/worker.js";
import { withTimeout } from "../utils/errorHandling.js";
import { logger } from "../utils/logger.js";
// ── Worker cache ────────────────────────────────────────
const workerCache = new Map();
/**
 * Clear all cached workers. Called by TaskManager.shutdown().
 */
export function clearWorkerCache() {
    workerCache.clear();
    logger.debug("[AutoresearchExecutor] Worker cache cleared");
}
// ── Phase advancement ──────────────────────────────────
/**
 * Determine the next phase based on which tools the AI called.
 *
 * The worker path (`runExperimentCycle`) drives phases internally.
 * The task path must replicate that advancement here. The mapping:
 *
 *   bootstrap → (get_context called) → propose
 *   propose   → (read_file called)   → edit
 *   edit      → (write_candidate)    → commit
 *   commit    → (commit_candidate)   → run
 *   run       → (run_experiment)     → evaluate
 *   evaluate  → (parse_log)          → record
 *   record    → (record called)      → accept_or_revert
 *   accept_or_revert → (accept/revert) → propose
 *
 * Returns null if no advancement is indicated (e.g. no recognised tools called).
 */
function inferNextPhase(currentPhase, calledTools) {
    if (calledTools.length === 0) {
        return null;
    }
    // Compute phase advancement per-tool against the running accumulated phase
    function phaseAfterTool(toolName, phase) {
        switch (toolName) {
            case "research_get_context":
                return phase === "bootstrap" || phase === "propose" ? "propose" : phase;
            case "research_read_file":
                return phase === "propose" ? "edit" : phase;
            case "research_write_candidate":
                return "commit";
            case "research_commit_candidate":
                return "run";
            case "research_run_experiment":
                return "evaluate";
            case "research_parse_log":
                return "record";
            case "research_record":
                return "accept_or_revert";
            case "research_accept":
                return "propose";
            case "research_revert":
                return "propose";
            default:
                return phase; // informational tools don't advance
        }
    }
    // Walk the called tools in order, accumulating phase progression
    let nextPhase = currentPhase;
    for (const toolName of calledTools) {
        nextPhase = phaseAfterTool(toolName, nextPhase);
    }
    return nextPhase !== currentPhase ? nextPhase : null;
}
/**
 * Get or create a ResearchWorker for the given task.
 * When an emitter is provided, it is injected into the worker
 * so that autoresearch:* events flow through the central event bus.
 */
async function getOrCreateWorker(task, emitter) {
    const tag = `task-${task.id}`;
    const cached = workerCache.get(tag);
    if (cached) {
        // Re-inject emitter in case it changed (e.g. after re-initialization)
        if (emitter) {
            cached.setEmitter(emitter);
        }
        // Attempt to resume existing state
        try {
            await cached.resume();
            return cached;
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            // Only re-initialize if state is genuinely missing; surface other errors
            if (msg.includes("STATE_NOT_FOUND") || msg.includes("No state file")) {
                workerCache.delete(tag);
            }
            else {
                throw err;
            }
        }
    }
    // Validated at caller (line 184 returns early if missing)
    const config = task.autoresearch;
    const worker = new ResearchWorker({
        repoPath: config.repoPath,
        mutablePaths: config.mutablePaths,
        runCommand: config.runCommand,
        metric: config.metric,
        ...(config.immutablePaths ? { immutablePaths: config.immutablePaths } : {}),
        ...(config.timeoutMs ? { timeoutMs: config.timeoutMs } : {}),
        ...(config.provider ? { provider: config.provider } : {}),
        ...(config.model ? { model: config.model } : {}),
        ...(config.thinkingLevel ? { thinkingLevel: config.thinkingLevel } : {}),
        ...(config.maxExperiments ? { maxExperiments: config.maxExperiments } : {}),
        // Forward artifact layout fields so scheduled runs match standalone config
        ...(config.programPath ? { programPath: config.programPath } : {}),
        ...(config.resultsPath ? { resultsPath: config.resultsPath } : {}),
        ...(config.statePath ? { statePath: config.statePath } : {}),
        ...(config.logPath ? { logPath: config.logPath } : {}),
        ...(config.branchPrefix ? { branchPrefix: config.branchPrefix } : {}),
        ...(config.memoryMetric ? { memoryMetric: config.memoryMetric } : {}),
    });
    // Inject emitter before initialization so init events are captured
    if (emitter) {
        worker.setEmitter(emitter);
    }
    // Try resuming existing state first; only initialize if state is genuinely missing
    try {
        await worker.resume();
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        if (msg.includes("STATE_NOT_FOUND") || msg.includes("No state file")) {
            await worker.initialize(tag);
        }
        else {
            throw err;
        }
    }
    workerCache.set(tag, worker);
    return worker;
}
/**
 * Execute one autoresearch tick for a task.
 *
 * Returns a TaskRunResult-shaped object.
 * If the task is missing autoresearch config, returns an error result
 * instead of throwing (so the scheduler can record the failure).
 *
 * @param emitter - Optional emitter to forward autoresearch lifecycle events
 */
export async function executeAutoresearchTick(task, neurolink, emitter) {
    const startTime = Date.now();
    const runId = `ar_${Date.now()}`;
    // Validate config presence
    if (!task.autoresearch) {
        return {
            taskId: task.id,
            runId,
            status: "error",
            error: `Task ${task.id} has type=autoresearch but no autoresearch config`,
            durationMs: Date.now() - startTime,
            timestamp: new Date().toISOString(),
        };
    }
    try {
        const worker = await getOrCreateWorker(task, emitter);
        // Get phase-appropriate tools and filter
        const tools = worker.getTools();
        const toolFilter = await worker.getToolFilterForCurrentPhase();
        const systemPrompt = await worker.getSystemPrompt();
        const cyclePrompt = await worker.getCyclePrompt();
        // Register research tools on NeuroLink if it supports registerTool
        const sdk = neurolink;
        if (typeof sdk.registerTool === "function") {
            for (const [name, tool] of Object.entries(tools)) {
                sdk.registerTool(name, tool);
            }
        }
        // Get the current phase's forced tool (if any)
        const phasePolicy = await worker.getPhaseToolPolicy();
        // Extract provider/model from autoresearch config
        const arConfig = task.autoresearch;
        // Create an AbortController so the tick respects the task timeout.
        // Each tick should complete within a bounded time; 20 tool-call steps
        // is enough for one explore→write→experiment→record cycle.
        const tickTimeoutMs = (task.timeout ?? 120_000);
        const abortController = new AbortController();
        const abortTimer = setTimeout(() => abortController.abort(), tickTimeoutMs);
        // Call generate
        const generateOptions = {
            input: { text: cyclePrompt },
            systemPrompt,
            tools: true,
            skipToolPromptInjection: true,
            maxSteps: 20, // Bound tool-call steps per tick to avoid runaway loops
            abortSignal: abortController.signal,
            // Forward provider/model from autoresearch config
            ...(arConfig?.provider ? { provider: arConfig.provider } : {}),
            ...(arConfig?.model ? { model: arConfig.model } : {}),
            ...(arConfig?.thinkingLevel || task.thinkingLevel
                ? {
                    thinkingConfig: {
                        thinkingLevel: arConfig?.thinkingLevel ?? task.thinkingLevel,
                    },
                }
                : {}),
            ...(task.timeout ? { timeout: task.timeout } : {}),
            // Phase tool restrictions
            toolFilter,
            // Use prepareStep to force tool only on first step, not all steps
            ...(phasePolicy.forcedTool
                ? {
                    prepareStep: ({ stepNumber }) => {
                        if (stepNumber === 0 && phasePolicy.forcedTool) {
                            return {
                                toolChoice: {
                                    type: "tool",
                                    toolName: phasePolicy.forcedTool,
                                },
                            };
                        }
                        return {};
                    },
                }
                : {}),
            // Observability
            requestId: runId,
            ...(task.metadata?.maxBudgetUsd
                ? { maxBudgetUsd: task.metadata.maxBudgetUsd }
                : {}),
        };
        let result;
        try {
            result = await withTimeout(neurolink.generate(generateOptions), tickTimeoutMs, new Error(`Autoresearch tick exceeded ${tickTimeoutMs}ms timeout`));
        }
        finally {
            clearTimeout(abortTimer);
        }
        // ── Phase advancement ──────────────────────────────
        // The deterministic worker path (runExperimentCycle) advances
        // phases internally.  The task path must do it here, based on
        // which tools the AI actually called this tick.
        const calledTools = (result.toolExecutions ?? [])
            .map((te) => {
            const t = te;
            return typeof t.name === "string" ? t.name : "";
        })
            .filter(Boolean);
        const currentState = await worker.getState();
        const currentPhase = currentState?.currentPhase ?? "bootstrap";
        const nextPhase = inferNextPhase(currentPhase, calledTools);
        if (nextPhase) {
            await worker.advancePhase(nextPhase);
            logger.debug("[AutoresearchExecutor] Phase advanced", {
                from: currentPhase,
                to: nextPhase,
                calledTools,
            });
        }
        return {
            taskId: task.id,
            runId,
            status: "success",
            output: result.content,
            toolCalls: result.toolExecutions?.map((te) => ({
                name: te.name,
                input: te.input,
                output: te.output,
            })),
            tokensUsed: result.usage
                ? { input: result.usage.input ?? 0, output: result.usage.output ?? 0 }
                : undefined,
            durationMs: Date.now() - startTime,
            timestamp: new Date().toISOString(),
        };
    }
    catch (error) {
        const msg = error instanceof Error ? error.message : String(error);
        logger.error("[AutoresearchExecutor] Tick failed", {
            taskId: task.id,
            runId,
            error: msg,
        });
        return {
            taskId: task.id,
            runId,
            status: "error",
            error: msg,
            durationMs: Date.now() - startTime,
            timestamp: new Date().toISOString(),
        };
    }
}
