import path from 'node:path';
import { appendJsonlBounded, ensureDir, nowIso, readText, writeJsonAtomic } from '../fsx.js';
export const PARALLEL_RUNTIME_EVENT_SCHEMA = 'sks.parallel-runtime-event.v1';
export const PARALLEL_RUNTIME_PROOF_SCHEMA = 'sks.parallel-runtime-proof.v1';
export function parallelRuntimeEventPath(root, missionId) {
    return path.join(inferAgentsDir(root, missionId), 'parallel-runtime.events.jsonl');
}
export function parallelRuntimeProofPath(root, missionId) {
    return path.join(inferAgentsDir(root, missionId), 'parallel-runtime-proof.json');
}
export async function appendParallelRuntimeEvent(root, missionId, event) {
    const row = normalizeParallelRuntimeEvent(missionId, event);
    const file = parallelRuntimeEventPath(root, missionId);
    await ensureDir(path.dirname(file));
    await appendJsonlBounded(file, row);
    return row;
}
export async function buildParallelRuntimeProof(root, missionId, opts = {}) {
    const events = await readParallelRuntimeEvents(root, missionId);
    const sorted = events.sort((a, b) => a.ms - b.ms);
    const firstMs = sorted[0]?.ms || Date.now();
    const lastMs = sorted[sorted.length - 1]?.ms || firstMs;
    const workerActive = new Set();
    const processActive = new Set();
    const modelActive = new Set();
    const workerStarts = new Map();
    const workerDurations = [];
    const workerPids = new Set();
    const modelIds = new Set();
    const overlapWindows = [];
    let maxWorkers = 0;
    let maxProcesses = 0;
    let maxModels = 0;
    let previousMs = firstMs;
    let firstBatchLaunchSpanMs = 0;
    const batchStart = new Map();
    const batchCompleted = new Map();
    for (const event of sorted) {
        if (event.ms > previousMs) {
            overlapWindows.push({
                start_ms: previousMs - firstMs,
                end_ms: event.ms - firstMs,
                active_workers: workerActive.size,
                active_model_calls: modelActive.size
            });
            previousMs = event.ms;
        }
        const workerKey = event.session_id || event.slot_id || (event.pid == null ? '' : `pid:${event.pid}`);
        const processKey = event.pid == null ? workerKey : `pid:${event.pid}`;
        if (event.event_type === 'batch_dispatch_started' && event.batch_id)
            batchStart.set(event.batch_id, event.ms);
        if (event.event_type === 'batch_dispatch_completed' && event.batch_id) {
            batchCompleted.set(event.batch_id, event.ms);
            const started = batchStart.get(event.batch_id);
            if (started != null && firstBatchLaunchSpanMs === 0)
                firstBatchLaunchSpanMs = Math.max(0, event.ms - started);
        }
        if (event.event_type === 'worker_launch_invoked' || event.event_type === 'worker_process_spawned') {
            if (workerKey) {
                workerActive.add(workerKey);
                if (!workerStarts.has(workerKey))
                    workerStarts.set(workerKey, event.ms);
            }
            if (event.event_type === 'worker_process_spawned' && processKey)
                processActive.add(processKey);
            if (event.pid != null)
                workerPids.add(event.pid);
        }
        if (event.event_type === 'worker_completed' || event.event_type === 'worker_failed') {
            if (workerKey) {
                workerActive.delete(workerKey);
                const started = workerStarts.get(workerKey);
                if (started != null)
                    workerDurations.push(Math.max(0, event.ms - started));
            }
            if (processKey)
                processActive.delete(processKey);
        }
        if (event.event_type === 'model_call_started') {
            const id = event.model_call_id || event.session_id || `model:${event.ms}:${modelActive.size}`;
            modelActive.add(id);
            modelIds.add(id);
        }
        if (event.event_type === 'model_call_completed') {
            const id = event.model_call_id || event.session_id || '';
            if (id)
                modelActive.delete(id);
        }
        maxWorkers = Math.max(maxWorkers, workerActive.size);
        maxProcesses = Math.max(maxProcesses, processActive.size);
        maxModels = Math.max(maxModels, modelActive.size);
    }
    const requestedWorkers = positiveInt(opts.requestedWorkers, workerStarts.size || workerPids.size || maxWorkers);
    const targetActiveSlots = positiveInt(opts.targetActiveSlots, requestedWorkers);
    const wallMs = Math.max(0, lastMs - firstMs);
    const sequentialEstimateMs = workerDurations.length
        ? workerDurations.reduce((sum, value) => sum + value, 0)
        : requestedWorkers * positiveInt(opts.expectedWorkerRuntimeMs, 4000);
    const visiblePanes = nonNegativeInt(opts.visiblePanes, sorted.filter((event) => event.placement === 'zellij-pane').length ? new Set(sorted.filter((event) => event.placement === 'zellij-pane').map((event) => event.slot_id || event.session_id || '')).size : 0);
    const observedHeadlessWorkers = sorted.filter((event) => event.placement === 'headless' && (event.event_type === 'worker_launch_invoked' || event.event_type === 'worker_process_spawned')).length;
    const headlessWorkers = Math.max(observedHeadlessWorkers, Math.max(0, targetActiveSlots - visiblePanes));
    const minActiveWorkers = opts.minActiveWorkers === undefined
        ? Math.min(targetActiveSlots, requestedWorkers)
        : nonNegativeInt(opts.minActiveWorkers, Math.min(targetActiveSlots, requestedWorkers));
    const minSpeedup = Number.isFinite(Number(opts.minSpeedupRatio)) ? Number(opts.minSpeedupRatio) : requestedWorkers >= 16 ? 5 : 1;
    const speedupRatio = wallMs > 0 ? Number((sequentialEstimateMs / wallMs).toFixed(3)) : 0;
    const launchEvents = sorted.filter((event) => event.event_type === 'worker_launch_invoked' || event.event_type === 'worker_process_spawned');
    const launchSpanMs = launchEvents.length ? Math.max(...launchEvents.map((event) => event.ms)) - Math.min(...launchEvents.map((event) => event.ms)) : 0;
    const firstBatchLimit = positiveInt(opts.firstBatchLaunchSpanLimitMs, requestedWorkers >= 16 ? 2500 : 30000);
    const blockers = [
        ...(sorted.length ? [] : ['parallel_runtime_events_missing']),
        ...(minActiveWorkers <= 0 || maxWorkers >= minActiveWorkers ? [] : ['max_observed_active_workers_below_target']),
        ...(requestedWorkers >= 16 && workerPids.size && workerPids.size < minActiveWorkers ? ['unique_worker_pids_below_target'] : []),
        ...(speedupRatio >= minSpeedup ? [] : ['speedup_ratio_below_target']),
        ...(firstBatchLaunchSpanMs <= firstBatchLimit ? [] : ['first_batch_launch_span_above_limit'])
    ];
    return {
        schema: PARALLEL_RUNTIME_PROOF_SCHEMA,
        mission_id: missionId,
        generated_at: nowIso(),
        requested_workers: requestedWorkers,
        target_active_slots: targetActiveSlots,
        max_observed_active_workers: maxWorkers,
        max_observed_worker_processes: Math.max(maxProcesses, workerPids.size ? maxProcesses : maxWorkers),
        unique_worker_pids: workerPids.size,
        unique_model_call_ids: modelIds.size,
        max_observed_model_calls: maxModels,
        launch_span_ms: launchSpanMs,
        first_batch_launch_span_ms: firstBatchLaunchSpanMs,
        wall_ms: wallMs,
        sequential_estimate_ms: sequentialEstimateMs,
        speedup_ratio: speedupRatio,
        overlap_windows: coalesceOverlapWindows(overlapWindows),
        visible_panes: visiblePanes,
        headless_workers: headlessWorkers,
        passed: blockers.length === 0,
        blockers
    };
}
export async function writeParallelRuntimeProof(root, missionId, opts = {}) {
    const proof = await buildParallelRuntimeProof(root, missionId, opts);
    await writeJsonAtomic(parallelRuntimeProofPath(root, missionId), proof);
    return proof;
}
async function readParallelRuntimeEvents(root, missionId) {
    const text = await readText(parallelRuntimeEventPath(root, missionId), '');
    return String(text)
        .split(/\r?\n/)
        .map((line) => line.trim())
        .filter(Boolean)
        .map((line) => {
        try {
            const parsed = JSON.parse(line);
            return parsed?.schema === PARALLEL_RUNTIME_EVENT_SCHEMA ? parsed : null;
        }
        catch {
            return null;
        }
    })
        .filter((row) => Boolean(row));
}
function normalizeParallelRuntimeEvent(missionId, event) {
    return {
        schema: PARALLEL_RUNTIME_EVENT_SCHEMA,
        ts: nowIso(),
        ms: Date.now(),
        mission_id: missionId,
        event_type: event.event_type,
        slot_id: event.slot_id == null ? null : String(event.slot_id),
        generation_index: event.generation_index == null ? null : Math.max(1, Math.floor(Number(event.generation_index) || 1)),
        session_id: event.session_id == null ? null : String(event.session_id),
        pid: event.pid == null || !Number.isFinite(Number(event.pid)) ? null : Math.floor(Number(event.pid)),
        backend: String(event.backend || 'unknown'),
        placement: normalizePlacement(event.placement),
        ...(event.worktree_id === undefined ? {} : { worktree_id: event.worktree_id == null ? null : String(event.worktree_id) }),
        ...(event.model_call_id === undefined ? {} : { model_call_id: event.model_call_id == null ? null : String(event.model_call_id) }),
        ...(event.batch_id === undefined ? {} : { batch_id: event.batch_id == null ? null : String(event.batch_id) }),
        ...(event.meta && typeof event.meta === 'object' ? { meta: event.meta } : {})
    };
}
function normalizePlacement(value) {
    const text = String(value || 'unknown');
    if (text === 'zellij-pane' || text === 'process' || text === 'headless')
        return text;
    return 'unknown';
}
function positiveInt(value, fallback) {
    const parsed = Number(value);
    if (!Number.isFinite(parsed) || parsed < 1)
        return Math.max(1, Math.floor(fallback || 1));
    return Math.floor(parsed);
}
function nonNegativeInt(value, fallback) {
    const parsed = Number(value);
    if (!Number.isFinite(parsed) || parsed < 0)
        return Math.max(0, Math.floor(fallback || 0));
    return Math.floor(parsed);
}
function coalesceOverlapWindows(windows) {
    return windows
        .filter((window) => window.end_ms > window.start_ms)
        .filter((window) => window.active_workers > 0 || window.active_model_calls > 0)
        .slice(0, 2000);
}
function inferAgentsDir(root, missionId) {
    const resolved = path.resolve(root);
    if (path.basename(resolved) === 'agents' && path.basename(path.dirname(resolved)) === missionId)
        return resolved;
    if (path.basename(resolved) === missionId && path.basename(path.dirname(resolved)) === 'missions')
        return path.join(resolved, 'agents');
    const marker = `${path.sep}.sneakoscope${path.sep}missions${path.sep}${missionId}${path.sep}`;
    const index = resolved.indexOf(marker);
    if (index >= 0)
        return path.join(resolved.slice(0, index + marker.length - 1), 'agents');
    return path.join(resolved, '.sneakoscope', 'missions', missionId, 'agents');
}
//# sourceMappingURL=parallel-runtime-proof.js.map