/**
 * commands/workflow.mjs — workflow phase-enforcement runner (C29).
 *
 * Usage:
 *   apt-tools workflow create  <project-dir> --definition <path>
 *   apt-tools workflow advance <project-dir> --id <wf-id> --phase <name>
 *   apt-tools workflow status  <project-dir> --id <wf-id>
 *   apt-tools workflow destroy <project-dir> --id <wf-id> [--archive]
 *
 * With --archive (C56 A1): move the workflow file to
 * `.aperant/workflows/archive/{id}-{unix-ts}.json` instead of unlink. The
 * envelope gains `archived_to: <path> | null`. Default behavior is unchanged.
 *
 * State lives in .aperant/workflows/{id}.json, coordinated via file locks.
 * Any CLI (Claude Code, Codex, Gemini CLI) that can run bash commands gets
 * identical enforcement — the gate logic is in the tool, not in the prompt.
 */
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { parseFlags } from '../util/args.mjs';
import { withFileLock } from '../util/fs-lock.mjs';
import { err, ok, UsageError } from '../util/result.mjs';
/**
 * Sentinel error class for helper-level usage failures inside cmdWorkflow.
 * The dispatcher catches it and converts it into the standard err() envelope.
 */
class WorkflowUsageError extends UsageError {
    constructor(message) {
        super(message);
        this.name = 'WorkflowUsageError';
    }
}
export function cmdWorkflow(subcommand, projectDir, extraArgs) {
    if (!subcommand)
        return err('Usage: apt-tools workflow <create|advance|status|destroy> <project-dir> [flags]');
    if (!projectDir)
        return err('Usage: apt-tools workflow ' + subcommand + ' <project-dir> [flags]');
    const targetDir = resolve(projectDir);
    const workflowsDir = join(targetDir, '.aperant', 'workflows');
    mkdirSync(workflowsDir, { recursive: true });
    const flags = parseFlags(extraArgs);
    try {
        switch (subcommand) {
            case 'create': {
                const defPath = flags.get('definition');
                if (!defPath)
                    throw new WorkflowUsageError('Required: --definition <path> (JSON file with id and phases)');
                const absDefPath = resolve(defPath);
                if (!existsSync(absDefPath))
                    throw new WorkflowUsageError(`Definition file not found: ${absDefPath}`);
                let definition;
                try {
                    definition = JSON.parse(readFileSync(absDefPath, 'utf-8'));
                }
                catch (e) {
                    throw new WorkflowUsageError(`Failed to parse definition file: ${e.message}`);
                }
                if (!definition.id)
                    throw new WorkflowUsageError('Definition must have an "id" field');
                if (!Array.isArray(definition.phases) || definition.phases.length === 0) {
                    throw new WorkflowUsageError('Definition must have a non-empty "phases" array');
                }
                // Validate phase objects
                for (const phase of definition.phases) {
                    if (!phase.name)
                        throw new WorkflowUsageError(`Each phase must have a "name" field`);
                    if (typeof phase.required === 'undefined')
                        phase.required = true;
                }
                // Check for duplicates
                const names = definition.phases.map((p) => p.name);
                const dupes = names.filter((n, i) => names.indexOf(n) !== i);
                if (dupes.length > 0)
                    throw new WorkflowUsageError(`Duplicate phase names: ${dupes.join(', ')}`);
                const wfPath = join(workflowsDir, `${definition.id}.json`);
                if (existsSync(wfPath))
                    throw new WorkflowUsageError(`Workflow already exists: ${definition.id}`);
                const workflow = {
                    id: definition.id,
                    created_at: new Date().toISOString(),
                    active_phase: null,
                    phases: definition.phases.map((p) => ({
                        name: p.name,
                        required: p.required !== false,
                        status: 'pending',
                        entered_at: null,
                        completed_at: null,
                    })),
                };
                withFileLock(wfPath, () => {
                    writeFileSync(wfPath, JSON.stringify(workflow, null, 2) + '\n');
                });
                return ok({
                    status: 'ok',
                    command: 'workflow-create',
                    workflow_id: definition.id,
                    phases: workflow.phases.map((p) => p.name),
                });
            }
            case 'advance': {
                const wfId = flags.get('id');
                const targetPhase = flags.get('phase');
                if (!wfId)
                    throw new WorkflowUsageError('Required: --id <workflow-id>');
                if (!targetPhase)
                    throw new WorkflowUsageError('Required: --phase <phase-name>');
                const wfPath = join(workflowsDir, `${wfId}.json`);
                if (!existsSync(wfPath))
                    throw new WorkflowUsageError(`Workflow not found: ${wfId}`);
                // Use result variable to avoid throwing inside withFileLock
                // (throw bypasses the finally{} lock cleanup)
                const result = withFileLock(wfPath, () => {
                    const workflow = JSON.parse(readFileSync(wfPath, 'utf-8'));
                    // Find target phase
                    const targetIdx = workflow.phases.findIndex((p) => p.name === targetPhase);
                    if (targetIdx === -1) {
                        return {
                            error: `Phase not found: "${targetPhase}". Available: ${workflow.phases.map((p) => p.name).join(', ')}`,
                        };
                    }
                    const target = workflow.phases[targetIdx];
                    // Prevent re-entering a completed phase
                    if (target.status === 'completed') {
                        return { error: `Phase "${targetPhase}" is already completed` };
                    }
                    // Gate check: all required phases before target must be completed
                    // Active phases are treated as completable (they'll be auto-completed)
                    const blockers = [];
                    for (let i = 0; i < targetIdx; i++) {
                        const prior = workflow.phases[i];
                        if (prior.required && prior.status !== 'completed' && prior.status !== 'active') {
                            blockers.push(prior.name);
                        }
                    }
                    if (blockers.length > 0) {
                        return {
                            error: `Cannot advance to "${targetPhase}" — required phase(s) not completed: ${blockers.join(', ')}`,
                        };
                    }
                    const now = new Date().toISOString();
                    // Auto-complete currently active phase (if any)
                    if (workflow.active_phase) {
                        const activeIdx = workflow.phases.findIndex((p) => p.name === workflow.active_phase);
                        if (activeIdx !== -1 && workflow.phases[activeIdx].status === 'active') {
                            workflow.phases[activeIdx].status = 'completed';
                            workflow.phases[activeIdx].completed_at = now;
                        }
                    }
                    // Auto-skip optional phases between current and target
                    for (let i = 0; i < targetIdx; i++) {
                        const prior = workflow.phases[i];
                        if (prior.status === 'pending' && !prior.required) {
                            prior.status = 'skipped';
                            prior.completed_at = now;
                        }
                    }
                    // Activate target phase
                    target.status = 'active';
                    target.entered_at = now;
                    workflow.active_phase = targetPhase;
                    writeFileSync(wfPath, JSON.stringify(workflow, null, 2) + '\n');
                    return {
                        ok: true,
                        phase: targetPhase,
                        phase_index: targetIdx,
                        total_phases: workflow.phases.length,
                        remaining: workflow.phases.filter((p) => p.status === 'pending').length,
                    };
                });
                if (result.error) {
                    throw new WorkflowUsageError(result.error);
                }
                return ok({
                    status: 'ok',
                    command: 'workflow-advance',
                    workflow_id: wfId,
                    phase: result.phase,
                    phase_index: result.phase_index,
                    total_phases: result.total_phases,
                    remaining: result.remaining,
                });
            }
            case 'status': {
                const wfId = flags.get('id');
                if (!wfId)
                    throw new WorkflowUsageError('Required: --id <workflow-id>');
                const wfPath = join(workflowsDir, `${wfId}.json`);
                if (!existsSync(wfPath))
                    throw new WorkflowUsageError(`Workflow not found: ${wfId}`);
                const workflow = JSON.parse(readFileSync(wfPath, 'utf-8'));
                return ok({
                    status: 'ok',
                    command: 'workflow-status',
                    workflow_id: wfId,
                    active_phase: workflow.active_phase,
                    phases: workflow.phases.map((p) => ({
                        name: p.name,
                        status: p.status,
                        required: p.required,
                    })),
                    completed: workflow.phases.filter((p) => p.status === 'completed').length,
                    total: workflow.phases.length,
                    pending: workflow.phases.filter((p) => p.status === 'pending').length,
                });
            }
            case 'destroy': {
                const wfId = flags.get('id');
                if (!wfId)
                    throw new WorkflowUsageError('Required: --id <workflow-id>');
                const archive = flags.has('archive');
                const wfPath = join(workflowsDir, `${wfId}.json`);
                if (!existsSync(wfPath)) {
                    return ok({
                        status: 'ok',
                        command: 'workflow-destroy',
                        workflow_id: wfId,
                        archived_to: null,
                        message: 'No workflow file found (already destroyed)',
                    });
                }
                // Read final state before destroying
                const workflow = JSON.parse(readFileSync(wfPath, 'utf-8'));
                const summary = {
                    completed: workflow.phases.filter((p) => p.status === 'completed').length,
                    skipped: workflow.phases.filter((p) => p.status === 'skipped').length,
                    pending: workflow.phases.filter((p) => p.status === 'pending').length,
                };
                // C56 A1 — optional archive path. Default behavior unchanged (unlink);
                // with --archive, move the file to .aperant/workflows/archive/{id}-{ts}.json
                // so operators can audit post-merge lifecycle GCs.
                let archivedTo = null;
                if (archive) {
                    const archiveDir = join(workflowsDir, 'archive');
                    mkdirSync(archiveDir, { recursive: true });
                    const ts = Date.now();
                    archivedTo = join(archiveDir, `${wfId}-${ts}.json`);
                    renameSync(wfPath, archivedTo);
                }
                else {
                    unlinkSync(wfPath);
                }
                // Also clean up lock sidecar if it exists
                const lockPath = wfPath + '.lock';
                if (existsSync(lockPath)) {
                    try {
                        unlinkSync(lockPath);
                    }
                    catch {
                        /* ignore */
                    }
                }
                return ok({
                    status: 'ok',
                    command: 'workflow-destroy',
                    workflow_id: wfId,
                    summary,
                    archived_to: archivedTo,
                });
            }
            default:
                return err(`Unknown workflow subcommand: ${subcommand}. Expected: create, advance, status, destroy`);
        }
    }
    catch (e) {
        if (e instanceof WorkflowUsageError)
            return err(e.message);
        throw e;
    }
}
//# sourceMappingURL=workflow.mjs.map