/**
 * OwlCoda Native Task Store
 *
 * In-memory task management backing the Task* tools.
 * Tasks persist for the session lifetime.
 *
 * Upstream parity notes:
 * - Upstream uses utils/tasks.ts with file-backed JSON storage
 * - Supports task lists, hooks, teammate ownership
 * - Our version: in-memory Map with same field structure
 *
 * Execution layer (re-introduced 0.13.31):
 *   When `TaskCreate` is given a `command` and it has cleared the bash
 *   risk classifier (see tools/task-create.ts), we spawn a bash child
 *   here and store its handle in `runningProcesses`. Stdout / stderr are
 *   buffered on the Task itself so `TaskOutput` can surface them when
 *   the task hits a terminal state. Exit is event-driven (`child.on('exit')`),
 *   never polled. `TaskStop` writes status='cancelled' BEFORE sending
 *   SIGTERM so the exit handler doesn't race the stop and overwrite
 *   `cancelled` with `completed`. `resetTaskStore` SIGKILLs any survivors
 *   so test runs don't leak processes.
 *
 * Task Execution Mode v1 (Slice 1, 0.14.19+):
 *   Tasks can now carry a structured execution plan: `deliverables` and
 *   `steps`. Steps track state, expected artifacts, and verification.
 *   Use `updateTaskStep` / `getCurrentOrNextStep` / `taskHasOpenRequiredSteps`
 *   to drive the task execution loop.
 */
import { spawn } from 'node:child_process';
let nextId = 1;
const tasks = new Map();
const runningProcesses = new Map();
/** Generate a sequential task ID. */
function genId() {
    return `task-${nextId++}`;
}
/** Create a new task. */
export function createTask(opts) {
    const now = new Date().toISOString();
    // Normalize steps: assign IDs, fill defaults
    let normalizedSteps;
    if (opts.steps && opts.steps.length > 0) {
        normalizedSteps = opts.steps.map((s, i) => {
            const id = s.id ?? `step-${i + 1}`;
            return {
                id,
                title: s.title,
                description: s.description,
                status: 'pending',
                action: s.action ?? { kind: 'local_tool' },
                expectedArtifacts: s.expectedArtifacts ?? [],
                verification: s.verification ?? [],
                verificationResults: [],
                touchedPaths: [],
                startedAt: undefined,
                completedAt: undefined,
                failureReason: undefined,
            };
        });
    }
    const task = {
        id: genId(),
        subject: opts.subject,
        description: opts.description,
        status: 'pending',
        activeForm: opts.activeForm,
        metadata: opts.metadata,
        blocks: [],
        blockedBy: [],
        createdAt: now,
        updatedAt: now,
        command: opts.command,
        cwd: opts.cwd,
        bashRisk: opts.bashRisk,
        deliverables: opts.deliverables ?? [],
        steps: normalizedSteps,
    };
    tasks.set(task.id, task);
    return task;
}
// ---------------------------------------------------------------------------
// Step helpers (Slice 1)
// ---------------------------------------------------------------------------
/** Get a step from a task by step ID. */
export function getTaskStep(taskId, stepId) {
    const task = tasks.get(taskId);
    return task?.steps?.find(s => s.id === stepId);
}
/**
 * Update a task step's mutable fields.
 *
 * Enforces transition rules:
 *   - Only one step may be `in_progress` per task (unless another is already in_progress)
 *   - `completed` requires all verification results to be passed (or none exist)
 *   - `failed` and `blocked` require `failureReason`
 *   - `completed` cannot be reopened (returns error)
 */
export function updateTaskStep(taskId, stepId, updates) {
    const task = tasks.get(taskId);
    if (!task)
        return { ok: false, reason: `Task "${taskId}" not found.` };
    if (!task.steps)
        return { ok: false, reason: `Task "${taskId}" has no steps.` };
    const step = task.steps.find(s => s.id === stepId);
    if (!step)
        return { ok: false, reason: `Step "${stepId}" not found in task "${taskId}".` };
    const newStatus = updates.status;
    // Validate transition
    if (newStatus !== undefined) {
        // Cannot reopen completed step
        if (step.status === 'completed') {
            return { ok: false, reason: `Step "${stepId}" is already completed and cannot be reopened.` };
        }
        // Only one in_progress at a time
        if (newStatus === 'in_progress') {
            const alreadyInProgress = task.steps.find(s => s.id !== stepId && s.status === 'in_progress');
            if (alreadyInProgress) {
                return {
                    ok: false,
                    reason: `Cannot set step "${stepId}" to in_progress: step "${alreadyInProgress.id}" is already in_progress. Complete or block it first.`,
                };
            }
        }
        // completed requires all verification results passed (if any exist)
        if (newStatus === 'completed') {
            const results = updates.verificationResults ?? step.verificationResults;
            const hasFailedResult = results.some(r => !r.passed);
            if (hasFailedResult) {
                return {
                    ok: false,
                    reason: `Cannot complete step "${stepId}": one or more verification checks failed. Fix the failures before marking completed.`,
                };
            }
        }
        // failed and blocked require failureReason
        if ((newStatus === 'failed' || newStatus === 'blocked') && !updates.failureReason && !step.failureReason) {
            return {
                ok: false,
                reason: `Cannot mark step "${stepId}" as ${newStatus} without a failureReason.`,
            };
        }
    }
    // Apply updates
    if (newStatus !== undefined) {
        step.status = newStatus;
        if (newStatus === 'in_progress' && !step.startedAt) {
            step.startedAt = new Date().toISOString();
        }
        if (newStatus === 'completed') {
            step.completedAt = new Date().toISOString();
        }
    }
    if (updates.touchedPaths !== undefined) {
        step.touchedPaths = [...step.touchedPaths, ...updates.touchedPaths];
    }
    if (updates.verificationResults !== undefined) {
        step.verificationResults = updates.verificationResults;
    }
    if (updates.failureReason !== undefined) {
        step.failureReason = updates.failureReason;
    }
    // Update currentStepId on the task
    if (newStatus === 'in_progress') {
        task.currentStepId = stepId;
    }
    else if (newStatus !== undefined && task.currentStepId === stepId) {
        // Step moved away from in_progress — find next in_progress or clear
        const nextInProgress = task.steps.find(s => s.status === 'in_progress');
        task.currentStepId = nextInProgress?.id;
    }
    task.updatedAt = new Date().toISOString();
    return { ok: true, task, step };
}
/**
 * Returns the current active step or the next pending step.
 * Priority: in_progress > first pending.
 * blocked steps are returned (not skipped) so callers know the plan is stuck.
 */
export function getCurrentOrNextStep(taskId) {
    const task = tasks.get(taskId);
    if (!task?.steps)
        return undefined;
    // Prefer in_progress
    const inProgress = task.steps.find(s => s.status === 'in_progress');
    if (inProgress)
        return inProgress;
    // Next pending
    const pending = task.steps.find(s => s.status === 'pending');
    if (pending)
        return pending;
    // Next blocked (stuck)
    return task.steps.find(s => s.status === 'blocked');
}
/**
 * Returns true if the task has any required steps that are not yet done.
 * A step is "open" if its status is pending, in_progress, failed, or blocked.
 * Tasks without steps always return false (pure TODO task).
 */
export function taskHasOpenRequiredSteps(task) {
    if (!task.steps)
        return false;
    return task.steps.some(s => s.status === 'pending' || s.status === 'in_progress' || s.status === 'failed' || s.status === 'blocked');
}
/** Find the latest task with open required steps (for next-step auto-select). */
export function findLatestOpenTask() {
    const allTasks = [...tasks.values()];
    return allTasks
        .filter(t => taskHasOpenRequiredSteps(t))
        .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0];
}
/**
 * Find the next executable step for a given task (or the latest open task if taskId omitted).
 *
 * Rules:
 *   - in_progress step: return it (it's already active)
 *   - first pending step: return it
 *   - blocked step as next: return it with reason='blocked'
 *   - no open steps: hasNext=false, reason='no_open_steps'
 *   - no tasks at all: hasNext=false, reason='no_tasks'
 */
export function findNextStep(taskId) {
    let task;
    if (taskId) {
        task = tasks.get(taskId);
        if (!task)
            return { taskId: taskId ?? '', hasNext: false, reason: 'no_tasks' };
    }
    else {
        task = findLatestOpenTask();
        if (!task)
            return { taskId: '', hasNext: false, reason: 'no_tasks' };
    }
    if (!task.steps || task.steps.length === 0) {
        return { taskId: task.id, hasNext: false, reason: 'no_open_steps' };
    }
    const next = getCurrentOrNextStep(task.id);
    if (!next) {
        return { taskId: task.id, hasNext: false, reason: 'no_open_steps' };
    }
    if (next.status === 'blocked') {
        return { taskId: task.id, hasNext: true, nextStep: next, reason: 'blocked' };
    }
    return { taskId: task.id, hasNext: true, nextStep: next };
}
/** Get a task by ID. */
export function getTask(id) {
    return tasks.get(id);
}
/** List all tasks. */
export function listTasks() {
    return [...tasks.values()];
}
/** Update a task's fields. Returns the updated task or undefined if not found. */
export function updateTask(id, updates) {
    const task = tasks.get(id);
    if (!task)
        return undefined;
    if (updates.subject !== undefined)
        task.subject = updates.subject;
    if (updates.description !== undefined)
        task.description = updates.description;
    if (updates.status !== undefined)
        task.status = updates.status;
    if (updates.activeForm !== undefined)
        task.activeForm = updates.activeForm;
    if (updates.metadata !== undefined)
        task.metadata = { ...task.metadata, ...updates.metadata };
    task.updatedAt = new Date().toISOString();
    return task;
}
/** Add a blocking relationship: taskId blocks blockedId. */
export function blockTask(taskId, blockedId) {
    const task = tasks.get(taskId);
    const blocked = tasks.get(blockedId);
    if (!task || !blocked)
        return false;
    if (!task.blocks.includes(blockedId))
        task.blocks.push(blockedId);
    if (!blocked.blockedBy.includes(taskId))
        blocked.blockedBy.push(taskId);
    return true;
}
/** Delete a task. */
export function deleteTask(id) {
    const task = tasks.get(id);
    if (!task)
        return false;
    // Remove from blocking relationships
    for (const t of tasks.values()) {
        t.blocks = t.blocks.filter(bid => bid !== id);
        t.blockedBy = t.blockedBy.filter(bid => bid !== id);
    }
    // Drop any associated process handle (the caller is expected to have
    // already SIGTERM'd via `stopTask` or this is a test-only purge).
    const child = runningProcesses.get(id);
    if (child) {
        try {
            child.kill('SIGKILL');
        }
        catch { /* ignore */ }
        runningProcesses.delete(id);
    }
    return tasks.delete(id);
}
/**
 * Stop a task: set status='cancelled' BEFORE signalling the child. This
 * ordering matters — the exit handler reads `task.status` to decide
 * whether to keep `cancelled` or transition to `completed`. If we
 * SIGTERM'd first, a fast-exiting child would race past us and the
 * handler would write `completed` over the user's stop request.
 */
export function stopTask(id) {
    const task = tasks.get(id);
    if (!task)
        return undefined;
    if (task.status === 'completed' || task.status === 'cancelled')
        return task;
    const updated = updateTask(id, { status: 'cancelled' });
    const child = runningProcesses.get(id);
    if (child) {
        try {
            child.kill('SIGTERM');
        }
        catch { /* ignore */ }
    }
    return updated;
}
/** Reset store (for testing). SIGKILLs any survivor children. */
export function resetTaskStore() {
    for (const [, child] of runningProcesses) {
        try {
            child.kill('SIGKILL');
        }
        catch { /* ignore */ }
    }
    runningProcesses.clear();
    tasks.clear();
    nextId = 1;
}
/**
 * Spawn the task's bash command and wire stdout/stderr/exit handlers.
 * Caller is responsible for having ALREADY classified the command with
 * `classifyBashCommand` and concluded it is safe to spawn — this fn does
 * NOT re-check risk.
 *
 * Returns immediately; the task transitions to terminal state via the
 * `exit` event. Caller can `await waitForTask(id, timeout)` if it needs
 * to block.
 */
export function spawnTaskCommand(taskId) {
    const task = tasks.get(taskId);
    if (!task || !task.command)
        return;
    // status flips to in_progress on spawn.
    task.status = 'in_progress';
    task.updatedAt = new Date().toISOString();
    task.stdout = '';
    task.stderr = '';
    let child;
    try {
        child = spawn('bash', ['-c', task.command], {
            cwd: task.cwd ?? process.cwd(),
            stdio: ['ignore', 'pipe', 'pipe'],
            env: process.env,
        });
    }
    catch (err) {
        task.status = 'completed';
        task.error = err instanceof Error ? err.message : String(err);
        task.exitCode = -1;
        task.updatedAt = new Date().toISOString();
        return;
    }
    runningProcesses.set(taskId, child);
    // Cap captured I/O to keep a runaway `cat /dev/zero`-style command
    // from ballooning memory. 1 MiB per stream is plenty for an audit
    // trail; consumers wanting full output should use `bash` directly.
    const MAX_BUF = 1024 * 1024;
    child.stdout?.on('data', (chunk) => {
        const cur = task.stdout?.length ?? 0;
        if (cur >= MAX_BUF)
            return;
        task.stdout = (task.stdout ?? '') + chunk.toString('utf8');
        if (task.stdout.length > MAX_BUF) {
            task.stdout = task.stdout.slice(0, MAX_BUF) + '\n[truncated]';
            // Stop reading; caller already has all the audit data they get.
            try {
                child.stdout?.destroy();
            }
            catch { /* ignore */ }
        }
    });
    child.stderr?.on('data', (chunk) => {
        const cur = task.stderr?.length ?? 0;
        if (cur >= MAX_BUF)
            return;
        task.stderr = (task.stderr ?? '') + chunk.toString('utf8');
        if (task.stderr.length > MAX_BUF) {
            task.stderr = task.stderr.slice(0, MAX_BUF) + '\n[truncated]';
            try {
                child.stderr?.destroy();
            }
            catch { /* ignore */ }
        }
    });
    child.on('error', (err) => {
        task.error = err instanceof Error ? err.message : String(err);
    });
    child.on('exit', (code, signal) => {
        runningProcesses.delete(taskId);
        // If `stopTask` already wrote 'cancelled', preserve it. Otherwise
        // transition to 'completed' regardless of code (non-zero exits are
        // still completion in the bash-task sense — the consumer reads
        // exitCode to know if it failed).
        if (task.status !== 'cancelled') {
            task.status = 'completed';
        }
        task.exitCode = typeof code === 'number' ? code : (signal ? -1 : 0);
        task.updatedAt = new Date().toISOString();
    });
}
/** True iff this task currently has a live child process. */
export function hasRunningProcess(taskId) {
    return runningProcesses.has(taskId);
}
/** Test/debug helper. */
export function _processCount() {
    return runningProcesses.size;
}
//# sourceMappingURL=task-store.js.map