"use strict";
/**
 * TrackRunnerRegistry — per-project TrackRunner accounting.
 *
 * Singleton owned by backend/src/index.ts. Wraps creation of a runner
 * (with the project's PTY injector + workflow_data path) and exposes
 * lifecycle calls used by the HTTP route handlers.
 *
 * One project can have at most one in-flight track run; starting a
 * new one while another is running rejects with 'busy'.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTrackRegistry = createTrackRegistry;
const path = __importStar(require("path"));
const track_runner_1 = require("./track-runner");
const workflow_data_watcher_1 = require("./workflow-data-watcher");
const ask_user_bridge_1 = require("./ask-user-bridge");
function createTrackRegistry(deps) {
    const projects = new Map();
    function emit(projectId, kind, payload) {
        deps.broadcast(projectId, { type: kind, ...payload });
    }
    return {
        async start(projectId, absTrackPath, trackFilename, args = []) {
            // Synchronous in-flight gate — set BEFORE any await to close the
            // race window between two concurrent start() calls.
            const existing = projects.get(projectId);
            if (existing && (existing.inFlight || existing.lastState?.status === 'running')) {
                return { ok: false, reason: 'a track is already running for this project' };
            }
            const projectFolder = deps.getProjectFolder(projectId);
            if (!projectFolder) {
                return { ok: false, reason: 'project not found' };
            }
            const runId = `track-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
            const workflowDataPath = path.join(projectFolder, '.ccweb', 'workflow_data.json');
            const watcher = (0, workflow_data_watcher_1.createWorkflowDataWatcher)(workflowDataPath);
            const bridge = (0, ask_user_bridge_1.createAskUserBridge)((event) => {
                emit(projectId, 'track_ask_user', event);
            });
            const runner = (0, track_runner_1.createTrackRunner)({
                projectId,
                injector: (text) => deps.injectIntoPty(projectId, text),
                watcher,
                logger: deps.logger,
                askUserBridge: bridge,
                // Pin the runner's runId to the one registry hands out, so
                // bridge.pending keys, registry.entry.runId, the runId returned
                // to the route, and state.runId all match. Pre-fix mismatch
                // made getPendingAskUser/submitInput/cancelAllForRun no-ops
                // since they looked up the wrong key.
                runId,
                onState: (state) => {
                    // Ignore late callbacks from a previous run — only the run
                    // whose runId matches the entry's current runId is allowed
                    // to overwrite lastState. (Defense against runner finishing
                    // after the entry has been replaced.)
                    const e = projects.get(projectId);
                    if (!e || e.runId !== runId)
                        return;
                    e.lastState = state;
                    emit(projectId, 'track_status_change', {
                        state: state,
                    });
                },
            });
            const entry = {
                runner,
                bridge,
                lastState: null,
                trackFilename,
                inFlight: true,
                runId,
            };
            projects.set(projectId, entry);
            // Fire and forget — the run resolves later; route returns immediately.
            //
            // The .catch is defense-in-depth: track-runner converts its own
            // exceptions to TrackRunResult, so .then's callback handles every
            // expected outcome. But if track-runner ever throws (host bug, OOM,
            // future refactor), there's no .catch to break a rejected promise
            // chain — it escapes to logger.ts's unhandledRejection handler
            // which process.exit(1)s the entire daemon. The .catch keeps that
            // class of bug from killing ccweb.
            void runner
                .run(absTrackPath, args)
                .then((result) => {
                const e = projects.get(projectId);
                if (e && e.runId === runId) {
                    e.inFlight = false;
                }
                deps.logger?.info?.({ projectId, runId, ok: result.ok, error: result.error }, '[TrackRegistry] run finished');
                emit(projectId, 'track_run_complete', {
                    ok: result.ok,
                    value: result.value,
                    error: result.error,
                });
            })
                .catch((err) => {
                const message = err instanceof Error ? err.message : String(err);
                deps.logger?.error?.({ projectId, runId, err: message }, '[TrackRegistry] run threw — converted to failed event');
                // Synthesize a terminal state so isRunning() returns false and
                // the frontend's track_status_change consumers see a definite
                // end-of-run, not a stuck 'running'. Without this, runner-level
                // bugs that future-broke runner's NEVER-THROW contract would
                // leave the project stuck (entry.lastState would stay null/running).
                const e = projects.get(projectId);
                const failedState = {
                    runId,
                    trackFilename: e?.trackFilename ?? '',
                    startedAt: e?.lastState?.startedAt ?? Date.now(),
                    status: 'failed',
                    endedAt: Date.now(),
                    error: { errorType: 'RunnerThrew', message },
                };
                if (e && e.runId === runId) {
                    e.inFlight = false;
                    e.lastState = failedState;
                }
                emit(projectId, 'track_status_change', {
                    state: failedState,
                });
                emit(projectId, 'track_run_complete', {
                    ok: false,
                    value: null,
                    error: { errorType: 'RunnerThrew', message },
                });
            });
            return { ok: true, runId };
        },
        abort(projectId) {
            const entry = projects.get(projectId);
            if (!entry)
                return false;
            entry.runner.cancel();
            entry.bridge.cancelAllForRun(entry.runId, 'aborted');
            return true;
        },
        submitInput(projectId, requestId, data) {
            const entry = projects.get(projectId);
            if (!entry)
                return { ok: false, message: 'no track running for project' };
            // Pass values straight through — bridge.submitInput does per-field
            // type validation (text/number/bool/enum).
            return entry.bridge.submitInput(entry.runId, requestId, data);
        },
        getState(projectId) {
            return projects.get(projectId)?.lastState ?? null;
        },
        isRunning(projectId) {
            const entry = projects.get(projectId);
            if (!entry)
                return false;
            // Window 1: inFlight true during start() pre-state phase.
            // Window 2: lastState.status running/paused after first onState fires.
            if (entry.inFlight)
                return true;
            const running = ['running', 'paused'];
            return entry.lastState ? running.includes(entry.lastState.status) : false;
        },
        getPendingAskUser(projectId) {
            const entry = projects.get(projectId);
            if (!entry)
                return null;
            return entry.bridge.getPending(entry.runId);
        },
    };
}
//# sourceMappingURL=registry.js.map