/**
 * util/aperant-section.mjs — generator for the CLAUDE.md / AGENTS.md
 * "## Aperant Framework" section (FRAMEWORK-BUG-023).
 *
 * Replaces the previously-hardcoded `APT_SECTION` constant in copy.mjs.
 * The skill TABLE is rebuilt from `discoverSkills` (filesystem source of
 * truth — same helper the router uses at runtime) so install and runtime
 * can never disagree about the catalog.
 *
 * Curated-doc risk: the appendix template
 * (`templates/aperant-claude-md-appendix.md`) still contains hand-written
 * sub-sections — "Fast Path Guarantee", "Discoverability —
 * `apt-tools modes list`", "Feature Registry", "Config". These are small
 * (~30 lines), rarely change, and rot would be visible (sections about
 * non-existent features). Acceptable tradeoff per plan §Approach (c).
 */

import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { getTrustedSkillSources } from '../config/load.mjs'
import { discoverSkills } from '../route/skill-discover.mjs'

const SECTION_START = '<!-- APT:framework-start -->'
const SECTION_END = '<!-- APT:framework-end -->'

/**
 * Matches the full APT managed block (start marker through end marker,
 * inclusive). Exported so callers in the same package can reference a
 * single source rather than inlining duplicate regex literals.
 *
 * `APT_SECTION_RE`       — no trailing newline; used for in-place replace.
 * `APT_SECTION_STRIP_RE` — includes optional trailing newline; used for
 *                          strip operations that want to remove the newline
 *                          the block was preceded/followed by.
 */
export const APT_SECTION_RE = /<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->/
export const APT_SECTION_STRIP_RE =
	/<!-- APT:framework-start -->[\s\S]*?<!-- APT:framework-end -->\n?/
const SECTION_HEADING = '## Aperant Framework'
const APPENDIX_REL_PATH = ['templates', 'aperant-claude-md-appendix.md']

/**
 * Escape a SKILL `description` for safe inclusion as a markdown table cell.
 * The only character we have to defend against is the pipe (`|`) — it
 * terminates the table column. `apt:triage` ships with a literal `|` in
 * its description, so this is a live case, not a theoretical one.
 *
 * @param {string} text
 * @returns {string}
 */
function escapePipes(text) {
	return text.replace(/\|/g, '\\|')
}

/**
 * Deterministic sort: the bare `apt` router skill first, then ASCII-sorted
 * by `name`. The router goes first so the generated table mirrors the user's
 * mental model — the entry point, then everything reachable from it.
 *
 * @param {{ name: string }} a
 * @param {{ name: string }} b
 * @returns {number}
 */
function sortSkills(a, b) {
	if (a.name === 'apt' && b.name !== 'apt') return -1
	if (b.name === 'apt' && a.name !== 'apt') return 1
	return a.name.localeCompare(b.name)
}

/**
 * Build the full "## Aperant Framework" section, wrapped in the
 * `<!-- APT:framework-* -->` markers.
 *
 * @param {Object} opts
 * @param {string} opts.canonicalRoot   Absolute path to the framework root
 *                                       (the directory that contains
 *                                       `templates/aperant-claude-md-appendix.md`).
 * @param {string} opts.targetDir       Project root (passed to discoverSkills).
 * @param {string} [opts.cli]           Host CLI label (forwarded to
 *                                       getTrustedSkillSources so the install
 *                                       root for the runtime is included).
 * @returns {string}                     The full section, including markers
 *                                       and trailing newline before the
 *                                       end-marker. No leading newline.
 */
export function buildAptSection({ canonicalRoot, targetDir, cli }) {
	const trustedSkillSources = getTrustedSkillSources(targetDir, cli)
	const discovered = discoverSkills({ targetDir, trustedSkillSources })
	const skills = discovered.skills
		.filter((s) => s.user_invocable === true && s.internal === false)
		.slice()
		.sort(sortSkills)

	const rows = skills.map((s) => `| \`/${s.name}\` | ${escapePipes(s.description)} |`)

	const tableBlock = [
		'### Available Skills',
		'| Skill | Description |',
		'|-------|-------------|',
		...rows,
	].join('\n')

	const appendixPath = join(canonicalRoot, ...APPENDIX_REL_PATH)
	let appendix
	try {
		appendix = readFileSync(appendixPath, 'utf-8').replace(/\n+$/, '')
	} catch (cause) {
		const err = new Error(
			`aperant-section: template not found at ${appendixPath} — re-install the framework package`,
			{ cause },
		)
		err.code = 'aperant-section.template-missing'
		throw err
	}

	const body = [SECTION_HEADING, '', appendix, '', tableBlock].join('\n')

	return `${SECTION_START}\n${body}\n${SECTION_END}`
}

/**
 * Extract the `/apt:<slug>` (or bare `/apt`) row IDs from the
 * `<!-- APT:framework-* -->` section of an existing instruction file.
 * Returns `[]` when the file has no APT section.
 *
 * Used by `injectInstructionFile` to detect foreign rows that would be
 * silently clobbered by a regeneration — see `--force-overwrite-catalog`.
 *
 * @param {string} text   Full instruction-file contents.
 * @returns {string[]}    Row IDs in file order, e.g. `['/apt', '/apt:quick', …]`.
 */
export function extractAptSectionRowIds(text) {
	const match = text.match(/<!-- APT:framework-start -->([\s\S]*?)<!-- APT:framework-end -->/)
	if (!match) return []
	// Strip HTML comments (including multiline) before scanning for row IDs so
	// that commented-out example rows don't trigger false-positive foreign-row exits.
	const section = match[1].replace(/<!--[\s\S]*?-->/g, '')
	const ids = []
	// Match table rows whose first cell is `/apt` or `/apt:<slug>` wrapped in
	// backticks. The leading `|` plus optional whitespace anchors us to a
	// markdown table cell; the trailing backtick + ` |` anchors the column
	// boundary so we don't match prose mentions like "the `/apt` router".
	const rowRe = /^\|\s*`(\/apt(?::[a-z0-9-]+)?)`\s*\|/gm
	let m = rowRe.exec(section)
	while (m !== null) {
		ids.push(m[1])
		m = rowRe.exec(section)
	}
	return ids
}
