/**
 * @internal
 * event-log.mjs — append-only lifecycle event log (R10).
 *
 * Writes one JSON-encoded line per event to
 * `.aperant/events/{YYYY-MM-DD}.jsonl` via the R3 appendJsonl primitive.
 * POSIX O_APPEND gives us atomic concurrent writes across agents, so
 * multiple processes running `task update` / `task close` cannot tear each
 * other's lines.
 *
 * Consumption policy:
 *   - WRITES go through the CoordinationStore seam (`store.appendEvent`).
 *     This is the policy boundary — concurrent writers must coordinate.
 *   - READ-ONLY helpers (`readLifecycleEventsForDay`,
 *     `findMostRecentEventForTask`, `readTaskEventsOverDays`) MAY be
 *     imported directly. They do not mutate state and the event log is
 *     append-only, so a stale read is the worst case and is bounded by
 *     "the next read picks up new appends." Direct read imports keep the
 *     R6 telemetry surface (PR #115) out of the store seam, which is
 *     scoped to coordination primitives.
 *
 * Do NOT import this module from bin/apt-tools.mjs directly. CLI commands
 * import the helpers above via their command modules.
 *
 * The existing numbered-JSON event surface in commands/event.mjs is NOT
 * touched — its `rebuild-state` consumer keeps working. The JSONL log is
 * additive, covering the forensic trail the roundtable's Devil's Advocate
 * required for when state.json / team-status leave git (R2, R6).
 */

import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { appendJsonl } from '../util/io.mjs'
import { getAgentIdentity, resolveSharedRoot } from './agent-identity.mjs'

/**
 * Derive the day bucket for the current instant. UTC so agents on different
 * machines agree on the file name.
 */
function todayBucket(now = new Date()) {
	return now.toISOString().slice(0, 10)
}

/**
 * Append a lifecycle event. `evt` shape:
 *   {
 *     op: 'task.created' | 'task.lifecycle.{phase}' | 'task.closed' | ...,
 *     task: string,                // task id
 *     data?: object,               // op-specific payload
 *   }
 *
 * The writer stamps `t`, `agent`, `pid`, `hostname`, `incarnation` onto the
 * record so consumers can correlate across machines without fetching state.
 *
 * `projectRoot` may be a worktree path; the writer resolves the shared main
 * repo root so every agent's events land in a single directory.
 *
 * @internal
 */
export function appendLifecycleEvent(projectRoot, evt, now = new Date()) {
	const sharedRoot = resolveSharedRoot(projectRoot)
	const identity = getAgentIdentity(projectRoot)
	const file = join(sharedRoot, '.aperant', 'events', `${todayBucket(now)}.jsonl`)
	const record = {
		t: now.toISOString(),
		agent: identity.agentId,
		pid: identity.pid,
		hostname: identity.hostname,
		incarnation: identity.incarnation,
		op: evt.op,
		task: evt.task ?? null,
		data: evt.data ?? {},
	}
	appendJsonl(file, record)
	return { file, record }
}

/**
 * Read all events for a given day bucket (defaults to today, UTC). Returns
 * an array of parsed objects; malformed lines are skipped defensively.
 *
 * @internal
 */
export function readLifecycleEventsForDay(projectRoot, day = todayBucket()) {
	const sharedRoot = resolveSharedRoot(projectRoot)
	const file = join(sharedRoot, '.aperant', 'events', `${day}.jsonl`)
	if (!existsSync(file)) return []
	const body = readFileSync(file, 'utf-8')
	const out = []
	for (const line of body.split('\n')) {
		if (!line) continue
		try {
			out.push(JSON.parse(line))
		} catch {
			/* skip malformed */
		}
	}
	return out
}

/**
 * Read events for a task across the today + yesterday UTC day buckets,
 * sorted ascending by `t`. Optionally filtered to events whose `op` starts
 * with `opPrefix`. Covers tasks that span a UTC day boundary without
 * globbing the whole `.aperant/events/` directory.
 *
 * Read-only — does NOT mutate state. Per the original `@internal` policy
 * this module is reached via `store.appendEvent` for WRITES, but read-only
 * helpers like this one (and `readLifecycleEventsForDay`) are direct
 * imports because there is no state to coordinate (PR #115 review finding
 * FIT-001 — comment updated accordingly).
 *
 * @internal
 */
export function readTaskEventsOverDays(projectRoot, taskId, opPrefix = null) {
	function filterAndPush(events, into) {
		for (const e of events) {
			if (e.task !== taskId) continue
			if (opPrefix && e.op !== opPrefix && !e.op.startsWith(opPrefix)) continue
			into.push(e)
		}
	}
	const today = todayBucket()
	const yesterday = new Date()
	yesterday.setUTCDate(yesterday.getUTCDate() - 1)
	const yest = yesterday.toISOString().slice(0, 10)
	const out = []
	// Yesterday first so the combined array stays chronological.
	filterAndPush(readLifecycleEventsForDay(projectRoot, yest), out)
	filterAndPush(readLifecycleEventsForDay(projectRoot, today), out)
	return out
}

/**
 * Find the most recent event matching `taskId`, optionally filtered to events
 * whose `op` starts with `opPrefix`. Looks at today's bucket first; only
 * reads yesterday's bucket on a today-miss — keeps the common case to a
 * single file read.
 *
 * Used by R6 telemetry to compute `elapsed_ms` between lifecycle transitions
 * (roundtable 2026-05-14). Returns null when no matching event is found.
 *
 * @internal
 */
export function findMostRecentEventForTask(projectRoot, taskId, opPrefix = null) {
	function search(events) {
		const filtered = events.filter(
			(e) => e.task === taskId && (!opPrefix || e.op === opPrefix || e.op.startsWith(opPrefix)),
		)
		return filtered.length > 0 ? filtered[filtered.length - 1] : null
	}
	const today = todayBucket()
	const todayMatch = search(readLifecycleEventsForDay(projectRoot, today))
	if (todayMatch) return todayMatch
	const yesterday = new Date()
	yesterday.setUTCDate(yesterday.getUTCDate() - 1)
	const yest = yesterday.toISOString().slice(0, 10)
	return search(readLifecycleEventsForDay(projectRoot, yest))
}
