/**
 * 租戶字串的**唯一產地**（Arcrun#108）。
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * 這個檔案存在的理由（不是為了整潔，是為了不再犯同一個錯）
 * ─────────────────────────────────────────────────────────────────────────────
 * #105：`ownerNamespace(env) = env.MCP_OWNER_NAMESPACE || "leo"` ——身分來自環境變數。
 * #108：`portalTenant(env)  = env.CONSOLE_TENANT      || "leo"` ——同一句話換一個檔案。
 *
 * 兩次的形狀一模一樣：**「這筆資料是誰的」與「這個請求是誰」來自兩個可以各自漂移的地方**。
 * leo 的知識在 `owner_id=bfezv28v`（08-11 回灌時定的名，也就是他 `~/.arcrun/config.yaml`
 * 的 `api_key`、小幫手上傳時帶的 `X-Arcrun-API-Key`），而 cypher 拿 repo 預設值 `"leo"`
 * 去過濾 ⇒ 1854 條三元組被過濾成 0，畫面卻只寫「沒有庫」。
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * 定案：租戶字串從哪裡來
 * ─────────────────────────────────────────────────────────────────────────────
 * **從「寫入這批知識的那一方」來，而不是從一份手抄的環境變數預設值來。**
 *
 * 寫入端只有一個真相源：使用者 `~/.arcrun/config.yaml` 的 `api_key`（＝實例 namespace）。
 * CLI 用它 push workflow（`{ns}:wf:*`）、小幫手用它上傳知識（`owner_id=ns`）、
 * MCP 用它當 Bearer。**讀取端必須用同一個值**，否則讀寫兩端各說各話。
 * 所以 `acr init/update` 把它注入成 `ARCRUN_NAMESPACE`（cli/src/lib/deploy.ts，
 * 與 CF_ACCOUNT_ID / WORKER_SUBDOMAIN / MULTI_TENANT 同一批 CLI 管理值）——
 * 它不是「使用者要自己維護的設定」，是**從既有真相源導出的值**，因此不會漂。
 *
 * 那為什麼不像 #105 一樣「掛在登入者身上」？因為在這個架構裡租戶**不是**每人一個：
 * portal 帳號共用同一台實例的知識庫（design D-2，帳號自己住 `{tenant}::portal` 子
 * namespace），帳號之間的差別是 `libraries` 權限，不是 owner_id。把 owner_id 複製一份
 * 到每個帳號上，只會多一個可以各自過期的副本——那正是本票的病，不是解藥。
 * #105 真正的教訓不是「一律搬到帳號上」，而是：
 *   **過濾用的租戶字串要有單一權威來源、解析不到要誠實失敗、而且要能被機械驗證。**
 * 這三件事就是本檔在做的事。
 *
 * ─────────────────────────────────────────────────────────────────────────────
 * 型別即閘（`TenantId`）
 * ─────────────────────────────────────────────────────────────────────────────
 * `TenantId` 是 branded string，**只能**由本檔產生（`knowledgeOwner` / `tenantFromApiKey`）。
 * 所有資料面 owner_id 過濾一律經 `ownerQuery()` / `ownerField()`，而那兩支只吃 `TenantId`
 * ⇒ 想把「隨手一個 env 字串」拿去過濾，`tsc` 當場就不給過。
 *
 * 配套的機械檢查在 `scripts/check-tenant-source.mjs`（測試 `tests/tenant-gate.test.ts`
 * 會同時驗「repo 現況乾淨」與「這道閘真的擋得住壞例子」）。
 */
import type { Bindings } from '../types';

/**
 * 可以拿去做資料面過濾的租戶識別。
 *
 * branded type：外面拿不到建構子，只能從本檔的兩支 minter 取得——
 * 一支從實例 namespace 來（`knowledgeOwner`），一支從請求本身來（`tenantFromApiKey`）。
 * 兩支都不含字面預設值。
 */
export type TenantId = string & { readonly __tenantId: unique symbol };

/** 實例 namespace 解析不出來 → 誠實炸掉，不拿預設值當答案（#100「讀不到就說讀不到」同源）。 */
export class TenantUnresolvedError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'TenantUnresolvedError';
  }
}

/**
 * 這台實例的**知識 owner_id**——三元組 / entries / records / 藏書地圖 / 工作流 KV
 * 全部掛在這個字串底下，由 CLI、小幫手、MCP 寫入時決定。
 *
 * 解析順序（**沒有字面預設值**）：
 *   1. `ARCRUN_NAMESPACE`——`acr init/update` 從 `~/.arcrun/config.yaml` 的 `api_key` 注入。
 *      這是寫入端用的那個值本身，因此永遠對得上。
 *   2. `CONSOLE_TENANT`——官方 prod（`cypher.arcrun.dev`）與 #108 之前部署的實例走這條。
 *      官方 prod 的知識確實寫在 `leo` 底下，所以對它而言這是正解；對跑過 `acr update`
 *      的 self-hosted 實例，第 1 條會先命中。
 *   3. 兩個都沒有 → **丟 TenantUnresolvedError**。不回 `"leo"`：那個預設值正是把
 *      「這台機器沒設定」偽裝成「你沒有資料」的元凶。
 */
export function knowledgeOwner(env: Bindings): TenantId {
  const injected = (env.ARCRUN_NAMESPACE ?? '').trim();
  if (injected) return injected as TenantId;
  const legacy = (env.CONSOLE_TENANT ?? '').trim();
  if (legacy) return legacy as TenantId;
  throw new TenantUnresolvedError(
    '這個部署沒有知識命名空間（ARCRUN_NAMESPACE / CONSOLE_TENANT 都沒設）——' +
      '不知道要去哪一格找資料。請跑 `acr update` 讓它從你的 ~/.arcrun/config.yaml 注入。',
  );
}

/**
 * 請求自帶的租戶（`X-Arcrun-API-Key`＝namespace 明碼，self-hosted 身分模型）。
 *
 * 這條路的租戶來自**請求本身**而不是環境變數，本來就沒有 #105/#108 的漂移問題；
 * 收進本檔只是為了讓「所有 owner_id 過濾值都是 TenantId」這條型別閘沒有破口。
 * 空字串不給過——沒有身分就不該有查詢範圍。
 */
export function tenantFromApiKey(apiKey: string): TenantId {
  const key = (apiKey ?? '').trim();
  if (!key) throw new TenantUnresolvedError('缺少 X-Arcrun-API-Key，無法決定查詢範圍');
  return key as TenantId;
}

/**
 * 帳號子 namespace 用的租戶字串（design D-2：帳號資料住 `{tenant}::portal`）。
 *
 * 🔴 **回傳的是 `string`，不是 `TenantId`——這是刻意的**：帳號那批資料是 cypher 自己
 * 寫進去的（用的就是這個值），所以它自洽；但它**不可以**拿去過濾知識資料面，
 * 否則就是把 #108 再犯一次。型別上不給過，不必靠人記得。
 *
 * 保留 `'leo'` 預設值是為了不動既有帳號的落點（改了會讓舊實例登不進去）。
 */
export function accountTenant(env: Bindings): string {
  return env.CONSOLE_TENANT || 'leo';
}

/**
 * KBDB query string 的 owner_id 過濾片段——**資料面過濾的唯一入口之一**。
 * 用法：`kbdbFetch(env, `/map?${ownerQuery(tenant)}`)`
 */
export function ownerQuery(tenant: TenantId): string {
  return `owner_id=${encodeURIComponent(tenant)}`;
}

/**
 * 要放進 JSON body / URLSearchParams 的 owner_id 值——**資料面過濾的唯一入口之一**。
 * 用法：`JSON.stringify({ owner_id: ownerField(tenant) })`
 */
export function ownerField(tenant: TenantId): string {
  return tenant;
}

/**
 * 🔴 **刻意不帶租戶範圍**的查詢片段（KBDB 慣例：`owner_id` 空值＝不過濾）。
 *
 * 唯一合法用途：#100 的普查——「本租戶查到 0 筆」時再問一次「整台實例到底有沒有」，
 * 用來分辨「查不到」與「沒有」。**回傳的是統計數字，不是任何人的內容**；
 * 拿它去撈實際資料就是跨租戶外洩。名字取得這麼長就是要讓 review 一眼看見。
 */
export function censusQueryAllTenants(): string {
  return 'owner_id=';
}

/** 逐筆核對歸屬（讀回來的 record/entry 是不是這個租戶的）。缺欄位一律視為不是。 */
export function isOwnedBy(value: unknown, tenant: TenantId): boolean {
  return typeof value === 'string' && value === (tenant as string);
}
