/**
 * commands/check.mjs — router-0.3.0 integrity check.
 *
 * Walks every canonical-source `packages/framework/skills/apt-<slug>/SKILL.md`,
 * validates its frontmatter against SkillFrontmatterSchema, and cross-
 * references every `gates: [...]` entry against the live gate registry
 * (populated by `import '../gate/gates/index.mjs'`). Used by:
 *
 *   - `apt-tools check <project-dir>` directly (CI friction or local
 *     authoring sanity check).
 *   - `apt-tools init <project-dir>` as a pre-install guard so malformed
 *     skills can't leak into runtime install roots.
 *
 * Exit semantics: envelope.status === 'ok' when zero errors, exit 0.
 * Any error → status 'fail' + exit 1. Warnings do NOT block (they're
 * informational only).
 */

import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parse as parseYaml } from 'yaml'
import '../gate/gates/index.mjs'
import { listGates } from '../gate/registry.mjs'
import { sanitizeAptVersionForYamlParse } from '../install/version-header.mjs'
import { SkillFrontmatterSchema } from '../skill-author/contract.mjs'
import { exitWith, ok } from '../util/result.mjs'

const __filename = fileURLToPath(import.meta.url)
const FRAMEWORK_ROOT = resolve(dirname(__filename), '..', '..', '..')
const SKILLS_ROOT_DEFAULT = join(FRAMEWORK_ROOT, 'skills')

const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/

/**
 * @typedef {{ file: string, field: string, message: string }} CheckError
 * @typedef {{ file: string, field: string, message: string }} CheckWarning
 */

/**
 * Walk a skills root and return absolute paths to every apt-<slug>/SKILL.md.
 *
 * @param {string} root
 * @returns {string[]}
 */
function listSkillFiles(root) {
	if (!existsSync(root)) return []
	let stat
	try {
		stat = statSync(root)
	} catch {
		return []
	}
	if (!stat.isDirectory()) return []
	const out = []
	for (const entry of readdirSync(root, { withFileTypes: true })) {
		if (!entry.isDirectory() || !entry.name.startsWith('apt')) continue
		const skillFile = join(root, entry.name, 'SKILL.md')
		if (existsSync(skillFile)) out.push(skillFile)
	}
	return out.sort()
}

/**
 * Parse + validate one SKILL.md's frontmatter.
 *
 * @param {string} file
 * @returns {{ errors: CheckError[], warnings: CheckWarning[], frontmatter: Record<string, unknown>|null }}
 */
function checkSkill(file) {
	const errors = []
	const warnings = []
	let frontmatter = null
	let text
	try {
		text = readFileSync(file, 'utf-8')
	} catch (err) {
		errors.push({
			file,
			field: '(read)',
			message: err && /** @type {Error} */ (err).message ? err.message : String(err),
		})
		return { errors, warnings, frontmatter }
	}
	const m = text.match(FRONTMATTER_RE)
	if (!m) {
		errors.push({ file, field: '(frontmatter)', message: 'missing YAML frontmatter block' })
		return { errors, warnings, frontmatter }
	}
	let parsed
	try {
		// Canonical SKILL.md frontmatter carries the literal `{{APT_VERSION}}`
		// Mustache placeholder, which yaml@2 interprets as a flow-map-keyed-by-
		// flow-map and warns about on every parse. Swap for a YAML-safe
		// placeholder before parse — the schema accepts string | object here,
		// so this is a no-op for validation and silences ~29 install-time warnings.
		parsed = parseYaml(sanitizeAptVersionForYamlParse(m[1]))
	} catch (err) {
		errors.push({
			file,
			field: '(frontmatter)',
			message: `YAML parse error: ${err && /** @type {Error} */ (err).message ? err.message : String(err)}`,
		})
		return { errors, warnings, frontmatter }
	}
	if (parsed === null || typeof parsed !== 'object') {
		errors.push({
			file,
			field: '(frontmatter)',
			message: 'frontmatter did not parse to an object',
		})
		return { errors, warnings, frontmatter }
	}
	frontmatter = /** @type {Record<string, unknown>} */ (parsed)
	const schemaRes = SkillFrontmatterSchema.safeParse(frontmatter)
	if (!schemaRes.success) {
		for (const issue of schemaRes.error.issues) {
			errors.push({
				file,
				field: issue.path.length ? issue.path.join('.') : '(root)',
				message: issue.message,
			})
		}
		return { errors, warnings, frontmatter }
	}
	return { errors, warnings, frontmatter }
}

/**
 * Cross-reference every `gates: [...]` entry against the live gate
 * registry. Unknown IDs are hard errors.
 *
 * @param {string} file
 * @param {Record<string, unknown>} frontmatter
 * @param {Set<string>} knownGateIds
 * @returns {CheckError[]}
 */
function checkGateIds(file, frontmatter, knownGateIds) {
	const errors = []
	const gates = /** @type {unknown} */ (frontmatter.gates)
	if (!Array.isArray(gates)) return errors
	for (const id of gates) {
		if (typeof id !== 'string' || id.length === 0) {
			errors.push({
				file,
				field: 'gates',
				message: `gates entry must be a non-empty string (got ${JSON.stringify(id)})`,
			})
			continue
		}
		if (!knownGateIds.has(id)) {
			errors.push({
				file,
				field: 'gates',
				message: `unknown gate id "${id}" — not in gate registry`,
			})
		}
	}
	return errors
}

/**
 * Resolve the skills root to scan. Prefers the SKILL_DIR env var (used by
 * unit tests planting fixtures under a tmpdir), then the explicit
 * --skills-root CLI flag, and finally falls back to the framework's
 * canonical `packages/framework/skills`.
 *
 * @param {string[]} extraArgs
 * @returns {string}
 */
function resolveSkillsRoot(extraArgs) {
	if (process.env.APT_CHECK_SKILLS_ROOT) return process.env.APT_CHECK_SKILLS_ROOT
	for (let i = 0; i < extraArgs.length; i++) {
		if (extraArgs[i] === '--skills-root' && extraArgs[i + 1]) {
			return resolve(extraArgs[i + 1])
		}
	}
	return SKILLS_ROOT_DEFAULT
}

/**
 * Run the check. Exported as a pure fn so `init.mjs` can call it without
 * an extra subprocess and `cmdCheck` can wrap it with envelope + exit.
 *
 * @param {Object} [opts]
 * @param {string} [opts.skillsRoot]
 * @returns {{ status: 'ok'|'fail', files_checked: number, errors: CheckError[], warnings: CheckWarning[] }}
 */
export function runCheck({ skillsRoot } = {}) {
	const root = skillsRoot ?? SKILLS_ROOT_DEFAULT
	const files = listSkillFiles(root)
	const knownGateIds = new Set(listGates().map((g) => g.id))
	const errors = []
	const warnings = []
	for (const file of files) {
		const result = checkSkill(file)
		errors.push(...result.errors)
		warnings.push(...result.warnings)
		if (result.frontmatter) {
			errors.push(...checkGateIds(file, result.frontmatter, knownGateIds))
		}
	}
	return {
		status: errors.length === 0 ? 'ok' : 'fail',
		files_checked: files.length,
		errors,
		warnings,
	}
}

/**
 * CLI entrypoint for `apt-tools check <project-dir>`.
 *
 * @param {string} projectDir  accepted for API parity with other commands
 * @param {string[]} extraArgs
 */
export function cmdCheck(projectDir, extraArgs = []) {
	const skillsRoot = resolveSkillsRoot(extraArgs)
	const result = runCheck({ skillsRoot })
	const envelope = {
		command: 'check',
		project_dir: projectDir ? resolve(projectDir) : null,
		skills_root: skillsRoot,
		...result,
	}
	return result.status === 'ok' ? ok(envelope) : exitWith(envelope, 1)
}
