/**
 * ci-watch/state.mjs — per-PR state-file primitives for the /apt:watch-ci
 * post-ship watcher (D-04).
 *
 * Storage shape (`.aperant/ci-watches/{pr}.json`), spec.md §8:
 *
 *   {
 *     pr, task_id, host, scheduler_handle | null,
 *     attempts, started_at, deadline, last_poll,
 *     last_status | null, next_tick_interval_ms,
 *     paused, auto_merge_on_green
 *   }
 *
 * This module is pure read/write — classification logic lives in
 * stop-matrix.mjs, the orchestrator lives in commands/ci-watch.mjs.
 *
 * Crash-safety: `writeState` goes through `atomicWriteJson` (tmp + fsync +
 * rename). `readState` returns `null` for a missing file and never throws on
 * a missing path — callers can distinguish "no watch yet" from "watch is
 * corrupted".
 *
 * The corrupted-JSON branch returns a recoverable error envelope rather than
 * throwing so the orchestrator can surface it without crashing the cron
 * payload mid-tick.
 */

import { existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { atomicWriteJson } from '../util/io.mjs'

function watchesDir(root) {
	return join(resolve(root), '.aperant', 'ci-watches')
}

function stateFile(root, pr) {
	return join(watchesDir(root), `${pr}.json`)
}

/**
 * Read the per-PR state file. Returns `null` when the file does not exist.
 * Returns `{ error: 'corrupted', message }` when the file exists but is not
 * parseable JSON — callers route that to the infra-backoff branch.
 *
 * @param {string} root
 * @param {number|string} pr
 */
export function readState(root, pr) {
	const path = stateFile(root, pr)
	if (!existsSync(path)) return null
	try {
		return JSON.parse(readFileSync(path, 'utf-8'))
	} catch (err) {
		return { error: 'corrupted', message: err.message }
	}
}

/**
 * Atomically write the per-PR state file. Caller passes the full state object
 * — this function does not merge, it overwrites. Use `readState` first if a
 * merge is needed.
 *
 * @param {string} root
 * @param {number|string} pr
 * @param {object} state
 */
export function writeState(root, pr, state) {
	atomicWriteJson(stateFile(root, pr), state)
}

/**
 * Delete the per-PR state file. No-op when the file does not exist.
 *
 * @param {string} root
 * @param {number|string} pr
 */
export function deleteState(root, pr) {
	const path = stateFile(root, pr)
	if (existsSync(path)) unlinkSync(path)
}

/**
 * Enumerate every per-PR state file under `.aperant/ci-watches/`. Returns an
 * array of parsed state objects in lexicographic filename order. Skips files
 * whose name does not match `<pr>.json` and skips corrupted files (their
 * presence does not block the rest of the listing).
 *
 * @param {string} root
 */
export function listWatches(root) {
	const dir = watchesDir(root)
	if (!existsSync(dir)) return []
	const watches = []
	for (const f of readdirSync(dir).sort()) {
		if (!/^\d+\.json$/.test(f)) continue
		try {
			watches.push(JSON.parse(readFileSync(join(dir, f), 'utf-8')))
		} catch {
			// Skip corrupted state files — listWatches is a read-only enumerator
			// and must not throw on partial corruption.
		}
	}
	return watches
}
