/**
 * Named Webhook（acr push 使用）
 *
 * POST /webhooks/named
 *   Header: X-Arcrun-API-Key
 *   Body: { name, graph, config?, description? }
 *   → 以 {api_key}:wf:{name} 存入 WEBHOOKS KV
 *   → 回傳 webhook_url
 *
 * POST /webhooks/named/:name/trigger
 *   Header: X-Arcrun-API-Key
 *   Body: 任意 JSON（作為 trigger context）
 *   → 以 {api_key}:wf:{name} 讀取執行圖，執行後回傳結果
 *
 * GET /webhooks/named
 *   Header: X-Arcrun-API-Key
 *   → 列出當前 api_key 下所有 named webhook
 *
 * DELETE /webhooks/named/:name
 *   Header: X-Arcrun-API-Key
 *   → 刪除指定 workflow
 */

import { Hono } from 'hono';
import type { Context } from 'hono';
import type { Bindings } from '../types';
import { executeWebhookGraph } from '../actions/webhook-handlers';
import { writeExecutionVerdict } from '../actions/execution-logger';
import type { GraphNode } from '../types';
import { extractCronExpr } from '../lib/cron-match';
import { updateCronIndexEntry, CRON_INDEX_KEY } from '../lib/cron-index';
import { recordTelemetry } from '../lib/telemetry';
import { fetchTenantWorkflowSearch } from '../lib/workflow-search';

export const webhooksNamedRouter = new Hono<{ Bindings: Bindings }>();

type NamedWorkflowRecord = {
  name: string;
  graph: Record<string, unknown>;
  config?: Record<string, unknown>;
  description: string;
  created_at: string;
  // 若首節點是 cron 零件，extract cron_expr 存進來供 scheduled() 比對
  // 對應 SDD: arcrun.md 三-A P1 #3
  cron_expr?: string;
  // 暴露 consent 閘已移除（leo 2026-06-29，Arcrun#13）。保留欄位只為向後相容舊 KV record。
  exposure_consent?: unknown;
};

function kvKey(apiKey: string, name: string): string {
  return `${apiKey}:wf:${name}`;
}

/**
 * workflow-discovery R2/Phase 2.1：部署時雙寫一個 embeddable entry 到 KBDB，讓 workflow 可被語意搜尋。
 *
 * 雙寫（design 方案 C）：WEBHOOKS KV record 照舊（list/get/trigger 不動），另寫 entry_type=workflow 的
 * entry 供 search。owner_id = api_key（租戶隔離，與 kbdb-proxy 同身份模型）。
 * content = description（被 embed 的主體）；metadata.embed:true → 命中 #7 精耕條件進 Vectorize（模組開時）。
 *
 * 非阻塞 + 失敗不致命（waitUntil + catch）：search 可發現性是加值，不該擋部署成功（對齊 #7 embedOnWrite 慣例）。
 * KBDB 連法沿用既有慣例（KBDB_BASE_URL fetch + 選用 token），不新增 service binding（rule 02 §3.1）。
 */
async function writeWorkflowSearchEntry(
  env: Bindings,
  apiKey: string,
  name: string,
  description: string,
  workflowId?: string,
): Promise<void> {
  const base = (env.KBDB_BASE_URL ?? 'https://arcrun-kbdb.uncle6-me.workers.dev').replace(/\/$/, '');
  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
  if (env.KBDB_INTERNAL_TOKEN) headers['Authorization'] = `Bearer ${env.KBDB_INTERNAL_TOKEN}`;
  await fetch(`${base}/entries`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      entry_type: 'workflow',
      owner_id: apiKey,              // 租戶隔離（與 kbdb-proxy 同身份）
      page_name: name,
      content: description,          // 被 embed / LIKE 命中的主體
      // KBDB createEntry 吃 metadata_json（TEXT），embed.ts isEmbeddable 讀 metadata_json.embed === true。
      metadata_json: JSON.stringify({
        embed: true,                // #7 精耕開關：標 true 才進 Vectorize
        workflow_name: name,
        workflow_id: workflowId ?? name,
      }),
    }),
  });
}

// POST /webhooks/named — 部署（acr push 呼叫）
webhooksNamedRouter.post('/webhooks/named', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }

  const body = await c.req.json().catch(() => null) as {
    name?: string;
    graph?: Record<string, unknown>;
    config?: Record<string, unknown>;
    description?: string;
  } | null;

  if (!body?.name || !body.graph) {
    return c.json({ error: '缺少必要欄位：name, graph' }, 400);
  }

  // workflow-discovery R1：description 強制非空（供語意搜尋，工作流可被發現）。
  // 定位（Q2 定案）：要求操盤的 AI 據實寫一句「這工作流能做什麼」，非逼 low-code 用戶手填、
  // 非介面層機械塞佔位。空 → 擋下，由操盤 CC 據實補一句再部署（用戶可改）。
  if (typeof body.description !== 'string' || body.description.trim() === '') {
    return c.json({
      error: 'description 必填：請操盤的 AI 據實寫一句「這工作流能做什麼」（如「呼叫可 Upsert Google Sheets」），用戶可再改。供語意搜尋用，不是寫文章。',
      requires: 'description',
    }, 400);
  }

  const name = body.name.trim();
  if (!/^[\w-]+$/.test(name)) {
    return c.json({ error: 'workflow name 只能包含英文字母、數字、底線和連字號' }, 400);
  }

  // 暴露 consent 閘已移除（leo 2026-06-29，Arcrun#13）：部署 webhook 不再需要人類確認，直接放行。

  // 偵測首節點是 cron 零件 → 抽 cron_expr 存進 record + 建輕量 index 給 scheduled()
  const cronExpr = extractCronExpr(body.graph);

  const record: NamedWorkflowRecord = {
    name,
    graph: body.graph,
    config: body.config,
    description: body.description.trim(),  // R1：已驗非空（見上），存 trim 後的值
    created_at: new Date().toISOString(),
    cron_expr: cronExpr ?? undefined,
  };

  const start = Date.now();
  await c.env.WEBHOOKS.put(kvKey(apiKey, name), JSON.stringify(record));

  // 維護單一 cron index key（8.P0）：有 cron_expr 就 upsert / 沒有就移除
  // （避免 push 改 yaml 拿掉 cron 後殘留）。scheduled() 每分鐘只 get 這一個 key。
  await updateCronIndexEntry(c.env.WEBHOOKS, apiKey, name, cronExpr);

  // workflow-discovery Phase 2.1：雙寫 embeddable search-entry（讓此 workflow 可被語意搜尋）。
  // 非阻塞（waitUntil）+ 失敗不致命（catch）：可發現性是加值，不擋部署成功（對齊 #7 embedOnWrite 慣例）。
  c.executionCtx.waitUntil(
    writeWorkflowSearchEntry(c.env, apiKey, name, record.description).catch(() => {}),
  );

  // Implicit telemetry (LI M1.2)
  recordTelemetry(c.env, apiKey, {
    event_type: 'deploy_success',
    workflow_name: name,
    duration_ms: Date.now() - start,
    agent_user_agent: c.req.header('User-Agent') ?? undefined,
  }, c.executionCtx);

  const baseUrl = new URL(c.req.url).origin;
  return c.json({
    name,
    webhook_url: `${baseUrl}/webhooks/named/${name}/trigger`,
    description: record.description,
    created_at: record.created_at,
  }, 201);
});

// GET /workflows/search?q=&mode= — workflow-discovery R2：語意搜尋本租戶的工作流。
// 轉發 KBDB /entries/search（限 entry_type=workflow + 本租戶 owner_id）。優先語意、未開 Vectorize
// 降級 keyword + capability_hint（KBDB 端已實作 #7 閉環，本端純轉發 + 注 entry_type/owner_id）。
// 形態對齊 u6u_search_components：自然語言 q 進、結果 + capability_hint 出。flag 安全：AI 主動 pull，無輪詢。
webhooksNamedRouter.get('/workflows/search', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  const q = c.req.query('q');
  if (!q) return c.json({ error: 'q 必填：用自然語言描述要找的工作流（如「把資料寫進 Google Sheets」）' }, 400);
  // 預設優先語意；caller 傳 mode=keyword 才強制關鍵字。KBDB 端未開 Vectorize 會自動降級。
  const mode = c.req.query('mode') === 'keyword' ? 'keyword' : 'semantic';

  // KBDB 轉發抽到 lib/workflow-search.ts（t159 target 參數）：本路由與
  // POST /cypher/search { target:"workflow" } 共用同一條路，行為必然一致。
  const res = await fetchTenantWorkflowSearch(c.env, apiKey, q, mode);
  return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
});

// POST /workflows/backfill-search-entries — workflow-discovery R3：把既有 workflow 補成可搜的 search-entry。
// 有 description 的 → 補寫 entry（讓它們可被 u6u_search_workflows 搜到）；無 description 的 → 列出待 re-deploy。
// 誠實：不自動編造 description（無 desc 的只列出、不假裝）。flag 安全：人/AI 主動呼叫一次，非 cron/輪詢。
webhooksNamedRouter.post('/workflows/backfill-search-entries', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);

  const prefix = `${apiKey}:wf:`;
  const list = await c.env.WEBHOOKS.list({ prefix });
  const backfilled: string[] = [];
  const needsDescription: string[] = [];
  const errors: string[] = [];

  for (const k of list.keys) {
    const name = k.name.slice(prefix.length);
    const raw = await c.env.WEBHOOKS.get(k.name, 'text');
    if (!raw) continue;
    const rec = JSON.parse(raw) as NamedWorkflowRecord;
    const desc = rec.description?.trim();
    if (!desc) {
      // 不自動編造：無 description 的列出來，請操盤 CC re-deploy 時據實補（誠實，mindset §7）。
      needsDescription.push(name);
      continue;
    }
    try {
      await writeWorkflowSearchEntry(c.env, apiKey, name, desc);
      backfilled.push(name);
    } catch (e) {
      errors.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
    }
  }

  return c.json({
    backfilled,
    backfilled_count: backfilled.length,
    needs_description: needsDescription,
    needs_description_count: needsDescription.length,
    errors,
    hint: needsDescription.length > 0
      ? `${needsDescription.length} 個工作流缺 description 無法被搜尋。請操盤的 AI re-deploy 它們時據實補一句「能做什麼」（不自動編造）。`
      : undefined,
  });
});

// POST /webhooks/named/migrate-cron-index — 一次性 migration（8.P0）：把舊的 per-key
// cron-idx:{apiKey}:{name} 折進單一 cron-idx:_all（這裡才 list 一次，非每分鐘 tick）。
// 增量寫、不刪舊 key（重跑安全、冪等）。部署 8.P0 後跑一次，讓既有 cron workflow 不漏掉。
// 必須在 /:name/trigger 之前註冊，否則 :name 會攔截 "migrate-cron-index"。
webhooksNamedRouter.post('/webhooks/named/migrate-cron-index', async (c) => {
  const list = await c.env.WEBHOOKS.list({ prefix: 'cron-idx:' });
  let migrated = 0, skipped = 0;
  const errors: string[] = [];
  for (const k of list.keys) {
    if (k.name === CRON_INDEX_KEY) { skipped++; continue; }        // 跳過新的集中 key 自己
    const parts = k.name.split(':');                                // cron-idx:{apiKey}:{name}
    if (parts.length < 3) { skipped++; continue; }
    const apiKey = parts[1];
    const name = parts.slice(2).join(':');
    try {
      const raw = await c.env.WEBHOOKS.get(k.name, 'text');
      if (!raw) { skipped++; continue; }
      const idx = JSON.parse(raw) as { cron_expr?: string };
      if (!idx.cron_expr) { skipped++; continue; }
      await updateCronIndexEntry(c.env.WEBHOOKS, apiKey, name, idx.cron_expr);
      migrated++;
    } catch (e) {
      errors.push(`${k.name}: ${e instanceof Error ? e.message : String(e)}`);
    }
  }
  return c.json({ success: errors.length === 0, migrated, skipped, errors });
});

// POST /webhooks/named/:name/trigger — 觸發執行（api_key 走 header；標準/向後相容）
webhooksNamedRouter.post('/webhooks/named/:name/trigger', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }
  return triggerNamed(c, apiKey, c.req.param('name'));
});

// POST /webhooks/named/:ns/:name/trigger — namespace 走 URL path（給公開表單用）。
// self-hosted namespace 是明碼分區標籤（非密碼），故可放 path 讓無法帶 header 的公開呼叫者觸發。
// 要防外部濫用 → 對 webhook 加保護（mindset §6）；arcrun 不做授權判斷（mindset §3）。
// SDD: sdk-and-website/self-hosted-init.md（壓測 §7.2 第3點 runtime 觸發）
webhooksNamedRouter.post('/webhooks/named/:ns/:name/trigger', async (c) => {
  return triggerNamed(c, c.req.param('ns'), c.req.param('name'));
});

// 共用觸發邏輯（header 路徑與 path 路徑都用，避免分叉）
async function triggerNamed(
  c: Context<{ Bindings: Bindings }>,
  apiKey: string,
  name: string,
) {
  const raw = await c.env.WEBHOOKS.get(kvKey(apiKey, name), 'text');
  if (!raw) {
    return c.json({ error: `找不到 workflow "${name}"，請先執行 acr push` }, 404);
  }

  let record: NamedWorkflowRecord;
  try {
    record = JSON.parse(raw) as NamedWorkflowRecord;
  } catch {
    return c.json({ error: 'workflow 定義損毀' }, 500);
  }

  let triggerContext: Record<string, unknown> = {};
  try {
    const body = await c.req.json().catch(() => null);
    if (body && typeof body === 'object') {
      triggerContext = body as Record<string, unknown>;
    }
  } catch {
    // 無 body 時使用空 context
  }

  const graph = record.graph as { id?: string; nodes?: unknown[] };
  const workflowId = graph.id ?? name;
  const nodes = Array.isArray(graph.nodes) ? (graph.nodes as GraphNode[]) : [];
  const userAgent = c.req.header('User-Agent') ?? undefined;

  // resumable-workflow SDD §5：?async=1 → 背景執行（waitUntil）+ 立回 202，不依賴呼叫端連線。
  // 不帶 ?async=1 維持原同步行為（向後相容）。
  if (c.req.query('async') === '1') {
    c.executionCtx.waitUntil(
      executeWebhookGraph(c.env, record.graph, triggerContext, name, apiKey, c.executionCtx, userAgent)
        .then(result =>
          writeExecutionVerdict(c.env, workflowId, nodes, result.success ? 'success' : 'failed', result.duration_ms, result.error ?? '', triggerContext, apiKey),
        ),
    );
    return c.json({ accepted: true }, 202);
  }

  const result = await executeWebhookGraph(
    c.env,
    record.graph,
    triggerContext,
    name,
    apiKey,
    c.executionCtx,
    userAgent,
  );

  c.executionCtx.waitUntil(
    writeExecutionVerdict(c.env, workflowId, nodes, result.success ? 'success' : 'failed', result.duration_ms, result.error ?? '', triggerContext, apiKey),
  );

  return c.json(result, result.success ? 200 : 500);
}

// ── 同步查詢 trigger（sync query）─────────────────────────────────────────────
//
// 動機：named webhook 的 /trigger 預設路徑雖已同步（await），但它回傳的是
//   `{ success, data, trace, duration_ms }` **信封**、且只有 POST 形態。
// 查詢面（console / MCP 打 graph neighbors / traverse 之類）需要 request→response
//   **直接拿「工作流最終節點輸出」** 當 HTTP response body，且常是一個 GET。
// 這組端點補上這個泛化：同步 await 執行 workflow graph → 把 **result.data（最終節點輸出）本身**
//   當 response body 回（非 202、非信封）。補上它，任何查詢端點都能是一個 workflow。
//
// 認證：沿用 X-Arcrun-API-Key（header 形態）或 namespace 走 path（公開形態，與 /trigger path 版對稱；
//   self-hosted namespace 是明碼分區標籤非密碼，故可放 path — mindset §3 arcrun 不做授權判斷）。
// 誠實（mindset §7）：節點失敗回錯誤 + trace 摘要（非把錯誤當輸出假綠）；paused 工作流無法同步
//   給答案 → 明講（409），不假裝成功。

// 同步查詢輸出上限（防超大 response body 撐爆 Worker / 呼叫端）。
// 超過 → 回 413 + 誠實錯誤（請在 workflow 內先聚合/分頁），不截斷假裝成功。
const MAX_QUERY_OUTPUT_BYTES = 5 * 1024 * 1024; // 5 MiB

// GET：把 query string 全部欄位當 triggerContext（值皆 string）。
function queryStringContext(c: Context<{ Bindings: Bindings }>): Record<string, unknown> {
  return { ...c.req.query() };
}

// POST：body（JSON object）當 triggerContext；非物件 / 無 body → 空 context。
async function bodyContext(c: Context<{ Bindings: Bindings }>): Promise<Record<string, unknown>> {
  const body = await c.req.json().catch(() => null);
  return body && typeof body === 'object' ? (body as Record<string, unknown>) : {};
}

// 共用同步查詢邏輯（header 路徑與 path 路徑、GET 與 POST 都用，避免分叉）。
async function queryNamed(
  c: Context<{ Bindings: Bindings }>,
  apiKey: string,
  name: string,
  triggerContext: Record<string, unknown>,
) {
  const raw = await c.env.WEBHOOKS.get(kvKey(apiKey, name), 'text');
  if (!raw) {
    return c.json({ error: `找不到 workflow "${name}"，請先執行 acr push` }, 404);
  }

  let record: NamedWorkflowRecord;
  try {
    record = JSON.parse(raw) as NamedWorkflowRecord;
  } catch {
    return c.json({ error: 'workflow 定義損毀' }, 500);
  }

  const graph = record.graph as { id?: string; nodes?: unknown[] };
  const workflowId = graph.id ?? name;
  const nodes = Array.isArray(graph.nodes) ? (graph.nodes as GraphNode[]) : [];
  const userAgent = c.req.header('User-Agent') ?? undefined;

  // 同步執行（await，非 waitUntil）：查詢端點必須 request→response 拿到結果。
  const result = await executeWebhookGraph(
    c.env,
    record.graph,
    triggerContext,
    name,
    apiKey,
    c.executionCtx,
    userAgent,
  );

  // 執行判決寫入不阻塞回應（waitUntil，與 /trigger 一致）。
  c.executionCtx.waitUntil(
    writeExecutionVerdict(c.env, workflowId, nodes, result.success ? 'success' : 'failed', result.duration_ms, result.error ?? '', triggerContext, apiKey),
  );

  if (!result.success) {
    // paused（如 claude_api 等外部 callback resume）無法同步給答案 → 明講，不假裝成功。
    const paused = typeof result.error === 'string' && /workflow paused/i.test(result.error);
    return c.json(
      {
        success: false,
        error: result.error ?? '工作流執行失敗',
        trace: result.trace,
        ...(paused
          ? { paused: true, hint: '此工作流會暫停等待非同步 callback，無法當同步查詢端點；改用 /webhooks/named/:name/trigger?async=1 + /workflows/resume。' }
          : {}),
      },
      paused ? 409 : 500,
    );
  }

  // 成功 → 回「最終節點輸出」本身當 response body（非 202、非信封）。
  const serialized = JSON.stringify(result.data ?? null);
  const byteLen = new TextEncoder().encode(serialized).byteLength;
  if (byteLen > MAX_QUERY_OUTPUT_BYTES) {
    return c.json(
      {
        success: false,
        error: `查詢輸出過大（${byteLen} bytes > 上限 ${MAX_QUERY_OUTPUT_BYTES}）。請在 workflow 內先聚合 / 分頁再回。`,
      },
      413,
    );
  }
  return new Response(serialized, {
    status: 200,
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      'X-Arcrun-Duration-Ms': String(result.duration_ms),
    },
  });
}

// GET /webhooks/named/:name/query — header 認證，input 走 query string（console/MCP 主用）
webhooksNamedRouter.get('/webhooks/named/:name/query', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  return queryNamed(c, apiKey, c.req.param('name'), queryStringContext(c));
});

// POST /webhooks/named/:name/query — header 認證，input 走 body
webhooksNamedRouter.post('/webhooks/named/:name/query', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  return queryNamed(c, apiKey, c.req.param('name'), await bodyContext(c));
});

// POST /webhooks/named/:ns/:name/query — namespace 走 path（公開查詢，與 /trigger path 版對稱）
webhooksNamedRouter.post('/webhooks/named/:ns/:name/query', async (c) => {
  return queryNamed(c, c.req.param('ns'), c.req.param('name'), await bodyContext(c));
});

// GET /q/:ns/:name — 簡短查詢入口（namespace 走 path，input 走 query string）
webhooksNamedRouter.get('/q/:ns/:name', async (c) => {
  return queryNamed(c, c.req.param('ns'), c.req.param('name'), queryStringContext(c));
});

// GET /webhooks/named/:name/definition — 吐 workflow 的可攜定義（t158 export 原語）。
// leo 07-31：「如果我要把我做的工作流分享給同事，我要怎麼 export？他要如何 import？
// 在從前就是寫成幾個 yaml 丟過去讓新的送進 KBDB 不是嗎？」
// 回 record 原樣（graph＋config＋description）＝import 端可直接 POST /webhooks/named 送進
// 任何實例（acr workflow import／安裝器同一條路）。執行語義不驗證（部署≠發現）。
webhooksNamedRouter.get('/webhooks/named/:name/definition', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  const name = c.req.param('name');
  const raw = await c.env.WEBHOOKS.get(kvKey(apiKey, name), 'text');
  if (!raw) return c.json({ error: `找不到 workflow "${name}"` }, 404);
  const rec = JSON.parse(raw) as NamedWorkflowRecord;
  return c.json({
    name: rec.name,
    description: rec.description ?? '',
    graph: rec.graph,
    config: rec.config ?? {},
    created_at: rec.created_at ?? '',
    ...(rec.cron_expr ? { cron_expr: rec.cron_expr } : {}),
  });
});

// GET /webhooks/named — 列出當前 api_key 下所有 workflow
webhooksNamedRouter.get('/webhooks/named', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }

  const prefix = `${apiKey}:wf:`;
  const list = await c.env.WEBHOOKS.list({ prefix });

  // workflow-discovery 方向①：list 回完整欄位（description/created_at），讓 MCP u6u_list_workflows
  // 改讀本端點時欄位齊（取代舊的讀 workflow_metadata record）。需 get 每個 record 取 description。
  const baseUrl = new URL(c.req.url).origin;
  const result = await Promise.all(
    list.keys.map(async (k) => {
      const name = k.name.slice(prefix.length);
      const raw = await c.env.WEBHOOKS.get(k.name, 'text');
      const rec = raw ? (JSON.parse(raw) as NamedWorkflowRecord) : null;
      return {
        name,
        description: rec?.description ?? '',
        created_at: rec?.created_at ?? '',
        cron_expr: rec?.cron_expr,
        webhook_url: `${baseUrl}/webhooks/named/${name}/trigger`,
      };
    }),
  );

  return c.json({ workflows: result, total: result.length });
});

// DELETE /webhooks/named/:name — 刪除 workflow
webhooksNamedRouter.delete('/webhooks/named/:name', async (c) => {
  const apiKey = c.req.header('X-Arcrun-API-Key');
  if (!apiKey) {
    return c.json({ error: '缺少 X-Arcrun-API-Key header' }, 401);
  }

  const name = c.req.param('name');
  const existing = await c.env.WEBHOOKS.get(kvKey(apiKey, name), 'text');
  if (!existing) {
    return c.json({ error: `找不到 workflow "${name}"` }, 404);
  }

  await c.env.WEBHOOKS.delete(kvKey(apiKey, name));
  await updateCronIndexEntry(c.env.WEBHOOKS, apiKey, name, null);
  return c.json({ deleted: true, name });
});
