/**
 * commands/constitution.mjs — `apt-tools constitution <list|check>` (C14 subtask 5).
 *
 * Dedicated CLI surface for inspecting AGENTS.md principles independently of
 * the gate pipeline. Both subcommands emit the standard JSON envelope via
 * `util/io.mjs:output`; neither mutates state.
 *
 *   apt-tools constitution list <project-dir> [--profile <name>]
 *     → { status: 'ok', command: 'constitution-list', count, principles }
 *     With --profile: appends profile obligations (normalized to Principle
 *     shape) to the AGENTS.md principle set, deduped by normalized title
 *     (AGENTS.md wins). Adds `profile` + `chain` fields to the envelope.
 *
 *   apt-tools constitution check <project-dir> --task-id <id>
 *     → { status, command: 'constitution-check', task_id,
 *         summary, principles, reason }
 */

import { existsSync, readFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { checkCoverage } from '../constitution/coverage.mjs'
import { deriveKeywords, normalizeTitle, parseAgentsMd } from '../constitution/parse.mjs'
import { createCoordinationStore } from '../coordination/store.mjs'
import { resolveProfile } from '../profile/resolve.mjs'
import { parseFlags } from '../util/args.mjs'
import { err, ok, UsageError } from '../util/result.mjs'

/**
 * Sentinel error class for helper-level usage failures inside cmdConstitution.
 * The dispatcher catches it and converts it into the standard err() envelope.
 */
class ConstitutionUsageError extends UsageError {
	constructor(message) {
		super(message)
		this.name = 'ConstitutionUsageError'
	}
}

export async function cmdConstitution(subcommand, projectDir, extraArgs) {
	if (!subcommand) return err('Usage: apt-tools constitution <list|check> [project-dir] [flags]')
	try {
		switch (subcommand) {
			case 'list':
				return cmdConstitutionList(projectDir, extraArgs ?? [])
			case 'check':
				return cmdConstitutionCheck(projectDir, extraArgs ?? [])
			default:
				return err(`Unknown constitution subcommand: ${subcommand}. Expected: list, check`)
		}
	} catch (e) {
		if (e instanceof ConstitutionUsageError) return err(e.message)
		throw e
	}
}

function resolveSharedRoot(projectDir) {
	const absRoot = resolve(projectDir || '.')
	const store = createCoordinationStore(absRoot)
	return { absRoot, sharedRoot: store.resolveSharedRoot() }
}

function cmdConstitutionList(projectDir, extraArgs) {
	if (!projectDir)
		throw new ConstitutionUsageError('Usage: apt-tools constitution list <project-dir>')
	const flags = parseFlags(extraArgs ?? [])
	const profileName = flags.get('profile')
	const hasProfile = typeof profileName === 'string' && profileName.length > 0

	const { sharedRoot } = resolveSharedRoot(projectDir)
	const agentsPath = join(sharedRoot, 'AGENTS.md')

	// Start with AGENTS.md principles — an absent AGENTS.md is still a valid
	// greenfield state; profile obligations may be the only principles.
	let principles = []
	let greenfield = false
	if (existsSync(agentsPath)) {
		const md = readFileSync(agentsPath, 'utf-8')
		principles = parseAgentsMd(md)
	} else {
		greenfield = true
	}

	if (!hasProfile) {
		const base = {
			status: 'ok',
			command: 'constitution-list',
			count: principles.length,
			principles,
		}
		if (greenfield) base.reason = 'no constitution — greenfield'
		return ok(base)
	}

	// --profile <name> set: resolve the cascade and append obligations as
	// Principle-shaped records, then dedup by normalized title (AGENTS.md
	// wins on collision because we append profile after the base list).
	const resolved = resolveProfile(projectDir, profileName)
	if (!resolved.ok) {
		return ok({
			status: 'fail',
			command: 'constitution-list',
			reason: resolved.reason,
			errors: resolved.errors,
			chain: resolved.chain,
		})
	}
	const profilePrinciples = (resolved.resolved.obligations ?? []).map((o) => ({
		id: o.id,
		title: o.title,
		text: o.text,
		source: { section: `profile:${resolved.resolved.name}`, line: -1 },
		keywords: deriveKeywords(`${o.title} ${o.text}`),
		severity: o.severity,
	}))

	// Dedup: any profile obligation whose normalized title already appears in
	// the AGENTS.md set is dropped (constitution wins). Spec calls this out
	// so the profile cannot silently shadow a hard-coded project principle.
	const seen = new Set(principles.map((p) => normalizeTitle(p.title)))
	const merged = [...principles]
	for (const p of profilePrinciples) {
		const key = normalizeTitle(p.title)
		if (!key || seen.has(key)) continue
		seen.add(key)
		merged.push(p)
	}

	return ok({
		status: 'ok',
		command: 'constitution-list',
		count: merged.length,
		principles: merged,
		profile: resolved.resolved.name,
		chain: resolved.chain,
	})
}

function cmdConstitutionCheck(projectDir, extraArgs) {
	if (!projectDir)
		throw new ConstitutionUsageError(
			'Usage: apt-tools constitution check <project-dir> --task-id <id>',
		)
	const flags = parseFlags(extraArgs)
	const taskId = flags.get('task-id')
	if (!taskId || typeof taskId !== 'string')
		throw new ConstitutionUsageError('Required: --task-id <id>')

	const { sharedRoot } = resolveSharedRoot(projectDir)
	const agentsPath = join(sharedRoot, 'AGENTS.md')
	const taskDir = join(sharedRoot, '.aperant', 'tasks', taskId)
	if (!existsSync(taskDir)) throw new ConstitutionUsageError(`task not found: ${taskId}`)
	const specPath = join(taskDir, 'spec.md')

	const result = checkCoverage({ agentsPath, specPath })
	return ok({
		status: result.status,
		command: 'constitution-check',
		task_id: taskId,
		summary: result.summary,
		reason: result.reason,
		principles: result.principles,
	})
}
