// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
 * Build Skill References
 *
 * Generates an `_index.md` file per skill that points to Zod schema source
 * files inside the consumer's `node_modules/@objectstack/spec/src/`.
 *
 * Skills do NOT bundle copies of the schemas — when a skill is installed
 * into a metadata-driven project (e.g. via skills.sh), `@objectstack/spec`
 * is always present as a dependency. Pointing at the published source files
 * keeps a single source of truth and stays version-aligned automatically.
 *
 * The script:
 * 1. Reads a declarative mapping of { skill → core zod files }
 * 2. Recursively resolves local `import … from` dependencies (so the index
 *    surfaces shared schemas an agent will need to follow)
 * 3. Writes `skills/{name}/references/_index.md` with pointers + one-line
 *    descriptions extracted from each file's leading JSDoc comment
 *
 * Usage: tsx scripts/build-skill-references.ts
 */

import fs from 'fs';
import path from 'path';

// ── Paths ────────────────────────────────────────────────────────────────────

const REPO_ROOT = path.resolve(__dirname, '../../..');
const SPEC_SRC = path.resolve(__dirname, '../src');
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
const SPEC_PKG = '@objectstack/spec';

// ── Skill → Zod file mapping ────────────────────────────────────────────────
// Paths are relative to packages/spec/src/ (category/file.zod.ts)

const SKILL_MAP: Record<string, string[]> = {
  'objectstack-data': [
    'data/field.zod.ts',
    'data/object.zod.ts',
    'data/validation.zod.ts',
    'data/hook.zod.ts',
    'data/datasource.zod.ts',
    'data/dataset.zod.ts',
    'security/permission.zod.ts',
  ],
  'objectstack-query': [
    'data/query.zod.ts',
    'data/filter.zod.ts',
  ],
  'objectstack-ai': [
    'ai/agent.zod.ts',
    'ai/tool.zod.ts',
    'ai/skill.zod.ts',
    'ai/model-registry.zod.ts',
    'ai/conversation.zod.ts',
    'ai/mcp.zod.ts',
    'ai/embedding.zod.ts',
    'ai/usage.zod.ts',
  ],
  'objectstack-api': [
    'api/endpoint.zod.ts',
    'api/auth.zod.ts',
    'api/realtime.zod.ts',
    'api/rest-server.zod.ts',
    'api/graphql.zod.ts',
    'api/websocket.zod.ts',
    'api/errors.zod.ts',
    'api/batch.zod.ts',
    'api/versioning.zod.ts',
  ],
  'objectstack-automation': [
    'automation/flow.zod.ts',
    'automation/trigger-registry.zod.ts',
    'automation/approval.zod.ts',
    'automation/state-machine.zod.ts',
    'automation/execution.zod.ts',
    'automation/webhook.zod.ts',
    'automation/node-executor.zod.ts',
  ],
  'objectstack-ui': [
    'ui/view.zod.ts',
    'ui/app.zod.ts',
    'ui/dashboard.zod.ts',
    'ui/chart.zod.ts',
    'ui/action.zod.ts',
    'ui/page.zod.ts',
    'ui/widget.zod.ts',
    'ui/component.zod.ts',
    'ui/report.zod.ts',
    'ui/theme.zod.ts',
  ],
  'objectstack-platform': [
    // project setup (was objectstack-quickstart)
    'kernel/manifest.zod.ts',
    'data/datasource.zod.ts',
    'data/dataset.zod.ts',
    // plugin development (was objectstack-plugin)
    'kernel/plugin.zod.ts',
    'kernel/context.zod.ts',
    'kernel/service-registry.zod.ts',
    'kernel/plugin-lifecycle-events.zod.ts',
    'kernel/plugin-capability.zod.ts',
    'kernel/plugin-loading.zod.ts',
    'kernel/feature.zod.ts',
    'kernel/metadata-plugin.zod.ts',
  ],
  'objectstack-i18n': [
    'system/translation.zod.ts',
    'ui/i18n.zod.ts',
  ],
};

// ── Import resolver ──────────────────────────────────────────────────────────

function extractLocalImports(filePath: string): string[] {
  const content = fs.readFileSync(filePath, 'utf-8');
  const imports: string[] = [];
  const re = /^import\s+.*\s+from\s+['"](\.[^'"]+)['"]/gm;
  let match: RegExpExecArray | null;
  while ((match = re.exec(content)) !== null) {
    const importSpec = match[1];
    const dir = path.dirname(filePath);
    let resolved = path.resolve(dir, importSpec);
    if (!resolved.endsWith('.ts')) resolved += '.ts';
    if (fs.existsSync(resolved)) {
      imports.push(path.relative(SPEC_SRC, resolved));
    }
  }
  return imports;
}

function resolveAll(entryFiles: string[]): string[] {
  const visited = new Set<string>();
  const queue = [...entryFiles];
  while (queue.length > 0) {
    const rel = queue.shift()!;
    if (visited.has(rel)) continue;
    const abs = path.resolve(SPEC_SRC, rel);
    if (!fs.existsSync(abs)) {
      console.warn(`  ⚠ File not found: ${rel} (skipped)`);
      continue;
    }
    visited.add(rel);
    for (const dep of extractLocalImports(abs)) {
      if (!visited.has(dep)) queue.push(dep);
    }
  }
  return [...visited].sort();
}

// ── JSDoc description extractor ──────────────────────────────────────────────

function extractDescription(filePath: string): string {
  const content = fs.readFileSync(filePath, 'utf-8');
  const jsdocMatch = content.match(/\/\*\*\s*\n([\s\S]*?)\*\//);
  if (jsdocMatch) {
    const lines = jsdocMatch[1]
      .split('\n')
      .map((line) => line.replace(/^\s*\*\s?/, '').trim())
      .filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
    const firstLine = lines[0];
    if (firstLine && firstLine.length > 5) {
      const clean = firstLine.replace(/^#+\s*/, '');
      const sentence = clean.split(/\.\s/)[0];
      return sentence.length > 120 ? sentence.slice(0, 117) + '...' : sentence;
    }
  }
  const exports: string[] = [];
  const re = /export\s+const\s+(\w+Schema|\w+)\s*(?:[:=])/g;
  let m: RegExpExecArray | null;
  while ((m = re.exec(content)) !== null) exports.push(m[1]);
  if (exports.length > 0) return `Exports: ${exports.slice(0, 5).join(', ')}`;
  return '';
}

// ── Index generator ──────────────────────────────────────────────────────────

function pointerPath(rel: string): string {
  return `node_modules/${SPEC_PKG}/src/${rel}`;
}

function generateIndex(skillName: string, coreFiles: string[], allFiles: string[]): string {
  const coreSet = new Set(coreFiles);
  const lines: string[] = [
    `# ${skillName} — Schema References`,
    '',
    '> **Auto-generated** by `packages/spec/scripts/build-skill-references.ts`.',
    `> Do not edit — re-run \`pnpm --filter ${SPEC_PKG} run gen:skill-refs\` to update.`,
    '',
    `Schemas live in the published \`${SPEC_PKG}\` package. Read them directly`,
    'from `node_modules` — there is no local copy in the skill bundle.',
    '',
    '## Core schemas',
    '',
  ];

  for (const f of allFiles.filter((f) => coreSet.has(f))) {
    const desc = extractDescription(path.resolve(SPEC_SRC, f));
    lines.push(`- \`${pointerPath(f)}\`${desc ? ` — ${desc}` : ''}`);
  }

  const deps = allFiles.filter((f) => !coreSet.has(f));
  if (deps.length > 0) {
    lines.push('', '## Transitive dependencies', '');
    for (const f of deps) {
      const desc = extractDescription(path.resolve(SPEC_SRC, f));
      lines.push(`- \`${pointerPath(f)}\`${desc ? ` — ${desc}` : ''}`);
    }
  }

  lines.push(
    '',
    '## How to read these',
    '',
    `1. The schemas are runtime Zod definitions. Use \`Read\` on the absolute`,
    `   path under \`node_modules/${SPEC_PKG}/src/\` to inspect field shapes,`,
    `   \`.describe()\` text, enums, and refinements.`,
    `2. TypeScript types: \`import type { … } from '${SPEC_PKG}'\` (or the`,
    '   matching subpath export).',
    '3. Runtime values: `import { … } from \'' + SPEC_PKG + '\'` — the package',
    '   re-exports every schema and helper.',
    '',
  );

  return lines.join('\n');
}

// ── Cleanup helper ───────────────────────────────────────────────────────────

function cleanReferencesDir(refsDir: string) {
  if (!fs.existsSync(refsDir)) {
    fs.mkdirSync(refsDir, { recursive: true });
    return;
  }
  for (const entry of fs.readdirSync(refsDir)) {
    // Keep hand-written markdown other than _index.md untouched.
    if (entry === '_index.md') {
      fs.rmSync(path.resolve(refsDir, entry));
      continue;
    }
    const entryPath = path.resolve(refsDir, entry);
    const stat = fs.statSync(entryPath);
    if (stat.isDirectory()) {
      fs.rmSync(entryPath, { recursive: true });
    } else if (entry.endsWith('.zod.ts')) {
      fs.rmSync(entryPath);
    }
  }
}

// ── Main ─────────────────────────────────────────────────────────────────────

function main() {
  console.log('🔗 Building skill schema reference indexes...\n');
  let totalSkills = 0;

  for (const [skillName, coreFiles] of Object.entries(SKILL_MAP)) {
    const skillDir = path.resolve(SKILLS_DIR, skillName);
    if (!fs.existsSync(skillDir)) {
      console.warn(`⚠ Skill directory not found: ${skillName}, skipping`);
      continue;
    }

    console.log(`📦 ${skillName}`);
    const allFiles = resolveAll(coreFiles);
    console.log(`   ${coreFiles.length} core + ${allFiles.length - coreFiles.length} deps`);

    const refsDir = path.resolve(skillDir, 'references');
    cleanReferencesDir(refsDir);

    fs.writeFileSync(
      path.resolve(refsDir, '_index.md'),
      generateIndex(skillName, coreFiles, allFiles),
    );
    totalSkills += 1;
  }

  console.log(`\n✅ Done — ${totalSkills} skill index files written`);
}

main();
