#!/usr/bin/env node
/**
 * check-ds.mjs — 真相源自检闸（扫 design/ 自身，零依赖，Node >= 18）
 *
 * 七道检查（违规 exit 1，逐条打印文件:行）：
 *  ① tokens dark 键 ⊆ light 键（防孤儿 dark token）
 *  ② token 值内 var(--x) 引用链可解析；styles.css @import 的文件存在
 *  ③ design/ 内（guidelines/components/ui_kits/index.html/styles.css）裸色
 *     必须 ∈ tokens 值集，或行内带 `ds-ignore:` 豁免注释
 *  ④ components/**\/*.jsx 导出的组件必须在登记簿 design/index.html 出现
 *  ⑤ icons.js：key 唯一、path 为描边风格（禁 fill="#..."）
 *  ⑥ icon-map.json：web 列 ↔ icons.js key 双向同集；值为 string|null
 *  ⑦ 组件/ui_kits 界面文案禁 emoji（键帽符号 ⌘⇧⌫↵⌥ 不在 emoji 区，天然放行）
 *
 * 用法：node design-pipeline/check-ds.mjs
 */

import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
import { resolve, dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dir = dirname(fileURLToPath(import.meta.url));
const DESIGN = resolve(__dir, '../design');
const errors = [];
const err = (file, line, msg) =>
  errors.push(`${relative(resolve(__dir, '..'), file)}:${line}  ${msg}`);

// ─── 工具 ────────────────────────────────────────────────────────────────────

function walk(dir, exts, out = []) {
  for (const name of readdirSync(dir)) {
    const p = join(dir, name);
    if (statSync(p).isDirectory()) walk(p, exts, out);
    else if (exts.some((e) => name.endsWith(e))) out.push(p);
  }
  return out;
}

function parseBlock(css, selector) {
  const out = {};
  const re = new RegExp(`${selector.replace(/[[\]"=]/g, (m) => '\\' + m)}\\s*\\{([\\s\\S]*?)\\}`, 'g');
  let m;
  while ((m = re.exec(css))) {
    const body = m[1].replace(/\/\*[\s\S]*?\*\//g, '');
    for (const line of body.split(';')) {
      const mm = line.match(/--([\w-]+)\s*:\s*(.+)/);
      if (mm) out[mm[1].trim()] = mm[2].trim();
    }
  }
  return out;
}

// ─── 解析 tokens ─────────────────────────────────────────────────────────────

const tokenFiles = readdirSync(join(DESIGN, 'tokens')).filter((f) => f.endsWith('.css')).sort();
const light = {}, dark = {};
for (const f of tokenFiles) {
  const css = readFileSync(join(DESIGN, 'tokens', f), 'utf8');
  Object.assign(light, parseBlock(css, ':root'));
  Object.assign(dark, parseBlock(css, '[data-theme="dark"]'));
}

// tokens 合法值集（hex 大写 + rgba 去空格），供 ③ 比对
const tokenValues = new Set();
for (const v of [...Object.values(light), ...Object.values(dark)]) {
  for (const hex of v.matchAll(/#[0-9a-fA-F]{3,8}\b/g)) tokenValues.add(hex[0].toUpperCase());
  for (const fn of v.matchAll(/(?:rgba?|hsla?)\([^)]*\)/g)) tokenValues.add(fn[0].replace(/\s+/g, ''));
}

// ① dark ⊆ light
for (const k of Object.keys(dark)) {
  if (!(k in light)) err(join(DESIGN, 'tokens'), 0, `① 孤儿 dark token：--${k} 未在 :root 定义`);
}

// ② var() 链可解析 + @import 存在
for (const [scopeName, scope] of [['light', light], ['dark', dark]]) {
  for (const [k, v] of Object.entries(scope)) {
    for (const ref of v.matchAll(/var\(--([\w-]+)\)/g)) {
      if (!(ref[1] in light) && !(ref[1] in dark))
        err(join(DESIGN, 'tokens'), 0, `② --${k}（${scopeName}）引用了不存在的 var(--${ref[1]})`);
    }
  }
}
{
  const styles = readFileSync(join(DESIGN, 'styles.css'), 'utf8');
  for (const im of styles.matchAll(/@import\s+(?:url\()?['"]([^'"]+)['"]/g)) {
    if (!existsSync(join(DESIGN, im[1]))) err(join(DESIGN, 'styles.css'), 0, `② @import 目标不存在：${im[1]}`);
  }
}

// ③ design/ 裸色须 ∈ tokens 值集（ds-ignore 豁免；tokens/ 与资产 svg 不扫）
const scanFiles = [
  ...walk(join(DESIGN, 'guidelines'), ['.html', '.css']),
  ...walk(join(DESIGN, 'components'), ['.jsx', '.css', '.html']),
  ...walk(join(DESIGN, 'ui_kits'), ['.jsx', '.css', '.html']),
  join(DESIGN, 'index.html'),
  join(DESIGN, 'styles.css'),
];
for (const f of scanFiles) {
  const lines = readFileSync(f, 'utf8').split('\n');
  lines.forEach((line, i) => {
    if (line.includes('ds-ignore:')) return;
    for (const m of line.matchAll(/#[0-9a-fA-F]{3,8}\b|(?:rgba?|hsla?)\([^)]*\)/g)) {
      const val = m[0].startsWith('#') ? m[0].toUpperCase() : m[0].replace(/\s+/g, '');
      // #fff/#FFF 简写归一到 6 位再比对
      const norm = /^#[0-9A-F]{3}$/.test(val)
        ? '#' + [...val.slice(1)].map((c) => c + c).join('')
        : val;
      if (!tokenValues.has(norm))
        err(f, i + 1, `③ 裸色 ${m[0]} 不在 tokens 值集（改用 var(--*) 或加 ds-ignore: 理由）`);
    }
  });
}

// ④ 组件登记：components/**/*.jsx 导出名必须现于 index.html
const registry = readFileSync(join(DESIGN, 'index.html'), 'utf8');
for (const f of walk(join(DESIGN, 'components'), ['.jsx'])) {
  const src = readFileSync(f, 'utf8');
  for (const m of src.matchAll(/export\s+(?:function|const)\s+([A-Z]\w+)/g)) {
    if (!registry.includes(`<b>${m[1]}</b>`) && !registry.includes(`<b>${m[1]} `) && !new RegExp(`<b>[^<]*\\b${m[1]}\\b`).test(registry))
      err(f, 0, `④ 组件 ${m[1]} 未在登记簿 design/index.html 登记`);
  }
}

// ⑤ icons.js：key 唯一 + 描边风格
{
  const f = join(DESIGN, 'icons.js');
  const src = readFileSync(f, 'utf8');
  const keys = [...src.matchAll(/^\s{2}(\w+):/gm)].map((m) => m[1]);
  const seen = new Set();
  for (const k of keys) {
    if (seen.has(k)) err(f, 0, `⑤ icons.js 重复 key：${k}`);
    seen.add(k);
  }
  if (/fill="#/.test(src)) err(f, 0, '⑤ icons.js 含填充色（应为 stroke/currentColor 线条风格）');
  globalThis.__iconKeys = seen;
}

// ⑥ icon-map.json ↔ icons.js 双向同集
{
  const f = join(DESIGN, 'icon-map.json');
  const map = JSON.parse(readFileSync(f, 'utf8'));
  const webUsed = new Set();
  for (const [name, v] of Object.entries(map)) {
    if (name === '$schema') continue;
    for (const col of ['web', 'ios', 'android']) {
      if (!(col in v)) err(f, 0, `⑥ ${name} 缺 ${col} 列（未使用请置 null）`);
      else if (v[col] !== null && typeof v[col] !== 'string') err(f, 0, `⑥ ${name}.${col} 应为 string|null`);
    }
    if (v.web) {
      if (!globalThis.__iconKeys.has(v.web)) err(f, 0, `⑥ ${name}.web=${v.web} 不存在于 icons.js`);
      webUsed.add(v.web);
    }
  }
  for (const k of globalThis.__iconKeys) {
    if (!webUsed.has(k)) err(f, 0, `⑥ icons.js 的 ${k} 未登记进 icon-map.json（web 列）`);
  }
}

// ⑦ 界面文案禁 emoji（components + ui_kits 的 jsx/card.html）
const EMOJI = /[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE0F}]/u;
for (const f of [...walk(join(DESIGN, 'components'), ['.jsx', '.html']), ...walk(join(DESIGN, 'ui_kits'), ['.jsx', '.html'])]) {
  readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
    if (line.includes('ds-ignore:')) return;
    if (EMOJI.test(line)) err(f, i + 1, '⑦ 界面文案/原型含 emoji（品牌规范禁用；确属注释示例加 ds-ignore: 理由）');
  });
}

// ─── 汇总 ────────────────────────────────────────────────────────────────────

if (errors.length) {
  console.error(`❌ check-ds：${errors.length} 处违规\n` + errors.map((e) => '  ' + e).join('\n'));
  process.exit(1);
}
console.log('✅ check-ds：真相源自检 7 道全绿');
