/**
 * 「靜態租戶字串不得用於資料面過濾」— 機械閘（Arcrun#108）。
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * 為什麼要有這道閘
 * ─────────────────────────────────────────────────────────────────────────────
 * 同一句話已經寫錯兩次：
 *   #105  `ownerNamespace(env) = env.MCP_OWNER_NAMESPACE || "leo"`
 *   #108  `portalTenant(env)   = env.CONSOLE_TENANT      || "leo"`
 * 兩次都是「拿一個部署環境變數的字面預設值，當成使用者資料的歸屬」。規則早就在（rule 07
 * 薄殼、design §3.3 租戶不下發），但**沒有任何機制會擋**，所以它每隔幾週就長回來一次。
 * leo 2026-08-12：「做一個平台要減少 hotfix。」⇒ 修掉 bug 不算完成，要留下會擋的東西。
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * 判準：看「有沒有在做那件事」，不是看「有沒有出現那個詞」
 * ─────────────────────────────────────────────────────────────────────────────
 * 誤攔比漏攔更容易殺死一道閘（被擋煩了就有人把它關掉），所以三條規則全部盯**行為**：
 *
 *   T1 租戶環境變數只有一個產地
 *      `env.CONSOLE_TENANT` / `env.ARCRUN_NAMESPACE` 只能在 src/lib/tenant.ts 被讀取。
 *      盯的是「你在把部署設定讀成身分」這個動作本身。註解裡寫這兩個字不算（只看 `env.X` 取值）。
 *
 *   T2 資料面租戶識別不得憑空捏造
 *      `as TenantId` 只能出現在 src/lib/tenant.ts，且不得套在字面字串上。
 *      盯的是「繞過唯一產地自己造一個租戶」。
 *
 *   T3 帳號層字串不得流進知識資料面
 *      同一行同時「在組 owner_id」且「值來自 portalTenant()/accountTenant()」→ 擋。
 *      這正是 #108 那一行的形狀：`owner_id=${encodeURIComponent(portalTenant(c.env))}`。
 *      `owner_id: ns`（帳號子 namespace，合法）不命中；`x.owner_id` 這種讀取也不命中。
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * 這道閘自己要能被測試
 * ─────────────────────────────────────────────────────────────────────────────
 * 核心是純函式 `scanSource(relPath, text)`（不碰檔案系統），測試餵好例子/壞例子驗它會不會叫
 * （tests/tenant-gate.test.ts）——當晚有一道閘連讀自己的原始碼都擋，導致沒人驗得了它。
 * 本檔只掃 `src/`，測試與 fixture 都不在掃描範圍內，所以**不會擋到自己**。
 *
 * 本檔是**純規則**（零 node 相依），所以 Workers runtime 的 vitest 也 import 得動；
 * 走檔案系統的那半在 check-tenant-source.mjs。
 */

/** 唯一允許產出租戶識別的檔案（相對 cypher-executor/）。 */
export const TENANT_SOURCE_FILE = 'src/lib/tenant.ts';
/** 只宣告型別、不取值的檔案（`CONSOLE_TENANT?: string` 這種）。 */
const TYPE_DECL_FILES = new Set(['src/types.ts']);

/** 被視為「租戶來源」的環境變數——讀它們＝在決定使用者資料的歸屬。 */
const TENANT_ENV_VARS = ['CONSOLE_TENANT', 'ARCRUN_NAMESPACE'];

/** 帳號層租戶字串的取得方式（回的是 string 不是 TenantId，不得用於知識資料面）。 */
const ACCOUNT_TENANT_CALLS = ['portalTenant(', 'accountTenant('];

const ENV_READ = new RegExp(String.raw`\benv\s*\.\s*(${TENANT_ENV_VARS.join('|')})\b`);
const AS_TENANT_ID = /\bas\s+TenantId\b/;
const LITERAL_AS_TENANT_ID = /(['"`][^'"`]*['"`])\s*as\s+TenantId\b/;

/**
 * 「這一行在組 owner_id 嗎？」——**構造**才算，**讀取**不算。
 *   算：`owner_id=` 出現在字串/樣板裡、`owner_id:` 當成物件屬性在賦值
 *   不算：`x.owner_id`（讀）、`owner_id?:`（型別宣告）、`owner_id` 單獨出現在註解句子裡
 */
function buildsOwnerFilter(line) {
  const code = stripComment(line);
  if (!code.includes('owner_id')) return false;
  if (/owner_id\s*=/.test(code) && !/[.\w]owner_id\s*=/.test(code)) return true; // `?owner_id=` / `owner_id=${...}`
  if (/(^|[^.\w])owner_id\s*:/.test(code) && !/owner_id\s*\?\s*:/.test(code)) return true; // `owner_id: X`
  return false;
}

/** 去掉行末 `//` 註解（不處理跨行 /* *\/——那種行本來就不含可執行的取值）。 */
function stripComment(line) {
  const i = line.indexOf('//');
  return i === -1 ? line : line.slice(0, i);
}

/** 整行是註解？（`//` 開頭或位於 JSDoc 區塊的 ` *` 行） */
function isCommentLine(line) {
  const t = line.trim();
  return t.startsWith('//') || t.startsWith('*') || t.startsWith('/*');
}

/**
 * 掃一份原始碼，回傳違規清單（純函式，測試直接餵字串）。
 * @param {string} relPath 相對 cypher-executor/ 的路徑，例如 'src/routes/portal-data.ts'
 * @param {string} text 檔案內容
 * @returns {{rule: string, line: number, text: string, message: string}[]}
 */
export function scanSource(relPath, text) {
  const rel = relPath.split('\\').join('/');
  const violations = [];
  const lines = text.split('\n');

  lines.forEach((line, idx) => {
    const n = idx + 1;
    const push = (rule, message) =>
      violations.push({ rule, line: n, text: line.trim(), message });

    if (isCommentLine(line)) return;
    const code = stripComment(line);

    // T1：租戶環境變數只有一個產地
    if (rel !== TENANT_SOURCE_FILE && !TYPE_DECL_FILES.has(rel) && ENV_READ.test(code)) {
      push(
        'T1',
        `租戶環境變數只能在 ${TENANT_SOURCE_FILE} 讀取。` +
          '在別處讀它＝又一次「身分來自環境變數」（#105/#108 同形），' +
          '請改呼叫 knowledgeOwner(env)（知識資料面）或 accountTenant(env)（帳號層）。',
      );
    }

    // T2：資料面租戶識別不得憑空捏造
    if (AS_TENANT_ID.test(code)) {
      if (rel !== TENANT_SOURCE_FILE) {
        push(
          'T2',
          `TenantId 只能由 ${TENANT_SOURCE_FILE} 產生。自己 cast 一個等於繞過唯一產地——` +
            '請用 knowledgeOwner(env) 或 tenantFromApiKey(header)。',
        );
      } else if (LITERAL_AS_TENANT_ID.test(code)) {
        push(
          'T2',
          '不得把**字面字串**當成租戶識別（那就是 `|| "leo"` 那個預設值的原形）。' +
            '解析不到請丟 TenantUnresolvedError，誠實說讀不到。',
        );
      }
    }

    // T3：帳號層字串不得流進知識資料面
    if (buildsOwnerFilter(line) && ACCOUNT_TENANT_CALLS.some((fn) => code.includes(fn))) {
      push(
        'T3',
        '這一行拿**帳號層**租戶字串去組知識資料面的 owner_id 過濾——' +
          '正是 #108 那一行（1854 條三元組被過濾成 0）。' +
          '知識資料面請用 knowledgeOwner(env) + ownerQuery()/ownerField()。',
      );
    }
  });

  return violations;
}

