/**
 * commands/gate.mjs — `apt-tools gate <list|check>` CLI surface (C2).
 *
 * `list`  — enumerate registered gates, optionally filtered by --skill.
 * `check` — run the gate set for --skill against --task-id, emit the C2
 *           JSON envelope, exit 0 when no blocking failures remain (or
 *           --force --reason was supplied), exit 1 otherwise.
 *
 * Layer 1 (post-CLI-layering refactor, subtask 10). cmdGate is async and
 * returns `Promise<{envelope, exitCode}>` — it MUST NOT call process.exit,
 * output(), or fail() directly. The Layer 2 wrapper (dispatch.mjs `wrap()`)
 * writes stdout and exits.
 */

import { resolve } from 'node:path'
import { createCoordinationStore } from '../coordination/store.mjs'
import { buildGateContext } from '../gate/context.mjs'
import '../gate/gates/index.mjs'
import { listGates } from '../gate/registry.mjs'

// Cache module is optional — when present (subtask 4), the check loop reuses
// prior verdicts keyed on input hashes. The lazy import keeps subtask 3 self
// contained: the CLI works before cache.mjs lands.
let cacheModulePromise = null
async function loadCache() {
	if (!cacheModulePromise) {
		cacheModulePromise = import('../gate/cache.mjs').catch(() => null)
	}
	return cacheModulePromise
}

import { parseFlags } from '../util/args.mjs'
import { err, exitWith, ok } from '../util/result.mjs'

const BLOCKING_SEVERITIES = new Set(['blocking'])

export async function cmdGate(subcommand, projectDir, extraArgs) {
	if (!subcommand) return err('Usage: apt-tools gate <list|check> [project-dir] [flags]')

	switch (subcommand) {
		case 'list':
			return cmdGateList(projectDir, extraArgs ?? [])
		case 'check':
			return await cmdGateCheck(projectDir, extraArgs ?? [])
		default:
			return err(`Unknown gate subcommand: ${subcommand}. Expected: list, check`)
	}
}

function cmdGateList(projectDir, extraArgs) {
	// `list` is a pure read; project-dir is tolerated but unused. If the
	// caller passed `--skill` in the project-dir slot (no explicit dir), we
	// prepend it back into extraArgs so parseFlags picks it up.
	const merged =
		typeof projectDir === 'string' && projectDir.startsWith('--')
			? [projectDir, ...(extraArgs ?? [])]
			: (extraArgs ?? [])
	const flags = parseFlags(merged)
	const skill = typeof flags.get('skill') === 'string' ? flags.get('skill') : null
	const gates = listGates({ skill })
	return ok({
		status: 'ok',
		command: 'gate-list',
		skill: skill ?? null,
		count: gates.length,
		gates: gates.map((g) => ({
			id: g.id,
			description: g.description,
			severity: g.severity,
			skills: g.skills,
		})),
	})
}

async function cmdGateCheck(projectDir, extraArgs) {
	if (!projectDir)
		return err('Usage: apt-tools gate check <project-dir> --task-id <id> --skill <skill> [flags]')
	const absRoot = resolve(projectDir)
	const flags = parseFlags(extraArgs)
	const taskId = flags.get('task-id')
	const skill = flags.get('skill')
	const force = flags.get('force') === true
	const reasonRaw = flags.get('reason')
	const reason = typeof reasonRaw === 'string' ? reasonRaw : null

	if (!taskId || typeof taskId !== 'string') return err('Required: --task-id <id>')
	if (!skill || typeof skill !== 'string') return err('Required: --skill <skill>')
	if (force && !reason)
		return err(
			'--force requires --reason "<explanation>" (the override is logged to the audit trail)',
		)

	let ctx
	try {
		ctx = buildGateContext(absRoot, taskId)
	} catch (e) {
		// Every failure during context build surfaces as the standard envelope,
		// not a raw exception — the CLI contract is "always emit JSON on
		// stdout, exit non-zero on failure". ETASKNOTFOUND is the expected
		// case (bad --task-id); anything else (e.g. unreadable state.json) is
		// still reported through the same shape so callers don't need two
		// parsing paths.
		const reasonMsg =
			e?.code === 'ETASKNOTFOUND' ? e.message : `context build failed: ${e?.message ?? String(e)}`
		return exitWith(
			{
				status: 'fail',
				command: 'gate-check',
				skill,
				task_id: taskId,
				failed: [{ id: 'task-context', severity: 'blocking', reason: reasonMsg }],
				passed: [],
				warned: [],
				skipped: [],
				duration_ms: 0,
			},
			1,
		)
	}

	const gates = listGates({ skill })
	const startedNs = process.hrtime.bigint()

	const passed = []
	const failed = []
	const warned = []
	const skipped = []
	const forced = []
	const cacheHits = []

	// Load the persisted cache once. Writes happen incrementally so a crash
	// mid-run still keeps the verdicts that did complete. When cache.mjs is
	// not yet present (pre-subtask-4), `cacheMod` is null and every gate runs.
	const cacheMod = await loadCache()
	const cache = cacheMod ? cacheMod.readCache(ctx.taskDir) : {}

	for (const gate of gates) {
		// Applicability check fires BEFORE cache lookup or check() execution.
		// Gates that don't apply to the active profile (lite vs full-pipeline)
		// short-circuit with a skip verdict. Skip results are intentionally
		// NOT cached (see status: 'skip' branch below), so a track change on
		// the same task immediately reflects in the next gate-check.
		if (typeof gate.applies_to_profile === 'function' && gate.applies_to_profile(ctx) === false) {
			skipped.push({
				id: gate.id,
				severity: gate.severity,
				reason: `profile-not-applicable: ${ctx.profile}`,
			})
			continue
		}

		let verdict
		let cached = false
		let inputHash = null

		// Cache probe — reuse the prior verdict if the inputs it recorded
		// still hash identically.
		if (cacheMod) {
			const prior = cache[gate.id]
			const priorInputs = prior?.verdict?.inputs
			if (prior && Array.isArray(priorInputs)) {
				const currentHash = cacheMod.computeInputHash(gate.id, priorInputs)
				if (currentHash === prior.key) {
					verdict = prior.verdict
					cached = true
					cacheHits.push(gate.id)
				}
			}
		}

		if (!verdict) {
			try {
				verdict = await gate.check(ctx)
			} catch (e) {
				verdict = {
					status: 'fail',
					reason: `gate ${gate.id} threw: ${e.message}`,
				}
			}
			if (cacheMod && Array.isArray(verdict.inputs) && verdict.inputs.length > 0) {
				inputHash = cacheMod.computeInputHash(gate.id, verdict.inputs)
			}
		}

		const entry = {
			id: gate.id,
			severity: gate.severity,
			reason: verdict.reason,
			...(verdict.evidence ? { evidence: verdict.evidence } : {}),
			...(cached ? { cached: true } : {}),
		}

		if (verdict.status === 'pass') {
			passed.push(entry)
			if (!cached && inputHash && cacheMod)
				cacheMod.writeCache(ctx.taskDir, gate.id, inputHash, {
					status: verdict.status,
					reason: verdict.reason,
					evidence: verdict.evidence ?? null,
					inputs: verdict.inputs ?? [],
				})
		} else if (verdict.status === 'warn') {
			warned.push(entry)
			if (!cached && inputHash && cacheMod)
				cacheMod.writeCache(ctx.taskDir, gate.id, inputHash, {
					status: verdict.status,
					reason: verdict.reason,
					evidence: verdict.evidence ?? null,
					inputs: verdict.inputs ?? [],
				})
		} else if (verdict.status === 'skip') {
			skipped.push(entry)
			// Skipped verdicts are always cheap — intentionally not cached.
		} else if (verdict.status === 'fail') {
			failed.push(entry)
			// Cache failures too (unchanged inputs → still failing) but NEVER
			// cache a forced-through result — force must always re-run the gate
			// so the event log captures the current state.
			if (!cached && inputHash && cacheMod && !force)
				cacheMod.writeCache(ctx.taskDir, gate.id, inputHash, {
					status: verdict.status,
					reason: verdict.reason,
					evidence: verdict.evidence ?? null,
					inputs: verdict.inputs ?? [],
				})
		} else {
			failed.push({ ...entry, reason: `unknown verdict status "${verdict.status}"` })
		}
	}

	// Preserve sub-ms precision: convert ns → ms via float division, not
	// bigint integer division (which would truncate a 142μs check to 0).
	const duration_ms = Number(process.hrtime.bigint() - startedNs) / 1_000_000
	const blockingFailed = failed.filter((f) => BLOCKING_SEVERITIES.has(f.severity))

	// --force path: append one audit event per blocking failure, annotate envelope.
	let eventLogFile = null
	if (force && blockingFailed.length > 0) {
		const store = createCoordinationStore(absRoot)
		for (const f of blockingFailed) {
			const evt = store.appendEvent({
				op: 'gate.force-override',
				task: taskId,
				data: {
					gate_id: f.id,
					skill,
					severity: f.severity,
					reason,
					gate_reason: f.reason,
				},
			})
			if (evt?.file) eventLogFile = evt.file
			forced.push({ id: f.id, severity: f.severity, reason })
		}
	}

	const envelope = {
		command: 'gate-check',
		skill,
		task_id: taskId,
		profile: ctx.profile,
		passed: passed.map((p) => p.id),
		warned: warned.map((w) => ({ id: w.id, reason: w.reason })),
		skipped: skipped.map((s) => ({ id: s.id, reason: s.reason })),
		failed: blockingFailed.map((f) => ({ id: f.id, severity: f.severity, reason: f.reason })),
		duration_ms,
	}
	if (cacheHits.length > 0) envelope.cache_hits = cacheHits

	if (blockingFailed.length === 0) {
		envelope.status = 'ok'
		return ok(envelope)
	}

	if (force) {
		envelope.status = 'ok'
		envelope.forced = forced
		envelope.failed_before_force = envelope.failed
		envelope.failed = []
		if (eventLogFile) envelope.event_logged = eventLogFile
		return ok(envelope)
	}

	envelope.status = 'fail'
	envelope.hint =
		'Re-run with --force --reason "<explanation>" to override (the bypass will be logged to the audit trail).'
	return exitWith(envelope, 1)
}
