"use strict";
/**
 * TrackRunner — thin wrapper around train-lang's runFile that wires
 * the CcwebTrainAdapter + WorkflowDataWatcher + writeProtocolHint into
 * a single ergonomic API for the rest of the ccweb backend.
 *
 * Lifecycle:
 *   const runner = createTrackRunner({ ... deps ... })
 *   const result = await runner.run(absTrackPath)
 *   // result.ok / result.value / result.error
 *
 * Cancel mid-run: runner.cancel() → AbortController fires → train
 * short-circuits next fai dispatch + adapter reports FaiCancelled.
 *
 * train-lang is ESM and ccweb backend is CommonJS, so we use a
 * dynamic import (cached at first call). This is the standard
 * CJS→ESM interop pattern.
 *
 * 详见 ~/Obsidian/Base/cc-web/工作轨重构规划.md §9 T0。
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTrackRunner = createTrackRunner;
const ccweb_train_adapter_1 = require("./ccweb-train-adapter");
const ask_user_bridge_1 = require("./ask-user-bridge");
const train_loader_1 = require("./train-loader");
async function loadTrain() {
    // Reuse the shared cache in train-loader.ts so multiple consumers
    // (TrackRunner, AskUserBuiltin factory, any future host extension)
    // get the same module instance — avoids ESM module-graph duplication
    // and removes the double-cache foot-gun.
    return (0, train_loader_1.loadTrainCore)();
}
function createTrackRunner(deps) {
    let state = null;
    let abortController = null;
    let taskIndexCounter = 0;
    function updateState(patch) {
        if (!state)
            return;
        state = { ...state, ...patch };
        deps.onState?.(state);
    }
    return {
        async run(absTrackPath, args = []) {
            const train = await loadTrain();
            const runId = deps.runId ?? `track-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
            state = {
                runId,
                trackFilename: absTrackPath.split('/').pop() ?? absTrackPath,
                startedAt: Date.now(),
                status: 'running',
            };
            deps.onState?.(state);
            abortController = new AbortController();
            // Start watcher just before run begins
            deps.watcher.start();
            const adapter = (0, ccweb_train_adapter_1.createCcwebTrainAdapter)({
                projectId: deps.projectId,
                runId,
                injector: deps.injector,
                watcher: deps.watcher,
                nextTaskIndex: () => {
                    const i = taskIndexCounter++;
                    updateState({ currentTaskIndex: i });
                    return i;
                },
                logger: deps.logger,
            });
            // Build extraBuiltins map (currently: __ccweb_ask_user if bridge provided)
            const extraBuiltins = new Map();
            if (deps.askUserBridge) {
                const askUserFn = await (0, ask_user_bridge_1.createAskUserBuiltin)(deps.askUserBridge, runId, abortController.signal);
                extraBuiltins.set('__ccweb_ask_user', askUserFn);
            }
            try {
                const result = await train.runFile(absTrackPath, {
                    args,
                    adapter,
                    maxFaiAttempts: deps.maxFaiAttempts ?? 3,
                    defaultFaiTimeoutMs: deps.defaultFaiTimeoutMs ?? 600000,
                    writeProtocolHint: (0, ccweb_train_adapter_1.buildCcwebWriteProtocolHint)(),
                    signal: abortController.signal,
                    extraBuiltins,
                });
                if (result.lexErrors.length > 0 || result.parseErrors.length > 0) {
                    const message = `parse errors in ${absTrackPath} (${result.lexErrors.length + result.parseErrors.length})`;
                    updateState({
                        status: 'failed',
                        endedAt: Date.now(),
                        error: { errorType: 'ParseError', message },
                    });
                    return { ok: false, value: null, error: { errorType: 'ParseError', message } };
                }
                if (!result.ok) {
                    const exc = result.error;
                    const errObj = exc
                        ? {
                            errorType: exc.errorType ?? 'RuntimeError',
                            message: exc.message,
                            code: exc.code,
                        }
                        : { errorType: 'RuntimeError', message: 'unknown failure' };
                    const finalStatus = errObj.errorType === 'UserCancelError' ? 'cancelled' : 'failed';
                    updateState({ status: finalStatus, endedAt: Date.now(), error: errObj });
                    return { ok: false, value: null, error: errObj };
                }
                updateState({
                    status: 'completed',
                    endedAt: Date.now(),
                    result: result.value,
                });
                return { ok: true, value: result.value };
            }
            catch (err) {
                // NEVER-THROW contract: runFile / builtin / adapter throws (incl.
                // ask_user reject during abort) MUST be converted into TrackRunResult.
                // Without this catch the throw escapes runner.run, registry.ts's
                // `void runner.run(...).then(...)` has no .catch, and the global
                // unhandledRejection handler (logger.ts) calls process.exit(1).
                // → entire ccweb daemon dies whenever a track with pending ask_user
                // is aborted. (Repro: v-15-f / v-15-g logs, 2026-05-16 07:35 / 07:36.)
                const aborted = abortController?.signal.aborted ?? false;
                const e = err;
                const errObj = aborted
                    ? {
                        errorType: 'UserCancelError',
                        message: e?.message ?? 'cancelled',
                    }
                    : {
                        errorType: e?.errorType ?? 'RuntimeError',
                        message: e?.message ?? String(err),
                        code: e?.code,
                    };
                const finalStatus = aborted ? 'cancelled' : 'failed';
                updateState({ status: finalStatus, endedAt: Date.now(), error: errObj });
                return { ok: false, value: null, error: errObj };
            }
            finally {
                deps.watcher.stop();
                if (deps.askUserBridge && state) {
                    // Drain any pending ask_user that the bridge still holds.
                    deps.askUserBridge.cancelAllForRun(state.runId, 'run ended');
                }
                abortController = null;
            }
        },
        cancel() {
            if (abortController) {
                abortController.abort();
                deps.logger?.info?.('[TrackRunner] cancel requested');
            }
        },
        getState() {
            return state;
        },
    };
}
//# sourceMappingURL=track-runner.js.map