/**
 * MCP Server Generator (lean)
 *
 * Generates a Model Context Protocol server fully from the SpecVerse spec.
 *
 * Design: the MCP server is a thin wrapper around the specverse CLI. Every
 * CLI command declared in the spec becomes an invocable MCP tool; the live
 * JSON schema + user guide from @specverse/entities are served as MCP
 * resources. No static framework to maintain — everything is emitted from
 * the spec so a new CLI command auto-appears as a new MCP tool on the next
 * realize.
 */

import type { TemplateContext } from '@specverse/types';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { createRequire } from 'module';
import { dirname, join } from 'path';

const require = createRequire(import.meta.url);

export default function generateMCPServer(context: TemplateContext): string {
  const { spec, outputDir } = context;

  const mcpDir = join(outputDir || '.', 'tools', 'specverse-mcp');
  const srcDir = join(mcpDir, 'src');
  if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });

  const distribution = extractMCPDistribution(spec);
  // Prefer @specverse/self's real version over the spec's stale component
  // version label. Falls back to the spec + default.
  const version =
    distribution?.version ||
    resolveSelfVersion() ||
    spec?.metadata?.version ||
    spec?.version ||
    '5.1.0';
  const description = distribution?.description
    || 'SpecVerse MCP server — exposes the specverse CLI as MCP tools and the live spec schema + docs as MCP resources.';
  const displayName = distribution?.displayName || 'SpecVerse MCP';

  const cliCommands = extractCLITools(spec);

  writeFileSync(join(mcpDir, 'package.json'), generatePackageJson(version, description));
  writeFileSync(join(mcpDir, 'tsconfig.json'), generateTsconfig());
  writeFileSync(join(srcDir, 'server.ts'), generateServer(displayName, version));
  writeFileSync(join(srcDir, 'cli-runner.ts'), generateCliRunner());
  writeFileSync(join(srcDir, 'resources.ts'), generateResources(cliCommands));
  writeFileSync(join(srcDir, 'tools.ts'), generateTools(cliCommands));
  writeFileSync(join(srcDir, 'prompts.ts'), generatePrompts());

  return `MCP server generated in: ${mcpDir}\n  ${cliCommands.length} tools, 5 resources, 6 prompts (derived from @specverse/assets/prompts)`;
}

/**
 * Read the running @specverse/self install's version from its package.json so
 * the MCP server reports a real version instead of a stale component label.
 * Returns null if @specverse/self isn't resolvable (e.g. during in-repo tests).
 */
function resolveSelfVersion(): string | null {
  try {
    const pkgPath = require.resolve('@specverse/self/package.json');
    const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
    return pkg.version || null;
  } catch {
    return null;
  }
}

// ─── spec extraction ───────────────────────────────────────────────────────

/**
 * Flatten the spec's CLI commands (components[*].commands[*].subcommands[*])
 * into one entry per invocable leaf. Each becomes an MCP tool.
 *
 * - `validate <file>` → name `validate`, cliArgs `['validate']`
 * - `gen diagrams <file>` → name `gen_diagrams`, cliArgs `['gen', 'diagrams']`
 *
 * Arguments/flags declared in the spec become the tool's JSON-Schema input.
 */
function extractCLITools(spec: any): CLITool[] {
  const out: CLITool[] = [];
  const components = spec?.components || {};
  const componentList = Array.isArray(components)
    ? components
    : Object.entries(components).map(([name, data]) => ({ name, ...(data as any) }));

  for (const comp of componentList) {
    const cliCommands = (comp as any)?.commands;
    if (!cliCommands) continue;

    for (const [, rootDef] of Object.entries(cliCommands as Record<string, any>)) {
      const subcommands = (rootDef as any)?.subcommands || {};
      for (const [subName, subDef] of Object.entries(subcommands as Record<string, any>)) {
        const sub = subDef as any;
        const nestedSubs = sub?.subcommands;
        if (nestedSubs && Object.keys(nestedSubs).length > 0) {
          for (const [nestedName, nestedDef] of Object.entries(nestedSubs as Record<string, any>)) {
            out.push(buildCLITool([subName, nestedName], nestedDef as any));
          }
        } else {
          out.push(buildCLITool([subName], sub));
        }
      }
    }
  }
  return out;
}

interface CLITool {
  name: string;                // MCP tool name (underscored)
  description: string;
  cliArgs: string[];           // argv pieces before user args (["gen","diagrams"])
  positional: string[];        // input-schema keys to emit as positional argv, in order
  inputSchema: Record<string, any>;
}

function buildCLITool(cliArgs: string[], def: any): CLITool {
  const name = cliArgs.join('_');
  const description = def?.description || `Run specverse ${cliArgs.join(' ')}`;
  const properties: Record<string, any> = {};
  const required: string[] = [];

  const args = def?.arguments || {};
  // Collect positionals in declaration order, respecting an optional
  // `position` field (rank ascending; default 0). Named flags that
  // happen to share a key with a positional are impossible — the spec
  // forbids it.
  const positionalEntries: Array<[string, any]> = Object.entries(args as Record<string, any>)
    .filter(([, a]) => (a as any).positional)
    .sort(([, a], [, b]) => ((a as any).position || 0) - ((b as any).position || 0));
  const positional: string[] = positionalEntries.map(([k]) => k);

  for (const [argName, arg] of Object.entries(args as Record<string, any>)) {
    properties[argName] = {
      type: mapArgTypeToJsonSchema((arg as any).type),
      description: (arg as any).description || `${argName} argument`,
    };
    if ((arg as any).required) required.push(argName);
  }

  const flags = def?.flags || {};
  for (const [flagName, flag] of Object.entries(flags as Record<string, any>)) {
    const propName = flagName.replace(/^--/, '').replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
    properties[propName] = {
      type: mapArgTypeToJsonSchema((flag as any).type),
      description: (flag as any).description || `${flagName} flag`,
    };
    if ((flag as any).default !== undefined) {
      properties[propName].default = (flag as any).default;
    }
  }

  return {
    name,
    description,
    cliArgs,
    positional,
    inputSchema: {
      type: 'object',
      properties,
      ...(required.length > 0 ? { required } : {}),
    },
  };
}

function mapArgTypeToJsonSchema(type?: string): string {
  if (!type) return 'string';
  const t = type.toLowerCase();
  if (t === 'boolean') return 'boolean';
  if (t === 'integer' || t === 'number') return 'number';
  return 'string';
}

function extractMCPDistribution(spec: any): any | null {
  const all: any[] = [];
  if (spec?.distributions) {
    all.push(...(Array.isArray(spec.distributions)
      ? spec.distributions
      : Object.entries(spec.distributions).map(([name, data]) => ({ name, ...(data as any) }))));
  }
  const components = spec?.components || {};
  const componentList = Array.isArray(components) ? components : Object.values(components);
  for (const comp of componentList) {
    const distributions = (comp as any)?.distributions;
    if (!distributions) continue;
    all.push(...(Array.isArray(distributions)
      ? distributions
      : Object.entries(distributions).map(([name, data]) => ({ name, ...(data as any) }))));
  }
  for (const dist of all) if ((dist as any).type === 'mcp') return dist;
  return null;
}

// ─── emitted files ─────────────────────────────────────────────────────────

function generatePackageJson(version: string, description: string): string {
  return JSON.stringify({
    name: '@specverse/mcp',
    version,
    description,
    type: 'module',
    main: 'dist/server.js',
    bin: { 'specverse-mcp': 'dist/server.js' },
    scripts: {
      build: 'tsc',
      start: 'node dist/server.js',
      clean: 'rm -rf dist',
    },
    dependencies: {
      '@modelcontextprotocol/sdk': '^1.17.4',
      '@specverse/assets': '^1.0.0',
      '@specverse/engines': '^6.0.3',
      '@specverse/entities': '^5.1.0',
      '@specverse/self': '^5.4.0',
      'js-yaml': '^4.1.0',
    },
    devDependencies: {
      '@types/js-yaml': '^4.0.9',
      '@types/node': '^20.19.11',
      typescript: '^5.9.2',
    },
    files: ['dist', 'README.md'],
    engines: { node: '>=18.0.0' },
    publishConfig: { access: 'public' },
    license: 'MIT',
  }, null, 2) + '\n';
}

function generateTsconfig(): string {
  return JSON.stringify({
    compilerOptions: {
      target: 'ES2022',
      module: 'ESNext',
      moduleResolution: 'bundler',
      esModuleInterop: true,
      strict: true,
      skipLibCheck: true,
      declaration: true,
      outDir: './dist',
      rootDir: './src',
      resolveJsonModule: true,
      // Under moduleResolution: 'bundler' tsc's auto-inclusion of
      // @types/* packages from node_modules/@types is environment-
      // dependent — Node 20 on CI doesn't pick up @types/node while
      // Node 24 locally does. Listing it explicitly forces consistent
      // resolution everywhere.
      types: ['node'],
    },
    include: ['src/**/*'],
    exclude: ['node_modules', 'dist'],
  }, null, 2) + '\n';
}

function generateServer(displayName: string, version: string): string {
  const instructions = SERVER_INSTRUCTIONS;
  return `#!/usr/bin/env node
/**
 * SpecVerse MCP Server — stdio transport.
 *
 * Wires the MCP protocol to the generated tool registry + live resources
 * + canonical workflow prompts. Everything is derived from the spec
 * (tools from CLI commands, resources from @specverse/entities + @specverse/self,
 * prompts from @specverse/engines/assets/prompts/core/standard/default).
 */
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { TOOLS, callTool } from './tools.js';
import { RESOURCES, readResource } from './resources.js';
import { PROMPTS, getPrompt } from './prompts.js';

const INSTRUCTIONS = ${JSON.stringify(instructions)};

const server = new Server(
  { name: ${JSON.stringify(displayName)}, version: ${JSON.stringify(version)} },
  {
    capabilities: { tools: {}, resources: {}, prompts: {} },
    instructions: INSTRUCTIONS,
  },
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: TOOLS.map(t => ({
    name: t.name,
    description: t.description,
    inputSchema: t.inputSchema,
  })),
}));

server.setRequestHandler(CallToolRequestSchema, async (req: { params: { name: string; arguments?: Record<string, unknown> } }) => {
  const { name, arguments: args } = req.params;
  return callTool(name, args ?? {});
});

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: RESOURCES.map(r => ({
    uri: r.uri,
    name: r.name,
    description: r.description,
    mimeType: r.mimeType,
  })),
}));

server.setRequestHandler(ReadResourceRequestSchema, async (req: { params: { uri: string } }) => {
  return readResource(req.params.uri);
});

server.setRequestHandler(ListPromptsRequestSchema, async () => ({
  prompts: PROMPTS.map(p => ({
    name: p.name,
    description: p.description,
    arguments: p.arguments,
  })),
}));

server.setRequestHandler(GetPromptRequestSchema, async (req: { params: { name: string; arguments?: Record<string, string> } }) => {
  const { name, arguments: args } = req.params;
  return getPrompt(name, args ?? {});
});

const transport = new StdioServerTransport();
await server.connect(transport);
`;
}

const SERVER_INSTRUCTIONS = [
  'SpecVerse is a declarative specification language. You describe WHAT a system does in a .specly file, and the engines generate HOW — backend (Fastify/Prisma), frontend (React/Tailwind), CLI, tools, diagrams.',
  '',
  'Before using tools, read these resources to ground yourself in the language:',
  '- specverse://guide — canonical user guide (spec language, CLI reference, convention patterns)',
  '- specverse://ai-guidance — curated hints for generating valid specs',
  '- specverse://minimal-example — one minimal .specly showing every feature in ~100 lines',
  '- specverse://schema — the JSON Schema (draft 2020-12) for .specly validation',
  '- specverse://cli-reference — every spv CLI subcommand with flags + arguments',
  '',
  'Common high-leverage workflows are exposed as MCP prompts (preferred entry point):',
  '- create: natural-language requirements → minimal .specly',
  '- analyse: existing codebase → .specly that captures what is implemented',
  '- materialise: .specly → production code',
  '- realize: generate deployment configs',
  '- behavior: generate AI behavior function stubs',
  '- app-demo: interactive spec creation/modification for the runtime interpreter',
  '',
  'Tools map 1:1 to the spv CLI. Key distinctions:',
  '- validate: check a .specly file parses + matches the schema',
  '- validate-manifest: check an implementation manifest resolves cleanly',
  '- validate-bundle: check an entity bundle (schema + examples + tests + docs + behaviour facets)',
  '- infer: expand a minimal .specly to full architecture (controllers / services / events / views)',
  '- realize: turn inferred spec + manifest into generated code (Fastify / Prisma / React / CLI / tools)',
  '- init: scaffold a new project from a template',
].join('\n');


function generateCliRunner(): string {
  return `/**
 * Thin specverse CLI runner.
 *
 * Each MCP tool maps to a specverse CLI command + a set of user-supplied
 * arguments. The tool registry tells us which argument keys are positional
 * (declared in the spec as \`positional: true\`) — those get emitted in
 * order before any flags, matching the CLI's expected argv shape. The
 * specverse binary must be on PATH (the AI client that launches the
 * server inherits env from its parent).
 */
import { spawn } from 'child_process';

export interface CliResult {
  stdout: string;
  stderr: string;
  code: number | null;
}

export function runCli(
  cliArgs: string[],
  positional: string[],
  userArgs: Record<string, any>,
  cwd?: string,
): Promise<CliResult> {
  const argv = [...cliArgs];

  // 1. Positional args first, in spec-declared order. Undefined values
  //    are skipped — commander will surface missing-required errors.
  for (const key of positional) {
    const v = userArgs[key];
    if (v === undefined || v === null) continue;
    argv.push(String(v));
  }

  // 2. Remaining keys become --flag or --flag value.
  const positionalSet = new Set(positional);
  for (const [k, v] of Object.entries(userArgs)) {
    if (positionalSet.has(k)) continue;
    if (v === undefined || v === null) continue;
    if (typeof v === 'boolean') {
      if (v) argv.push(\`--\${k}\`);
    } else {
      argv.push(\`--\${k}\`, String(v));
    }
  }

  return new Promise<CliResult>(resolve => {
    const child = spawn('specverse', argv, { cwd: cwd || process.cwd(), env: process.env });
    let stdout = '';
    let stderr = '';
    child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
    child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
    child.on('close', (code: number | null) => resolve({ stdout, stderr, code }));
    child.on('error', (err: Error) => resolve({ stdout, stderr: stderr + '\\n' + err.message, code: -1 }));
  });
}
`;
}

function generateTools(tools: CLITool[]): string {
  return `/**
 * Tool registry — one MCP tool per specverse CLI subcommand.
 * Generated from the spec at realize time.
 */
import { runCli } from './cli-runner.js';

export interface Tool {
  name: string;
  description: string;
  cliArgs: string[];
  positional: string[];
  inputSchema: Record<string, any>;
}

export const TOOLS: Tool[] = ${JSON.stringify(tools, null, 2)};

const BY_NAME = new Map<string, Tool>(TOOLS.map(t => [t.name, t]));

export async function callTool(name: string, args: Record<string, any>) {
  const tool = BY_NAME.get(name);
  if (!tool) {
    return {
      isError: true,
      content: [{ type: 'text', text: \`Unknown tool: \${name}\` }],
    };
  }
  const { stdout, stderr, code } = await runCli(tool.cliArgs, tool.positional, args);
  if (code !== 0) {
    return {
      isError: true,
      content: [{ type: 'text', text: \`specverse \${tool.cliArgs.join(' ')} exited \${code}\\n\\n\${stderr || stdout}\` }],
    };
  }
  return {
    content: [{ type: 'text', text: stdout || '(no output)' }],
  };
}
`;
}

function generateResources(tools: CLITool[]): string {
  const cliReference = buildCliReferenceMarkdown(tools);
  return `/**
 * Resource registry — exposes the canonical SpecVerse reference material as
 * MCP resources:
 *   - schema           (JSON Schema draft 2020-12, from @specverse/entities)
 *   - ai-guidance      (curated LLM hints, from @specverse/entities)
 *   - minimal-example  (one .specly covering all features, from @specverse/entities)
 *   - guide            (user guide, from @specverse/self)
 *   - cli-reference    (derived from the spec at realize time, embedded below)
 */
import { readFileSync } from 'fs';
import { createRequire } from 'module';
import { dirname, join } from 'path';

const require = createRequire(import.meta.url);

export interface Resource {
  uri: string;
  name: string;
  description: string;
  mimeType: string;
  resolve: () => { text: string; mimeType: string };
}

function resolveEntitiesFile(relative: string): string {
  const pkg = require.resolve('@specverse/entities/package.json');
  return join(dirname(pkg), relative);
}

function resolveSelfFile(relative: string): string {
  const pkg = require.resolve('@specverse/self/package.json');
  return join(dirname(pkg), relative);
}

const CLI_REFERENCE_MARKDOWN = ${JSON.stringify(cliReference)};

export const RESOURCES: Resource[] = [
  {
    uri: 'specverse://schema',
    name: 'SpecVerse JSON Schema',
    description: 'JSON Schema (draft 2020-12) for validating .specly files — composed from entity-module fragments.',
    mimeType: 'application/json',
    resolve: () => ({
      text: readFileSync(resolveEntitiesFile('schema/SPECVERSE-SCHEMA.json'), 'utf8'),
      mimeType: 'application/json',
    }),
  },
  {
    uri: 'specverse://ai-guidance',
    name: 'SpecVerse AI Guidance Schema',
    description: 'YAML schema annotated with examples and LLM guidance for generating valid specs. Read this before authoring or mutating .specly files.',
    mimeType: 'application/x-yaml',
    resolve: () => ({
      text: readFileSync(resolveEntitiesFile('schema/SPECVERSE-SCHEMA-AI.yaml'), 'utf8'),
      mimeType: 'application/x-yaml',
    }),
  },
  {
    uri: 'specverse://minimal-example',
    name: 'SpecVerse Minimal Example',
    description: 'One complete minimal .specly demonstrating every feature (~100 lines) — component, models, relationships, lifecycle, CURVED controller, service, view, event, manifest, deployment.',
    mimeType: 'text/x-yaml',
    resolve: () => ({
      text: readFileSync(resolveEntitiesFile('schema/MINIMAL-SYNTAX-REFERENCE.specly'), 'utf8'),
      mimeType: 'text/x-yaml',
    }),
  },
  {
    uri: 'specverse://guide',
    name: 'SpecVerse Complete Guide',
    description: 'The canonical user guide — spec language, convention patterns, CLI reference.',
    mimeType: 'text/markdown',
    resolve: () => ({
      text: readFileSync(resolveSelfFile('docs/guides/SPECVERSE-COMPLETE-GUIDE.md'), 'utf8'),
      mimeType: 'text/markdown',
    }),
  },
  {
    uri: 'specverse://cli-reference',
    name: 'SpecVerse CLI Reference',
    description: 'Every spv CLI subcommand with its arguments, flags, and exit codes. Derived from the spec at realize time.',
    mimeType: 'text/markdown',
    resolve: () => ({
      text: CLI_REFERENCE_MARKDOWN,
      mimeType: 'text/markdown',
    }),
  },
];

const BY_URI = new Map<string, Resource>(RESOURCES.map(r => [r.uri, r]));

export async function readResource(uri: string) {
  const resource = BY_URI.get(uri);
  if (!resource) {
    throw new Error(\`Unknown resource URI: \${uri}\`);
  }
  const { text, mimeType } = resource.resolve();
  return {
    contents: [{ uri, mimeType, text }],
  };
}
`;
}

/**
 * Build the cli-reference.md content from the parsed CLI tool list.
 * Embedded as a string constant in the generated resources.ts so the MCP
 * server doesn't need filesystem access for it at runtime.
 */
function buildCliReferenceMarkdown(tools: CLITool[]): string {
  const lines: string[] = [];
  lines.push('# SpecVerse CLI Reference');
  lines.push('');
  lines.push('Generated from the self-spec at realize time. Every entry here matches an MCP tool exposed by this server (same name, same input schema).');
  lines.push('');
  for (const tool of tools) {
    const cliCmd = tool.cliArgs.join(' ');
    lines.push(`## \`spv ${cliCmd}\``);
    lines.push('');
    lines.push(tool.description);
    lines.push('');
    const props = (tool.inputSchema as any)?.properties || {};
    const required = new Set(((tool.inputSchema as any)?.required || []) as string[]);
    const positional = new Set(tool.positional);
    const argEntries = Object.entries(props) as [string, any][];
    if (argEntries.length > 0) {
      lines.push('| Name | Kind | Required | Type | Description |');
      lines.push('|---|---|---|---|---|');
      for (const [name, schema] of argEntries) {
        const kind = positional.has(name) ? 'positional' : 'flag';
        const req = required.has(name) ? 'yes' : 'no';
        const type = (schema as any)?.type || 'string';
        const desc = ((schema as any)?.description || '').replace(/\|/g, '\\|');
        lines.push(`| \`${name}\` | ${kind} | ${req} | ${type} | ${desc} |`);
      }
      lines.push('');
    }
  }
  return lines.join('\n');
}

/**
 * Emit prompts.ts — loads the canonical workflow prompts from
 * @specverse/assets/prompts/core/standard/default/ at module-init time.
 * Each YAML prompt becomes one MCP prompt; the declared
 * {{template variables}} surface as MCP prompt arguments the client fills.
 * Schema paths ({{aiSchemaPath}}, {{referenceSchemaPath}},
 * {{referenceExamplePath}}) are auto-filled with package-relative paths so
 * the caller doesn't have to know where they live on disk.
 */
function generatePrompts(): string {
  return `/**
 * Prompt registry — reads the canonical workflow prompts from
 * @specverse/assets/prompts/core/standard/default/ at startup and
 * serves them as MCP prompts. Each YAML file (create, analyse, materialise,
 * realize, behavior, app-demo) becomes one invocable prompt.
 */
import { readdirSync, readFileSync } from 'fs';
import { createRequire } from 'module';
import { dirname, join, basename } from 'path';
import { load as yamlLoad } from 'js-yaml';

const require = createRequire(import.meta.url);

export interface PromptArgument {
  name: string;
  description?: string;
  required: boolean;
}

export interface Prompt {
  name: string;
  description: string;
  arguments: PromptArgument[];
  source: any;  // parsed YAML — kept so getPrompt can re-render
}

function resolveEntitiesFile(relative: string): string {
  const pkg = require.resolve('@specverse/entities/package.json');
  return join(dirname(pkg), relative);
}

function resolveAssetsDir(relative: string): string {
  const pkg = require.resolve('@specverse/assets/package.json');
  return join(dirname(pkg), relative);
}

// Auto-filled template vars — paths into @specverse/entities/schema/
const SCHEMA_VAR_PATHS: Record<string, string> = {
  aiSchemaPath: resolveEntitiesFile('schema/SPECVERSE-SCHEMA-AI.yaml'),
  referenceSchemaPath: resolveEntitiesFile('schema/MINIMAL-SYNTAX-REFERENCE.specly'),
  referenceExamplePath: resolveEntitiesFile('schema/MINIMAL-SYNTAX-REFERENCE.specly'),
};

// User-facing vars the server does NOT auto-fill — surfaced as MCP prompt args.
const USER_FACING = new Set<string>([
  ...Object.keys(SCHEMA_VAR_PATHS),
]);

function loadPrompts(): Prompt[] {
  const promptsDir = resolveAssetsDir('prompts/core/standard/default');
  const entries = readdirSync(promptsDir).filter(f => f.endsWith('.prompt.yaml'));
  const prompts: Prompt[] = [];
  for (const entry of entries) {
    const full = join(promptsDir, entry);
    try {
      const parsed = yamlLoad(readFileSync(full, 'utf8')) as any;
      if (!parsed?.name) continue;
      const args = deriveArguments(parsed);
      prompts.push({
        name: parsed.name,
        description: parsed.description || \`SpecVerse \${parsed.name} workflow\`,
        arguments: args,
        source: parsed,
      });
    } catch (err) {
      // One bad prompt shouldn't kill the server; skip it.
      console.error(\`[prompts] skipping \${basename(entry)}: \${(err as Error).message}\`);
    }
  }
  return prompts;
}

/**
 * Every SpecVerse prompt YAML declares user.variables[] in the canonical shape
 * { name, type?, required?, description?, default? }. We surface the declared
 * list verbatim, filtering out auto-filled schema-path vars.
 */
function deriveArguments(parsed: any): PromptArgument[] {
  const declared = parsed?.user?.variables;
  if (!Array.isArray(declared)) return [];
  return declared
    .filter((v: any) => v && typeof v === 'object' && typeof v.name === 'string')
    .filter((v: any) => !USER_FACING.has(v.name))
    .map((v: any) => ({
      name: v.name,
      description: v.description || inferVarDescription(v.name, parsed),
      required: v.required !== false,
    }));
}

function inferVarDescription(varName: string, parsed: any): string {
  // Friendly defaults for the known common vars; fall back to a humanised
  // version of the variable name.
  const defaults: Record<string, string> = {
    requirements: 'Natural-language requirements to extract a specification from',
    scale: 'Project scale — personal, business, or enterprise',
    preferredTech: 'Preferred technology stack hint (e.g. "nextjs", "nestjs", "fastify+prisma", "auto")',
    implementationPath: 'Path to the existing codebase to reverse-engineer into a .specly',
    specPath: 'Path to an existing .specly file',
    manifestPath: 'Path to an implementation manifest (manifests/implementation.yaml)',
    outputDir: 'Output directory for generated artifacts',
    modelName: 'Name of the model the behavior is attached to',
    behaviorName: 'Name of the behavior method on the model',
  };
  if (defaults[varName]) return defaults[varName];
  return varName.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase());
}

function substitute(text: string, values: Record<string, string>): string {
  return text.replace(/\\{\\{\\s*([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\}\\}/g, (match, name) => {
    if (name in values) return values[name];
    if (name in SCHEMA_VAR_PATHS) return SCHEMA_VAR_PATHS[name];
    return match; // unknown var — leave the placeholder so it's visible
  });
}

function renderMessages(prompt: Prompt, args: Record<string, string>): { role: 'user'; content: { type: 'text'; text: string } }[] {
  const src = prompt.source;
  const sections: string[] = [];
  if (src?.system?.role) sections.push(\`[System role]\\n\${substitute(src.system.role, args).trim()}\`);
  if (src?.system?.context) sections.push(\`[Context]\\n\${substitute(src.system.context, args).trim()}\`);
  if (src?.user?.template) sections.push(substitute(src.user.template, args).trim());
  const text = sections.filter(Boolean).join('\\n\\n');
  return [{ role: 'user', content: { type: 'text', text } }];
}

export const PROMPTS: Prompt[] = loadPrompts();

const BY_NAME = new Map<string, Prompt>(PROMPTS.map(p => [p.name, p]));

export async function getPrompt(name: string, args: Record<string, string>) {
  const prompt = BY_NAME.get(name);
  if (!prompt) {
    throw new Error(\`Unknown prompt: \${name}\`);
  }
  return {
    description: prompt.description,
    messages: renderMessages(prompt, args),
  };
}
`;
}
