/**
 * commands/context.mjs — `apt-tools context write`.
 *
 * Upserts a term-block into the project's CONTEXT.md (the persistent
 * project glossary). Format ported from the Aperant CONTEXT-FORMAT.md
 * (see packages/framework/templates/context-format.md for the MIT
 * attribution + field semantics).
 *
 * Idempotent: re-running with the same `--term` updates the existing
 * block in place. Fields not explicitly overridden via flags are
 * preserved from the prior block.
 *
 * CLI:
 *   apt-tools context write <project-dir>
 *     --term <bold-term>
 *     --definition <one-line>
 *     [--aliases-to-avoid alias1,alias2]
 *     [--cardinality 'one-to-many' | 'one-to-one' | 'many-to-many']
 *     [--flagged-ambiguity 'note']
 *     [--example-dialogue 'example']
 *
 * Output: JSON envelope { status: 'ok' | 'error',
 *                         command: 'context-write',
 *                         file: <absolute path>,
 *                         term: <term>,
 *                         operation: 'created' | 'updated' }.
 */

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

const HEADER = '# Project glossary (CONTEXT.md)'
const HEADER_NOTE =
	'<!-- Managed by `apt-tools context write`. Hand-edits are preserved across upserts. -->'

/**
 * Parse a single term-block out of CONTEXT.md.
 *
 * A block looks like:
 *   ## {Term}
 *
 *   {Definition line}
 *
 *   - **Aliases to avoid:** ...
 *   - **Cardinality:** ...
 *   - **Flagged ambiguities:** ...
 *   - **Example dialogue:** ...
 *
 * Returns `{ definition, aliasesToAvoid, cardinality, flaggedAmbiguity, exampleDialogue }`
 * with each field as a string or null.
 *
 * @param {string} blockText
 */
function parseBlock(blockText) {
	const lines = blockText.split('\n')
	// First non-empty line after the ## header is the definition.
	let definition = ''
	let i = 0
	while (i < lines.length && lines[i].trim() === '') i++
	if (i < lines.length && !lines[i].startsWith('- ')) {
		definition = lines[i].trim()
		i++
	}
	const fields = {
		aliasesToAvoid: null,
		cardinality: null,
		flaggedAmbiguity: null,
		exampleDialogue: null,
	}
	for (; i < lines.length; i++) {
		const line = lines[i]
		const m = line.match(/^- \*\*([^*]+):\*\*\s*(.*)$/)
		if (!m) continue
		const key = m[1].trim()
		const value = m[2].trim()
		switch (key) {
			case 'Aliases to avoid':
				fields.aliasesToAvoid = value
				break
			case 'Cardinality':
				fields.cardinality = value
				break
			case 'Flagged ambiguities':
				fields.flaggedAmbiguity = value
				break
			case 'Example dialogue':
				fields.exampleDialogue = value
				break
		}
	}
	return { definition, ...fields }
}

/**
 * Render a term-block to markdown.
 *
 * @param {string} term
 * @param {{
 *   definition: string,
 *   aliasesToAvoid: string|null,
 *   cardinality: string|null,
 *   flaggedAmbiguity: string|null,
 *   exampleDialogue: string|null,
 * }} block
 */
function renderBlock(term, block) {
	const lines = [`## ${term}`, '', block.definition, '']
	const fields = [
		['Aliases to avoid', block.aliasesToAvoid],
		['Cardinality', block.cardinality],
		['Flagged ambiguities', block.flaggedAmbiguity],
		['Example dialogue', block.exampleDialogue],
	]
	for (const [key, value] of fields) {
		if (value !== null && value !== undefined && value !== '') {
			lines.push(`- **${key}:** ${value}`)
		}
	}
	return lines.join('\n')
}

/**
 * Find an existing block by term in the source markdown.
 * Returns `{ start, end, prior }` or `null` if no such block exists.
 * `start` and `end` are inclusive/exclusive offsets into `source`.
 *
 * @param {string} source
 * @param {string} term
 */
function findBlock(source, term) {
	const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
	const headerRe = new RegExp(`^##\\s+${escaped}\\s*$`, 'm')
	const m = headerRe.exec(source)
	if (!m) return null
	const start = m.index
	// Find the next ## or # header or EOF. LOG-004: including H1 prevents a
	// manually-inserted `# Heading` from being absorbed into the block's content.
	const tail = source.slice(m.index + m[0].length)
	const nextRe = /^#{1,2}\s+/m
	const nextMatch = nextRe.exec(tail)
	const end = nextMatch ? m.index + m[0].length + nextMatch.index : source.length
	const blockText = source.slice(m.index + m[0].length, end)
	return { start, end, prior: parseBlock(blockText) }
}

/**
 * CLI entry: `apt-tools context <subcommand> <project-dir> [flags]`.
 *
 * Only `write` is supported in v1; other subcommands return a usage error.
 */
export function cmdContext(subcommand, projectDir, extraArgs) {
	if (subcommand !== 'write') {
		return err(
			'Usage: apt-tools context write <project-dir> --term <term> --definition <line> [--aliases-to-avoid ...] [--cardinality ...] [--flagged-ambiguity ...] [--example-dialogue ...]',
			1,
			{ command: 'context' },
		)
	}
	if (!projectDir) {
		return err(
			'Usage: apt-tools context write <project-dir> --term <term> --definition <line>',
			1,
			{
				command: 'context-write',
			},
		)
	}

	const flags = parseFlags(extraArgs ?? [])
	const term = flags.get('term')
	const definition = flags.get('definition')
	if (typeof term !== 'string' || term.trim() === '') {
		return err('--term <bold-term> is required', 1, { command: 'context-write' })
	}
	if (typeof definition !== 'string' || definition.trim() === '') {
		return err('--definition <one-line> is required', 1, { command: 'context-write' })
	}

	const absProjectDir = resolve(projectDir)
	if (!existsSync(absProjectDir)) {
		return err(`project directory not found: ${absProjectDir}`, 1, {
			command: 'context-write',
		})
	}

	const contextPath = resolve(absProjectDir, 'CONTEXT.md')
	let source = ''
	if (existsSync(contextPath)) {
		source = readFileSync(contextPath, 'utf-8')
	} else {
		source = `${HEADER}\n\n${HEADER_NOTE}\n\n`
	}
	// Ensure the file starts with the header even if it existed pre-vertical-slice.
	if (!source.startsWith(HEADER)) {
		source = `${HEADER}\n\n${HEADER_NOTE}\n\n${source}`
	}

	const existing = findBlock(source, term)
	const incoming = {
		definition,
		aliasesToAvoid:
			typeof flags.get('aliases-to-avoid') === 'string' ? flags.get('aliases-to-avoid') : null,
		cardinality: typeof flags.get('cardinality') === 'string' ? flags.get('cardinality') : null,
		flaggedAmbiguity:
			typeof flags.get('flagged-ambiguity') === 'string' ? flags.get('flagged-ambiguity') : null,
		exampleDialogue:
			typeof flags.get('example-dialogue') === 'string' ? flags.get('example-dialogue') : null,
	}

	// Merge: prior block fields are preserved unless explicitly overridden.
	const merged = existing
		? {
				definition: incoming.definition, // definition always overrides
				aliasesToAvoid: incoming.aliasesToAvoid ?? existing.prior.aliasesToAvoid,
				cardinality: incoming.cardinality ?? existing.prior.cardinality,
				flaggedAmbiguity: incoming.flaggedAmbiguity ?? existing.prior.flaggedAmbiguity,
				exampleDialogue: incoming.exampleDialogue ?? existing.prior.exampleDialogue,
			}
		: incoming

	const rendered = renderBlock(term, merged)
	let updated
	if (existing) {
		// Replace the existing block (header + body) with the rendered one.
		const before = source.slice(0, existing.start).replace(/\n+$/, '')
		const after = source.slice(existing.end).replace(/^\n+/, '')
		updated = `${before}\n\n${rendered}\n${after.length > 0 ? `\n${after}` : ''}`
	} else {
		// Append a new block at the end.
		updated = `${source.replace(/\n+$/, '')}\n\n${rendered}\n`
	}

	writeFileSync(contextPath, updated)

	return ok({
		status: 'ok',
		command: 'context-write',
		file: contextPath,
		term,
		operation: existing ? 'updated' : 'created',
	})
}
