/**
 * commands/adr.mjs — `apt-tools adr write`.
 *
 * Writes an ADR under `docs/adr/NNNN-slug.md` ONLY when all three Nygard
 * gates pass: hard-to-reverse AND surprising-without-context AND
 * real-trade-off. When any gate fails, the writer returns a `filtered`
 * envelope and writes nothing — the decision stays in
 * `.aperant/context/notes/{task-id}.md` instead.
 *
 * CLI:
 *   apt-tools adr write <project-dir>
 *     --title <slug>
 *     --hard-to-reverse <true|false>
 *     --surprising-without-context <true|false>
 *     --real-trade-off <true|false>
 *     --decision <text>
 *     --consequences <text>
 *     [--context <text>]
 *     [--status Proposed|Accepted|Deprecated|Superseded]
 *
 * Output: JSON envelope.
 *   On filter:  { status: 'filtered', failed_gates: string[], reason: 'three-gate-filter' }
 *   On write:   { status: 'ok', command: 'adr-write', file: <abs path>, number: N, slug: '...' }
 */

import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parseFlags } from '../util/args.mjs'
import { err, exitWith, ok } from '../util/result.mjs'

/**
 * Parse a flag value as a strict boolean. Returns null on unrecognized form
 * so callers can distinguish "missing" from "false".
 *
 * @param {unknown} v
 * @returns {boolean|null}
 */
function parseBool(v) {
	if (v === true) return true
	if (typeof v !== 'string') return null
	const low = v.toLowerCase()
	if (low === 'true' || low === 'yes' || low === '1') return true
	if (low === 'false' || low === 'no' || low === '0') return false
	return null
}

/**
 * Slugify a title for the filename (NNNN-slug.md).
 *
 * @param {string} title
 */
function slugify(title) {
	return title
		.toLowerCase()
		.trim()
		.replace(/[^\w\s-]/g, '')
		.replace(/\s+/g, '-')
		.replace(/-+/g, '-')
		.replace(/^-|-$/g, '')
}

/**
 * Find the next ADR sequence number by scanning `docs/adr/`.
 *
 * @param {string} adrDir
 * @returns {number}
 */
function nextAdrNumber(adrDir) {
	if (!existsSync(adrDir)) return 1
	const entries = readdirSync(adrDir)
	let max = 0
	for (const name of entries) {
		const m = name.match(/^(\d{4})-/)
		if (!m) continue
		const n = Number.parseInt(m[1], 10)
		if (n > max) max = n
	}
	return max + 1
}

/**
 * Render the ADR markdown body.
 *
 * @param {{
 *   number: number,
 *   title: string,
 *   status: string,
 *   date: string,
 *   context: string,
 *   decision: string,
 *   consequences: string,
 *   hardToReverse: boolean,
 *   surprisingWithoutContext: boolean,
 *   realTradeOff: boolean,
 * }} opts
 */
function renderAdr(opts) {
	const paddedNumber = String(opts.number).padStart(4, '0')
	return [
		`# ADR-${paddedNumber}: ${opts.title}`,
		'',
		`**Status:** ${opts.status}`,
		`**Date:** ${opts.date}`,
		'',
		'## Context',
		'',
		opts.context,
		'',
		'## Decision',
		'',
		opts.decision,
		'',
		'## Consequences',
		'',
		opts.consequences,
		'',
		'---',
		'',
		'## Nygard 3-gate filter (audit trail)',
		'',
		'This ADR was written via `apt-tools adr write` and passed all three of',
		"Nygard's filter gates. Reversible / obvious / trade-off-free decisions",
		'stay in `.aperant/context/notes/{task-id}.md` and do NOT become ADRs.',
		'',
		`- **Hard to reverse:** ${opts.hardToReverse}`,
		`- **Surprising without context:** ${opts.surprisingWithoutContext}`,
		`- **Real trade-off:** ${opts.realTradeOff}`,
		'',
	].join('\n')
}

/**
 * CLI entry: `apt-tools adr <subcommand> <project-dir> [flags]`.
 */
export function cmdAdr(subcommand, projectDir, extraArgs) {
	if (subcommand !== 'write') {
		return err(
			'Usage: apt-tools adr write <project-dir> --title <slug> --hard-to-reverse <bool> --surprising-without-context <bool> --real-trade-off <bool> --decision <text> --consequences <text> [--context <text>] [--status <status>]',
			1,
			{ command: 'adr' },
		)
	}
	if (!projectDir) {
		return err('project-dir is required', 1, { command: 'adr-write' })
	}

	const flags = parseFlags(extraArgs ?? [])
	const title = flags.get('title')
	if (typeof title !== 'string' || title.trim() === '') {
		return err('--title <slug> is required', 1, { command: 'adr-write' })
	}

	// Three Nygard gates — every one must be parseable; missing means "not
	// asked", which is the same as "no" for the filter.
	const hardToReverse = parseBool(flags.get('hard-to-reverse'))
	const surprisingWithoutContext = parseBool(flags.get('surprising-without-context'))
	const realTradeOff = parseBool(flags.get('real-trade-off'))

	for (const [name, value] of [
		['--hard-to-reverse', hardToReverse],
		['--surprising-without-context', surprisingWithoutContext],
		['--real-trade-off', realTradeOff],
	]) {
		if (value === null) {
			return err(`${name} <true|false> is required`, 1, { command: 'adr-write' })
		}
	}

	const decision = flags.get('decision')
	const consequences = flags.get('consequences')
	if (typeof decision !== 'string' || decision.trim() === '') {
		return err('--decision <text> is required', 1, { command: 'adr-write' })
	}
	if (typeof consequences !== 'string' || consequences.trim() === '') {
		return err('--consequences <text> is required', 1, { command: 'adr-write' })
	}

	// Three-gate filter — every gate must be true to write.
	const failed = []
	if (!hardToReverse) failed.push('hard-to-reverse')
	if (!surprisingWithoutContext) failed.push('surprising-without-context')
	if (!realTradeOff) failed.push('real-trade-off')

	if (failed.length > 0) {
		return exitWith(
			{
				status: 'filtered',
				command: 'adr-write',
				reason: 'three-gate-filter',
				failed_gates: failed,
				note: 'Decision stays in .aperant/context/notes/{task-id}.md. ADRs are reserved for hard-to-reverse, surprising, real-trade-off decisions.',
			},
			0,
		)
	}

	// All three gates passed — write the ADR.
	const absProjectDir = resolve(projectDir)
	if (!existsSync(absProjectDir)) {
		return err(`project directory not found: ${absProjectDir}`, 1, { command: 'adr-write' })
	}

	const adrDir = join(absProjectDir, 'docs', 'adr')
	if (!existsSync(adrDir)) {
		mkdirSync(adrDir, { recursive: true })
	}
	const number = nextAdrNumber(adrDir)
	const slug = slugify(title)
	const paddedNumber = String(number).padStart(4, '0')
	const filename = `${paddedNumber}-${slug}.md`
	const filePath = join(adrDir, filename)

	const status = (typeof flags.get('status') === 'string' && flags.get('status')) || 'Proposed'
	const context =
		(typeof flags.get('context') === 'string' && flags.get('context')) ||
		'{Context not supplied — provide via --context on a re-run.}'
	const date = new Date().toISOString().slice(0, 10)

	const body = renderAdr({
		number,
		title,
		status: String(status),
		date,
		context: String(context),
		decision,
		consequences,
		hardToReverse: true,
		surprisingWithoutContext: true,
		realTradeOff: true,
	})

	writeFileSync(filePath, body)

	return ok({
		status: 'ok',
		command: 'adr-write',
		file: filePath,
		number,
		slug,
		title,
		passed_gates: ['hard-to-reverse', 'surprising-without-context', 'real-trade-off'],
	})
}
