#!/usr/bin/env node
// Vendor a pinned subset of nexu-io/open-design into workspace/skills/open-design/
//
// Usage:
//   node scripts/vendor-open-design.mjs [--source <dir>] [--target <dir>] [--dry-run]
//   OPEN_DESIGN_SOURCE=/path/to/open-design npm run vendor:open-design
//
// Idempotent. Re-running produces no diff on a clean checkout.
// Source defaults to /tmp/open-design (a pre-existing clone) or $OPEN_DESIGN_SOURCE.

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const here = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(here, '..');
const DEFAULT_TARGET = path.join(ROOT, 'workspace', 'skills', 'open-design');
const CONFIG_PATH = path.join(here, 'vendor-open-design.config.json');

function parseArgs(argv) {
  const out = { dryRun: false };
  for (let i = 2; i < argv.length; i++) {
    const arg = argv[i];
    if (arg === '--source') out.source = argv[++i];
    else if (arg === '--target') out.target = argv[++i];
    else if (arg === '--dry-run') out.dryRun = true;
    else if (arg === '--quiet') out.quiet = true;
  }
  return out;
}

export function loadConfig(configPath = CONFIG_PATH) {
  const raw = fs.readFileSync(configPath, 'utf8');
  return JSON.parse(raw);
}

export function isAllowedFile(relPath, config, { textOnlyAlways = false } = {}) {
  const lower = relPath.toLowerCase();
  const base = path.basename(relPath);

  if (config.license_filenames.includes(base) || config.license_filenames.map(n => n.toLowerCase()).includes(base.toLowerCase())) {
    return true;
  }

  const segments = relPath.split(path.sep);

  for (const seg of segments) {
    if (config.exclude_dir_names.includes(seg)) return false;
  }

  // Skip translated docs: README.<locale>.md, CONTRIBUTING.<locale>.md, etc.
  // Keep the canonical English `README.md`, `CONTRIBUTING.md`.
  const translated = /^(README|CONTRIBUTING|QUICKSTART|MAINTAINERS|AGENTS|CLAUDE)\.([a-z]{2}(-[A-Z0-9]+)?)\.md$/i;
  if (translated.test(base)) return false;

  const ext = path.extname(base).toLowerCase();
  if (!ext) return false;

  if (!config.text_extensions.includes(ext)) {
    return false;
  }

  // Always-text-only directories (e.g. design-templates) skip non-text — already
  // filtered above. The flag only matters for size enforcement here:
  if (textOnlyAlways) return true;

  return true;
}

function isExcludedRootDir(name, config) {
  return config.exclude_root_dirs.includes(name);
}

function walk(dir, baseDir = dir, out = []) {
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    if (entry.name === '.git' || entry.name === 'node_modules') continue;
    const abs = path.join(dir, entry.name);
    const rel = path.relative(baseDir, abs);
    if (entry.isDirectory()) {
      walk(abs, baseDir, out);
    } else if (entry.isFile()) {
      out.push({ abs, rel });
    }
  }
  return out;
}

function ensureDirOf(p) {
  fs.mkdirSync(path.dirname(p), { recursive: true });
}

function normalizeVendoredText(contents) {
  // Keep the vendored corpus git-diff-check clean without changing semantics.
  const lines = contents.split('\n').map(line => line.replace(/[ \t]+$/g, ''));
  while (lines.length > 1 && lines.at(-1) === '' && lines.at(-2) === '') lines.pop();
  return lines.join('\n');
}

function copyFile(src, dst) {
  ensureDirOf(dst);
  const contents = fs.readFileSync(src, 'utf8');
  fs.writeFileSync(dst, normalizeVendoredText(contents));
}

function fmtBytes(n) {
  if (n < 1024) return `${n}B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
  return `${(n / 1024 / 1024).toFixed(1)}MB`;
}

function detectSlugTitle(absPath, contents) {
  // Look for YAML-ish front-matter `---\nname: foo\ndescription: ...\n---`.
  // Open Design SKILL.md files use this shape.
  const fmMatch = contents.match(/^---\n([\s\S]*?)\n---/);
  let title = null;
  let slug = null;
  if (fmMatch) {
    const block = fmMatch[1];
    const nameMatch = block.match(/^name:\s*(.+)$/m);
    if (nameMatch) slug = nameMatch[1].trim();
    const descMatch = block.match(/^description:\s*(.+)$/m);
    if (descMatch) title = descMatch[1].trim().replace(/^["']|["']$/g, '').split('\n')[0];
  }
  if (!title) {
    const h1 = contents.match(/^#\s+(.+)$/m);
    if (h1) title = h1[1].trim();
  }
  if (!slug) slug = path.basename(path.dirname(absPath));
  return { slug, title: title || slug };
}

function classifyKind(relPath) {
  const segments = relPath.split(path.sep);
  if (segments[0] === 'craft') return 'craft';
  if (segments[0] === 'skills') return 'skill';
  if (segments[0] === 'design-systems') return 'design-system';
  if (segments[0] === 'design-templates') return 'template';
  if (segments[0] === 'prompt-templates') return 'prompt-template';
  return 'misc';
}

function entryPointForDir(dirAbs) {
  for (const candidate of ['SKILL.md', 'DESIGN.md', 'AGENTS.md', 'README.md']) {
    const p = path.join(dirAbs, candidate);
    if (fs.existsSync(p)) return { name: candidate, path: p };
  }
  return null;
}

function buildIndex(targetDir, sourceDir, config) {
  const entries = [];

  for (const subset of config.subset_dirs) {
    const subsetAbs = path.join(targetDir, subset);
    if (!fs.existsSync(subsetAbs)) continue;

    const kind = classifyKind(subset);

    if (subset === 'craft') {
      // Each craft note is a top-level md file.
      for (const f of fs.readdirSync(subsetAbs)) {
        if (!f.endsWith('.md')) continue;
        const abs = path.join(subsetAbs, f);
        const rel = path.relative(targetDir, abs);
        const contents = fs.readFileSync(abs, 'utf8');
        const { title } = detectSlugTitle(abs, contents);
        const slug = f.replace(/\.md$/, '');
        entries.push({ kind: 'craft', slug, title: title || slug, path: rel, scenario: null });
      }
      continue;
    }

    if (subset === 'prompt-templates') {
      const walked = walk(subsetAbs);
      for (const { abs, rel } of walked) {
        const innerRel = path.relative(targetDir, abs);
        if (!abs.endsWith('.md')) continue;
        const contents = fs.readFileSync(abs, 'utf8');
        const { title } = detectSlugTitle(abs, contents);
        const baseSlug = path.basename(abs, '.md');
        const scenarioSeg = rel.split(path.sep)[0];
        const slug = `${scenarioSeg}/${baseSlug}`;
        entries.push({ kind: 'prompt-template', slug, title: title || baseSlug, path: innerRel, scenario: scenarioSeg });
      }
      continue;
    }

    // skills/, design-systems/, design-templates/ each have child directories with an entry point.
    const children = fs.readdirSync(subsetAbs, { withFileTypes: true });
    for (const child of children) {
      if (!child.isDirectory()) continue;
      if (child.name.startsWith('_')) continue; // _schema
      const dirAbs = path.join(subsetAbs, child.name);
      const entry = entryPointForDir(dirAbs);
      if (!entry) continue;
      const contents = fs.readFileSync(entry.path, 'utf8');
      const detected = detectSlugTitle(entry.path, contents);
      entries.push({
        kind,
        slug: child.name,
        title: detected.title || child.name,
        path: path.relative(targetDir, entry.path),
        scenario: null,
      });
    }
  }

  entries.sort((a, b) => (a.kind + a.slug).localeCompare(b.kind + b.slug));
  return entries;
}

function writeIndex(targetDir, entries, config) {
  const lines = [];
  lines.push('# Open Design — vendored catalog');
  lines.push('');
  lines.push('Machine-readable index of every vendored resource. Generated by');
  lines.push('`scripts/vendor-open-design.mjs`. Do not edit by hand.');
  lines.push('');
  lines.push(`Upstream: ${config.upstream}`);
  lines.push(`Commit:   ${config.commit}`);
  lines.push(`License:  ${config.license}`);
  lines.push('');
  lines.push('## Items');
  lines.push('');
  lines.push('| kind | slug | title | path |');
  lines.push('| --- | --- | --- | --- |');
  for (const e of entries) {
    const safeTitle = String(e.title || '').replace(/\|/g, '\\|').slice(0, 160);
    lines.push(`| ${e.kind} | ${e.slug} | ${safeTitle} | ${e.path} |`);
  }
  lines.push('');
  lines.push('```json');
  lines.push(JSON.stringify({ commit: config.commit, generated_at: new Date().toISOString().slice(0, 10), entries }, null, 2));
  lines.push('```');
  lines.push('');
  fs.writeFileSync(path.join(targetDir, 'INDEX.md'), lines.join('\n'));
}

function findVendoredLicenses(targetDir) {
  const results = [];
  const walked = walk(targetDir);
  for (const { abs, rel } of walked) {
    const base = path.basename(abs);
    if (/^LICENSE(\.md|\.txt)?$/i.test(base) || /^NOTICE(\.md)?$/i.test(base)) {
      // skip our own NOTICE.md
      if (rel === 'NOTICE.md') continue;
      results.push(rel);
    }
  }
  return results.sort();
}

function writeNotice(targetDir, config, licenses) {
  const lines = [];
  lines.push('# NOTICE');
  lines.push('');
  lines.push('This directory contains a vendored, read-only subset of [`nexu-io/open-design`](https://github.com/nexu-io/open-design).');
  lines.push('');
  lines.push(`- Upstream: ${config.upstream}`);
  lines.push(`- Pinned commit: \`${config.commit}\``);
  lines.push(`- License: ${config.license} (see \`LICENSE\` in this directory)`);
  lines.push('');
  lines.push('## Subset');
  lines.push('');
  for (const d of config.subset_dirs) {
    lines.push(`- \`${d}/\``);
  }
  lines.push('');
  lines.push('## Exclusions');
  lines.push('');
  lines.push('Excluded directories: ' + config.exclude_root_dirs.map(d => `\`${d}/\``).join(', ') + '.');
  lines.push('Excluded files: non-English README/CONTRIBUTING/QUICKSTART/MAINTAINERS translations, binary assets (images, video, audio, fonts, archives), Electron-specific code, build output.');
  lines.push('');
  lines.push('## Per-resource LICENSE files');
  lines.push('');
  lines.push('Open Design embeds per-skill / per-template LICENSE files. Each one is preserved verbatim:');
  lines.push('');
  for (const rel of licenses) {
    lines.push(`- \`${rel}\``);
  }
  lines.push('');
  lines.push('## Re-vendoring');
  lines.push('');
  lines.push('Run `npm run vendor:open-design` (idempotent) to refresh this tree against the pinned commit. Update the commit hash in `scripts/vendor-open-design.config.json` first if you want to bump.');
  lines.push('');
  fs.writeFileSync(path.join(targetDir, 'NOTICE.md'), lines.join('\n'));
}

function writeReadme(targetDir, config) {
  const lines = [];
  lines.push('# Open Design — vendored design corpus');
  lines.push('');
  lines.push('Read-only mirror of [nexu-io/open-design](https://github.com/nexu-io/open-design) for use by');
  lines.push('madarX agents (Caretaker, Claude Code agents, Anthropic-engine agents).');
  lines.push('');
  lines.push(`Pinned commit: \`${config.commit}\`. License: ${config.license} (see \`LICENSE\` / \`NOTICE.md\`).`);
  lines.push('');
  lines.push('## Layout');
  lines.push('');
  lines.push('- `craft/` — design craft notes (typography, color, animation, anti-AI-slop).');
  lines.push('- `skills/` — agent-facing skills (each with `SKILL.md`).');
  lines.push('- `design-systems/` — per-brand DESIGN.md + token CSS (e.g. apple, openai, vercel).');
  lines.push('- `design-templates/` — per-template SKILL.md + sample HTML (Pretext-native).');
  lines.push('- `prompt-templates/` — image / video prompt scaffolds.');
  lines.push('');
  lines.push('## Reading from code');
  lines.push('');
  lines.push('Use `lib/open-design.js` (`listOpenDesignSkills`, `getOpenDesignSkill`,');
  lines.push('`listDesignSystems`, `getDesignSystem`, `listCraftNotes`, `listPromptTemplates`).');
  lines.push('');
  lines.push('## Reading from agents');
  lines.push('');
  lines.push('- Claude-Code-backed agents see this tree natively via `--add-dir workspace`.');
  lines.push('- Anthropic-engine agents receive an injected catalog via `lib/agent-open-design-skills.js`.');
  lines.push('');
  lines.push('See `INDEX.md` for the full catalog.');
  lines.push('');
  fs.writeFileSync(path.join(targetDir, 'README.md'), lines.join('\n'));
}

function cleanTarget(targetDir, config) {
  // Remove the previously vendored subset directories so renames/removals upstream
  // are reflected. Keep our own meta files (NOTICE.md/INDEX.md/README.md) — they
  // will be rewritten regardless.
  for (const d of config.subset_dirs) {
    const abs = path.join(targetDir, d);
    if (fs.existsSync(abs)) fs.rmSync(abs, { recursive: true, force: true });
  }
  for (const f of config.root_files) {
    const abs = path.join(targetDir, f);
    if (fs.existsSync(abs)) fs.rmSync(abs, { force: true });
  }
}

export function vendorOpenDesign({ source, target = DEFAULT_TARGET, dryRun = false, quiet = false, config = loadConfig() } = {}) {
  const sourceDir = source || process.env.OPEN_DESIGN_SOURCE || '/tmp/open-design';
  if (!fs.existsSync(sourceDir)) {
    throw new Error(
      `Open Design source not found at ${sourceDir}. ` +
      `Clone https://github.com/nexu-io/open-design (pinned commit ${config.commit}) ` +
      `and pass --source <path>, or set OPEN_DESIGN_SOURCE.`
    );
  }

  if (!dryRun) {
    fs.mkdirSync(target, { recursive: true });
    cleanTarget(target, config);
  }

  let copiedCount = 0;
  let skippedCount = 0;
  let totalBytes = 0;

  // Root-level reference files.
  for (const f of config.root_files) {
    const src = path.join(sourceDir, f);
    if (!fs.existsSync(src)) continue;
    const dst = path.join(target, f);
    const stat = fs.statSync(src);
    if (stat.size > config.max_file_bytes) { skippedCount++; continue; }
    if (!dryRun) copyFile(src, dst);
    copiedCount++;
    totalBytes += stat.size;
  }

  for (const subset of config.subset_dirs) {
    const subsetSrc = path.join(sourceDir, subset);
    if (!fs.existsSync(subsetSrc)) continue;
    const subsetDst = path.join(target, subset);
    const textOnly = config.always_dirs_text_only.includes(subset);

    const files = walk(subsetSrc);
    for (const { abs, rel } of files) {
      const segments = rel.split(path.sep);
      if (segments.some(s => isExcludedRootDir(s, config))) { skippedCount++; continue; }
      if (!isAllowedFile(rel, config, { textOnlyAlways: textOnly })) { skippedCount++; continue; }

      const stat = fs.statSync(abs);
      if (stat.size > config.max_file_bytes) { skippedCount++; continue; }

      const dst = path.join(subsetDst, rel);
      if (!dryRun) copyFile(abs, dst);
      copiedCount++;
      totalBytes += stat.size;
    }
  }

  let entries = [];
  let licenses = [];
  if (!dryRun) {
    entries = buildIndex(target, sourceDir, config);
    writeIndex(target, entries, config);
    licenses = findVendoredLicenses(target);
    writeNotice(target, config, licenses);
    writeReadme(target, config);
  }

  const summary = {
    source: sourceDir,
    target,
    commit: config.commit,
    copied: copiedCount,
    skipped: skippedCount,
    bytes: totalBytes,
    bytes_human: fmtBytes(totalBytes),
    index_entries: entries.length,
    license_files: licenses.length,
    dry_run: dryRun,
  };

  if (!quiet) {
    console.log(
      `[vendor-open-design] ${dryRun ? '(dry-run) ' : ''}` +
      `source=${sourceDir} target=${path.relative(ROOT, target)} ` +
      `copied=${copiedCount} skipped=${skippedCount} bytes=${fmtBytes(totalBytes)} ` +
      `entries=${entries.length} licenses=${licenses.length}`
    );
  }

  return summary;
}

const invokedDirectly = (() => {
  try {
    return fileURLToPath(import.meta.url) === path.resolve(process.argv[1] || '');
  } catch { return false; }
})();

if (invokedDirectly) {
  const args = parseArgs(process.argv);
  try {
    vendorOpenDesign({ source: args.source, target: args.target, dryRun: args.dryRun, quiet: args.quiet });
  } catch (err) {
    console.error(`[vendor-open-design] ${err.message}`);
    process.exit(1);
  }
}
