/**
 * commands/event.mjs — append-only event log for coordination history (C29).
 */
import { appendFileSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { hostname } from 'node:os';
import { join, resolve } from 'node:path';
import { readLifecycleEventsForDay } from '../coordination/event-log.mjs';
import { validateConductorEvent } from '../coordination/event-schema.mjs';
import { createCoordinationStore } from '../coordination/store.mjs';
import { detectWorktree } from '../git/worktree-detect.mjs';
import { parseFlags } from '../util/args.mjs';
import { err, ok } from '../util/result.mjs';
// QUA-003 / PRF-002 — single source of truth for valid event types.
// VALID_EVENT_TYPES is the authoritative list; CI_WATCH_EVENT_TYPES is the O(1)
// membership Set used to short-circuit ci-watch.* events to the JSONL path (D-04).
const VALID_EVENT_TYPES = [
    'task-claimed',
    'task-completed',
    'task-failed',
    'wave-started',
    'wave-completed',
    'install.started',
    'install.ok',
    'install.failed',
    'ci-watch.armed',
    'ci-watch.tick',
    'ci-watch.fix-pushed',
    'ci-watch.stopped',
    // Phase 2.5 — Conductor-readable ops emitted by APT skills so the
    // Conductor's readEventLog tool can drive its observe loop. These
    // events route to JSONL (like ci-watch.*) and carry terminal_id
    // auto-stamped from APERANT_TERMINAL_ID when present.
    'phase.changed',
    'artifact.ready',
    'awaiting-input',
    'input-rescinded',
    'heartbeat',
    'budget.exceeded',
    'terminal.enrolled',
    'terminal.unenrolled',
    'conductor.intent',
    'conductor.cross_check',
    'conductor.realignment',
];
const CI_WATCH_EVENT_TYPES = new Set([
    'ci-watch.armed',
    'ci-watch.tick',
    'ci-watch.fix-pushed',
    'ci-watch.stopped',
]);
const CONDUCTOR_EVENT_TYPES = new Set([
    'phase.changed',
    'artifact.ready',
    'awaiting-input',
    'input-rescinded',
    'heartbeat',
    'budget.exceeded',
    'terminal.enrolled',
    'terminal.unenrolled',
    'conductor.intent',
    'conductor.cross_check',
    'conductor.realignment',
]);
/**
 * Append a ci-watch event to the per-day JSONL file (D-04).
 * O(1) — no directory scan. Creates the events dir if missing.
 * @param {string} eventsDir
 * @param {object} payload
 * @returns {{ jsonlFile: string, jsonlPath: string }}
 */
function appendToJsonl(eventsDir, payload) {
    mkdirSync(eventsDir, { recursive: true });
    const today = new Date().toISOString().slice(0, 10);
    const jsonlFile = `${today}.jsonl`;
    const jsonlPath = join(eventsDir, jsonlFile);
    appendFileSync(jsonlPath, `${JSON.stringify(payload)}\n`, 'utf-8');
    return { jsonlFile, jsonlPath };
}
export function cmdEvent(subcommand, projectDir, extraArgs) {
    if (!subcommand)
        return err('Usage: apt-tools event <append|list|rebuild-state> <project-dir> [flags]');
    if (!projectDir)
        return err('Usage: apt-tools event ' + subcommand + ' <project-dir> [flags]');
    const targetDir = resolve(projectDir);
    // Use shared event dir (coordinates across worktrees via git common dir, same as locks)
    const wt = detectWorktree(targetDir);
    const eventsBase = wt.is_worktree && wt.main_repo_root ? wt.main_repo_root : targetDir;
    const eventsDir = join(eventsBase, '.aperant', 'events');
    mkdirSync(eventsDir, { recursive: true });
    const flags = parseFlags(extraArgs);
    switch (subcommand) {
        case 'append': {
            const type = flags.get('type');
            const subtask = flags.get('subtask') || '';
            const dataStr = flags.get('data');
            if (!type)
                return err('Required: --type <event-type>');
            // C54 — added install.{started,ok,failed} to signal worktree-install
            // progress to the statusline chip + external consumers. Data shape:
            //   install.started — { task_id, ecosystem, pid, started_at, log_path }
            //   install.ok      — { task_id, ecosystem, duration_ms, log_path }
            //   install.failed  — { task_id, ecosystem, exit_code, duration_ms, reason, log_path }
            // C28 (post-ship CI watcher D-04) — added ci-watch.{armed,tick,fix-pushed,stopped}
            // so /apt:watch-ci tick / start / stop can stream events into
            // .aperant/events/{today}.jsonl. The rebuild-state switch below
            // silently ignores them (they're informational, not task-lifecycle).
            if (!VALID_EVENT_TYPES.includes(type)) {
                return err(`Invalid event type: ${type}. Valid types: ${VALID_EVENT_TYPES.join(', ')}`);
            }
            let data = {};
            if (dataStr) {
                try {
                    data = JSON.parse(dataStr);
                }
                catch (e) {
                    return err(`Invalid JSON for --data: ${e.message}`);
                }
            }
            // PRF-002 — ci-watch.* events go to the per-day JSONL path (D-04).
            // O(1) append; no directory scan needed.
            if (CI_WATCH_EVENT_TYPES.has(type)) {
                const event = {
                    type,
                    subtask,
                    worker: `${hostname()}-${process.pid}`,
                    timestamp: new Date().toISOString(),
                    data,
                };
                const { jsonlFile } = appendToJsonl(eventsDir, event);
                return ok({ status: 'ok', command: 'event-append', event_file: jsonlFile, event });
            }
            // Phase 2.5 — Conductor-readable events route to the per-day JSONL
            // path with the canonical envelope the Conductor's readEventLog
            // expects: { t, agent, pid, hostname, incarnation, op, task, data }.
            // terminal_id is auto-stamped from APERANT_TERMINAL_ID env when set,
            // so APT skills running in a Conductor-spawned PTY automatically
            // tag their events with the originating terminal.
            if (CONDUCTOR_EVENT_TYPES.has(type)) {
                const terminalId = process.env.APERANT_TERMINAL_ID;
                if (terminalId && data && typeof data === 'object' && !data.terminal_id) {
                    data.terminal_id = terminalId;
                }
                // Step 1 — schema gate: reject payloads that violate the per-op
                // contract before the line lands on disk. The same validator
                // runs at the framework store seam and the core conductor
                // writer; gating here catches CLI emitters (APT skills).
                const validation = validateConductorEvent(type, data);
                if (!validation.ok) {
                    return err(`Schema violation for ${type}: ${validation.error}`);
                }
                const envelope = {
                    t: new Date().toISOString(),
                    agent: `${hostname()}-${process.pid}`,
                    pid: process.pid,
                    hostname: hostname(),
                    incarnation: 1,
                    op: type,
                    task: subtask || null,
                    data: data || {},
                };
                const { jsonlFile } = appendToJsonl(eventsDir, envelope);
                return ok({
                    status: 'ok',
                    command: 'event-append',
                    event_file: jsonlFile,
                    event: envelope,
                });
            }
            // Find next sequence number — scan existing files for highest number
            const existingFiles = readdirSync(eventsDir).filter((f) => f.endsWith('.json'));
            let maxId = 0;
            for (const f of existingFiles) {
                const num = parseInt(f.split('-')[0], 10);
                if (!isNaN(num) && num > maxId)
                    maxId = num;
            }
            const nextId = maxId + 1;
            const paddedId = String(nextId).padStart(4, '0');
            const eventFile = `${paddedId}-${type}.json`;
            const eventPath = join(eventsDir, eventFile);
            const event = {
                id: nextId,
                type,
                subtask,
                worker: `${hostname()}-${process.pid}`,
                timestamp: new Date().toISOString(),
                data,
            };
            // Write with retry on collision (T-11-02 mitigation)
            let written = false;
            let attempts = 0;
            let finalPath = eventPath;
            let finalFile = eventFile;
            let writeError = null;
            while (!written && attempts < 3) {
                try {
                    writeFileSync(finalPath, JSON.stringify(event, null, 2) + '\n', { flag: 'wx' });
                    written = true;
                }
                catch (e) {
                    if (e.code === 'EEXIST') {
                        attempts++;
                        const retryId = nextId + attempts;
                        event.id = retryId;
                        const retryPadded = String(retryId).padStart(4, '0');
                        finalFile = `${retryPadded}-${type}.json`;
                        finalPath = join(eventsDir, finalFile);
                    }
                    else {
                        writeError = `Failed to write event: ${e.message}`;
                        break;
                    }
                }
            }
            if (writeError)
                return err(writeError);
            if (!written)
                return err('Failed to write event after 3 collision retries');
            return ok({ status: 'ok', command: 'event-append', event_file: finalFile, event });
        }
        case 'list': {
            // R10 — `--today` reads the per-day JSONL lifecycle log added in
            // subtask 4. Separate code path from the legacy numbered-JSON event
            // list (still used by `event rebuild-state`). `--day YYYY-MM-DD`
            // reads an arbitrary day bucket.
            if (flags.has('today') || flags.has('day')) {
                const day = flags.has('day') ? flags.get('day') : undefined;
                const events = readLifecycleEventsForDay(targetDir, day);
                const filtered = flags.has('op')
                    ? events.filter((e) => e && e.op === flags.get('op'))
                    : events;
                return ok({
                    status: 'ok',
                    command: 'event-list',
                    source: 'lifecycle-log',
                    events: filtered,
                    count: filtered.length,
                });
            }
            const filterType = flags.get('type');
            const filterSubtask = flags.get('subtask');
            const eventFiles = readdirSync(eventsDir)
                .filter((f) => f.endsWith('.json'))
                .sort(); // lexicographic = numeric order due to zero-padding
            const events = [];
            for (const f of eventFiles) {
                try {
                    const event = JSON.parse(readFileSync(join(eventsDir, f), 'utf-8'));
                    if (filterType && event.type !== filterType)
                        continue;
                    if (filterSubtask && event.subtask !== filterSubtask)
                        continue;
                    events.push(event);
                }
                catch {
                    // Skip corrupted event files
                }
            }
            return ok({ status: 'ok', command: 'event-list', events, count: events.length });
        }
        case 'rebuild-state': {
            const eventFiles = readdirSync(eventsDir)
                .filter((f) => f.endsWith('.json'))
                .sort();
            const claimed = new Map(); // subtask -> event
            const completed = new Map(); // subtask -> event
            const failed = new Map(); // subtask -> event
            let eventsProcessed = 0;
            for (const f of eventFiles) {
                try {
                    const event = JSON.parse(readFileSync(join(eventsDir, f), 'utf-8'));
                    // Validate event type before processing (T-11-04 mitigation)
                    if (!VALID_EVENT_TYPES.includes(event.type))
                        continue;
                    eventsProcessed++;
                    switch (event.type) {
                        case 'task-claimed':
                            claimed.set(event.subtask, event);
                            break;
                        case 'task-completed':
                            completed.set(event.subtask, event);
                            claimed.delete(event.subtask); // no longer in-progress
                            break;
                        case 'task-failed':
                            failed.set(event.subtask, event);
                            claimed.delete(event.subtask); // no longer in-progress
                            break;
                        // wave-started and wave-completed are informational
                    }
                }
                catch {
                    // Skip corrupted files
                }
            }
            // R8 — route state reads/writes through the @internal CoordinationStore
            // seam so rebuild-state shares a single lock contract with task.mjs
            // writers. Behavior is byte-for-byte identical to the prior
            // withFileLock + atomicWriteJson pattern.
            const store = createCoordinationStore(targetDir);
            store.updateState((existing) => {
                let state = {
                    version: 2,
                    initialized_at: new Date().toISOString(),
                    last_activity: new Date().toISOString(),
                    active_tasks: {},
                    completed_tasks: [],
                    decisions: {},
                    has_tests: false,
                };
                if (existing && typeof existing === 'object') {
                    state = { ...state, ...existing };
                }
                state.completed_tasks = Array.from(completed.values()).map((e) => ({
                    id: e.subtask,
                    description: e.subtask,
                    completed_at: e.timestamp,
                    qa_verdict: 'approved',
                    commits: Array.isArray(e.data?.commits) ? e.data.commits : [],
                }));
                state.last_activity = new Date().toISOString();
                return state;
            });
            return ok({
                status: 'ok',
                command: 'event-rebuild-state',
                events_processed: eventsProcessed,
                tasks_completed: completed.size,
                tasks_failed: failed.size,
                tasks_in_progress: claimed.size,
            });
        }
        default:
            return err(`Unknown event subcommand: ${subcommand}. Expected: append, list, rebuild-state`);
    }
}
//# sourceMappingURL=event.mjs.map