/**
 * skill-author/contract.mjs — C4 canonical SKILL.md contract.
 *
 * Single source of truth for what a valid Aperant skill's SKILL.md looks like.
 * Every other module in src/cli/skill-author/ reads these constants and schemas
 * rather than re-encoding the rules, so the contract is pinned in exactly one
 * place.
 *
 * router-0.3.0 (2026-04-22): extended SkillFrontmatterSchema to enforce the
 * fat-frontmatter shape (stage, intent, when_to_use, user_invocable, internal,
 * spawns_agent, agent_name, allowed-tools, argument-hint, gates,
 * default_execution_mode, execution_modes). Mirrors
 * packages/framework/schemas/skill-frontmatter.json one-to-one. Both exist
 * because: the JSON Schema is the ship-ready contract shipped alongside the
 * package (referenced by docs + third-party tooling), while the Zod schema
 * is the runtime enforcement used by the CLI. Drift between the two is
 * covered by a test — see skill-frontmatter-schema.test.ts.
 *
 * Scope: v1 of the `author-skill` pipeline only covered NEW skills authored
 * via `apt-tools author-skill`. Starting in router-0.3.0, every shipping
 * `packages/framework/skills/apt-(slug)/SKILL.md` MUST pass
 * SkillFrontmatterSchema; that cutover is enforced by
 * skill-frontmatter-schema.test.ts.
 */

import { z } from 'zod'

// The frontmatter `name` field is either the bare router entry "apt" OR
// "apt:<kebab-case-slug>". Every downstream skill uses the slug form; the
// router's own SKILL.md is the only "apt" entry.
export const SKILL_NAME_REGEX = /^apt(?::[a-z][a-z0-9-]*)?$/

/**
 * Custom `x-*` intent prefix — for vertical-specific intents that haven't
 * graduated to the core enum yet. See packages/framework/docs/intents.md
 * for the promotion path (3 shipping skills using an intent → PR to core).
 */
export const CUSTOM_INTENT_REGEX = /^x-[a-z][a-z0-9-]*$/

/**
 * Central core-intent taxonomy. Closed set — new intents enter this array
 * only via the promotion path documented in docs/intents.md. See D-06 in
 * .aperant/context/notes/router-llm-first-redesign.md for the decision.
 */
export const CORE_INTENTS = Object.freeze([
	'implement',
	'capture',
	'discuss',
	'investigate',
	'design',
	'ship',
	'configure',
	'debug',
])

/**
 * Lifecycle stages a skill can declare as its primary phase. Closed enum.
 */
export const STAGES = Object.freeze([
	'plan',
	'execute',
	'verify',
	'review',
	'ship',
	'discuss',
	'debug',
	'design',
	'scan',
	'quick',
	'setup',
	'resume',
	'pause',
	'ops',
])

/**
 * Execution modes an autonomy run can be in. Shared with
 * `packages/framework/src/cli/task/execution-mode.mjs` — kept in sync by
 * a structural test (ensures the two lists never drift).
 */
export const EXECUTION_MODES = Object.freeze(['auto', 'step', 'plan-mode', 'plan-only', 'research'])

/**
 * Skill-passthrough policy values (router-0.4 / G25 fix). The `apt/SKILL.md`
 * orchestrator dispatches on this frontmatter field — missing values fail
 * closed so new skills can't silently bypass task registration. See
 * docs/frameworks/spec-gaps.md#g25 for the defect that motivated this.
 */
export const TASK_CONTEXTS = Object.freeze([
	'create-new',
	'require-existing',
	'self-managed',
	'none',
])

/**
 * Optional default track for skills with `task_context: create-new`. The
 * router falls back to a per-slug hardcoded table when this field is omitted;
 * declaring it here lets a skill own its default without a router code change.
 */
export const TRACK_VALUES = Object.freeze(['QUICK', 'STANDARD', 'DEEP', 'DEBUG'])

/**
 * Required XML-style section tags that MUST appear (opening + closing) in the
 * SKILL.md body. Order is not enforced — authors can lay out the sections
 * however they like as long as each tag pair is present.
 */
export const REQUIRED_SECTIONS = Object.freeze([
	'<objective>',
	'<your_environment>',
	'<state_files>',
	'<process>',
])

/**
 * Soft-required tags — missing these emits a warning rather than an error.
 * Encouraged by C1 (rationalization tables) and the autonomy model, but not
 * strictly required since some skills are pure informational loops.
 */
export const SOFT_REQUIRED_SECTIONS = Object.freeze(['<rationalization>', '<autonomy_interaction>'])

function isValidIntent(val) {
	if (typeof val !== 'string') return false
	if (CORE_INTENTS.includes(val)) return true
	return CUSTOM_INTENT_REGEX.test(val)
}

const IntentSchema = z.string().refine(isValidIntent, {
	message: `intent must be one of ${CORE_INTENTS.join('|')} OR match ${CUSTOM_INTENT_REGEX} (custom x- prefixed intent)`,
})

const StageSchema = z.enum([...STAGES])
const ExecutionModeSchema = z.enum([...EXECUTION_MODES])

/**
 * Zod schema for the YAML frontmatter block at the top of every SKILL.md.
 *
 * Strict mode is intentionally NOT used: skills carry vendor-specific keys
 * (triggers, color, apt-skill-version) and future extensions should be
 * additive without breaking the contract. We enforce the fat-frontmatter
 * keys below plus pass-through everything else.
 */
export const SkillFrontmatterSchema = z
	.object({
		name: z.string().regex(SKILL_NAME_REGEX, {
			message: `frontmatter \`name\` must match ${SKILL_NAME_REGEX} (e.g. apt:foo, apt:author-skill)`,
		}),
		description: z.string().min(1, 'frontmatter `description` must be a non-empty string'),
		stage: StageSchema,
		intent: IntentSchema,
		when_to_use: z.string().min(1, 'frontmatter `when_to_use` must be a non-empty sentence'),
		user_invocable: z.boolean(),
		internal: z.boolean(),
		spawns_agent: z.boolean(),
		agent_name: z.union([z.string().min(1), z.null()]),
		'allowed-tools': z.string(),
		'argument-hint': z.string(),
		gates: z.array(z.string()),
		default_execution_mode: ExecutionModeSchema,
		execution_modes: z.array(ExecutionModeSchema).min(1),
		task_context: z.enum([...TASK_CONTEXTS]),
		default_track: z.enum([...TRACK_VALUES]).optional(),
		// Legacy / optional pass-throughs — kept as opt-in so migrations
		// don't immediately break skills that carry them.
		triggers: z.array(z.string()).optional(),
		color: z.string().optional(),
		// ST-13 (AC12): optional `produces:` field enumerates
		// the content-formats a skill emits (e.g. "PRD-shaped spec.md",
		// "vertical-slice subtasks", "Aperant CONTEXT.md term-block").
		// Consumed by `apt-tools modes list` to surface what each skill ships.
		// Additive and backward-compatible — skills without the field default
		// to [] in modes list output.
		produces: z.array(z.string()).optional(),
		// Canonical SKILL.md files carry `apt-skill-version: {{APT_VERSION}}`,
		// which YAML parses as a flow mapping (an object). The install
		// pipeline rewrites this to a plain version string at transform
		// time. Both shapes must validate — that's why we accept
		// `string | object` rather than just `string`.
		'apt-skill-version': z.union([z.string(), z.object({}).passthrough()]).optional(),
		'user-invocable': z.boolean().optional(),
	})
	.passthrough()
	.superRefine((data, ctx) => {
		if (data.spawns_agent === true && (data.agent_name === null || data.agent_name === undefined)) {
			ctx.addIssue({
				code: z.ZodIssueCode.custom,
				message: 'agent_name is required (non-null) when spawns_agent=true',
				path: ['agent_name'],
			})
		}
		if (
			Array.isArray(data.execution_modes) &&
			typeof data.default_execution_mode === 'string' &&
			!data.execution_modes.includes(data.default_execution_mode)
		) {
			ctx.addIssue({
				code: z.ZodIssueCode.custom,
				message: `execution_modes must contain default_execution_mode ("${data.default_execution_mode}")`,
				path: ['execution_modes'],
			})
		}
	})

/**
 * Validate a parsed frontmatter object against the schema.
 *
 * @param {unknown} data — parsed YAML frontmatter
 * @returns {import('zod').SafeParseReturnType<unknown, z.infer<typeof SkillFrontmatterSchema>>}
 */
export function validateFrontmatter(data) {
	return SkillFrontmatterSchema.safeParse(data)
}
