/**
 * 樣式把關：顏色與字級只能來自 tokens.css。
 *
 * 會擋下的事：
 *   1. css 寫死 font-size 數值（em 例外：那是刻意的相對縮放）
 *   2. tsx 的 inline style 寫死 fontSize 或色碼
 *   3. css 出現任何 css 檔都沒定義的 --hb-*（--hb-warn 這種拼錯永遠吃 fallback，很難發現）
 *   4. css 出現寫死的顏色
 *   5. 把兩個有彩度的 token 互相 color-mix（藍配橘是補色，混出來一定是濁褐）
 *
 * 第 4 條放過兩種情形：純黑純白（只用於陰影與高光的 color-mix，本身不是主題色），
 * 以及 global.css 主題預覽色塊那 12 個值 —— 那裡必須同時顯示淺色與深色，
 * 不能吃會隨主題切換的 var()。
 */
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";

const STYLE_DIR = "src/styles";
const NEUTRAL = /^#(fff|000|ffffff|000000)$/i;
/**
 * 有彩度的 token。這些彼此不能混：主色在 H≈187、暖色在 H≈32，是補色關係，
 * 任何比例混出來都是濁褐。要調淡就跟 surface／line／transparent 混，要同一顏色
 * 的深淺兩階就用 X 與 X-deep。
 */
const CHROMATIC =
  /^--hb-(brand|brand-hover|brand-deep|brand-dim|magic|accent-warm|warning|warning-deep|danger|danger-deep|success|gold|gold-bright)$/;
const MIX = /color-mix\(in srgb,\s*var\((--hb-[a-z0-9-]+)\)[^,]*,\s*var\((--hb-[a-z0-9-]+)\)\s*\)/g;
/** 主題預覽色塊：見 global.css 的 .hb-theme-picker__swatch--* */
const HEX_BUDGET = { "global.css": 12, "layout.css": 0, "ui.css": 0 };

const errors = [];

function walk(dir) {
  const out = [];
  for (const name of readdirSync(dir)) {
    const path = join(dir, name);
    if (statSync(path).isDirectory()) out.push(...walk(path));
    else out.push(path);
  }
  return out;
}

const cssFiles = readdirSync(STYLE_DIR).filter((f) => f.endsWith(".css"));
// 除了 tokens.css 的全域 token，元件內宣告的區域變數（如 --hb-top-ctrl）也算已定義。
const defined = new Set(
  cssFiles.flatMap((f) => [
    ...readFileSync(join(STYLE_DIR, f), "utf8").matchAll(/^\s*(--hb-[a-z0-9-]+)\s*:/gm),
  ].map((m) => m[1])),
);

for (const file of readdirSync(STYLE_DIR)) {
  if (!file.endsWith(".css") || file === "tokens.css") continue;
  const path = join(STYLE_DIR, file);
  const text = readFileSync(path, "utf8");

  text.split("\n").forEach((line, i) => {
    const at = `${path}:${i + 1}`;
    if (/font-size:\s*[0-9.]+(rem|px|pt)/.test(line)) {
      errors.push(`${at} 寫死 font-size，請改用 var(--hb-text-*)：${line.trim()}`);
    }
    for (const [, name] of line.matchAll(/var\((--hb-[a-z0-9-]+)/g)) {
      if (!defined.has(name)) {
        errors.push(`${at} 用了 tokens.css 沒有的 ${name}（拼錯？）`);
      }
    }
    for (const [, a, b] of line.matchAll(MIX)) {
      if (CHROMATIC.test(a) && CHROMATIC.test(b)) {
        errors.push(
          `${at} 混了兩個有彩度的顏色（${a} + ${b}），會變濁。` +
            `要調淡請跟 surface／line／transparent 混，要深一階請用 -deep。`,
        );
      }
    }
  });

  const hex = (text.match(/#[0-9a-fA-F]{3,8}\b/g) ?? []).filter((h) => !NEUTRAL.test(h));
  const budget = HEX_BUDGET[file] ?? 0;
  if (hex.length > budget) {
    errors.push(
      `${path} 寫死顏色 ${hex.length} 處（上限 ${budget}）：${[...new Set(hex)].join(" ")}。` +
        `顏色請走 tokens.css，否則深淺模式會有一半畫面不跟著切。`,
    );
  }
}

for (const path of walk("src")) {
  if (!path.endsWith(".tsx")) continue;
  const text = readFileSync(path, "utf8");
  text.split("\n").forEach((line, i) => {
    const at = `${path}:${i + 1}`;
    if (/fontSize:\s*"[0-9.]/.test(line)) {
      errors.push(`${at} inline fontSize 寫死，請改用 var(--hb-text-*)`);
    }
    if (/(color|background|borderColor):\s*"#[0-9a-fA-F]{3,8}"/.test(line)) {
      errors.push(`${at} inline 色碼寫死，請改用 var(--hb-*)`);
    }
  });
}

if (errors.length) {
  console.error(`樣式把關失敗（${errors.length}）：`);
  for (const e of errors) console.error("  " + e);
  process.exit(1);
}
console.log("樣式把關通過：顏色與字級都來自 tokens.css");
