#!/usr/bin/env node
"use strict";

const { execFileSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");

const WAIVER = "local-first-allow-convex-main-storage";
const MIGRATION_PERIOD_WAIVER = "sqlite-migration-waiver";

const RUNTIME_FORBIDDEN = [
  {
    name: "未标注的 Convex documents 直连",
    pattern: /documents:(?:createWithParentReference|updateTitle|move|updateContent|getMeta|getContent|purge)\b/,
  },
  {
    name: "未标注的 Convex media 直连",
    pattern: /mediaAssets:(?:generateUploadUrl|createWithStorage|getById|refreshUrl|patchById|emptyTrashByWorkspace|purgeById|listByIds)\b/,
  },
  {
    name: "未标注的 Convex AI session 直连",
    pattern:
      /aiSessions:(?:upsertRuntimeRun|appendRuntimeEvent|getRuntimeRun|listRuntimeRuns|listRuntimeEvents|renameRuntimeSession|autoTitleRuntimeSession|deleteRuntimeSession|searchRuntimeSessions)\b/,
  },
  {
    name: "Convex media URL 主路径",
    pattern: /\/api\/media\/(?:upload|sign|batch)|assetId=/,
  },
];

const DESIGN_DEFAULT_CONVEX = /(?:Convex|convex).*(?:默认|default).*(?:主存储|主数据层|数据真相|页面正文真相|文件树真相|附件|AI 会话|session)/;
const DESIGN_NEGATION =
  /(?:不应|不得|不再|禁止|只作为|降级|控制面|检查|若把|应要求改成|不是|不作为|不依赖|可选|sync replica|cloud source|compat|依赖证据|代码盘点|未完成|部分完成|当前状态)/;
const ACTIVE_CONVEX_FORBIDDEN = [
  {
    name: "未标注的 Convex documents 表",
    pattern: /\bdocuments\s*:\s*defineTable\b/,
  },
  {
    name: "未标注的 Convex media_assets 表",
    pattern: /\bmedia_assets\s*:\s*defineTable\b/,
  },
  {
    name: "未标注的 Convex documents 函数",
    pattern: /export const (?:createWithParentReference|updateTitle|move|updateContent|getMeta|getContent|purge|listByWorkspace)\b/,
  },
  {
    name: "未标注的 Convex mediaAssets 函数",
    pattern: /export const (?:generateUploadUrl|createWithStorage|getById|refreshUrl|patchById|emptyTrashByWorkspace|purgeById|listByIds)\b/,
  },
];

const RETIRED_ROOT_CONVEX_PATHS = [
  {
    name: "根 convex schema",
    pattern: /^convex\/schema\.ts$/,
  },
  {
    name: "根 Convex Auth 函数",
    pattern: /^convex\/auth(?:\.config)?\.ts$/,
  },
  {
    name: "根 Convex 用户函数",
    pattern: /^convex\/users\.ts$/,
  },
  {
    name: "根 Convex AI session 函数",
    pattern: /^convex\/aiSessions\.ts$/,
  },
];

const RETIRED_CONVEX_INFRA_PATHS = [
  {
    name: "Convex 自托管 infra",
    pattern: /^infra\/convex(?:\/|$)/,
  },
  {
    name: "根 storage-convex-bridge 占位文件",
    pattern: /^storage-convex-bridge$/,
  },
];

// ─── SQLite 迁移期 forbid ───────────────────────────────────
// 在 SQLite 控制面迁移阶段（Phase 1-5），新增 Convex 控制面代码
// （users/workspaces/sessions/policies/audit/outbox/share 等）
// 应标记 sqlite-migration-waiver，否则提示应迁移到 SQLite。
const MIGRATION_PERIOD_FORBIDDEN = [
  {
    name: "未标注迁移例外的 Convex 控制面 users 代码",
    pattern: /(?:[Uu]sers?\s*[:=.]|[a-z]+[A-Z][a-z]*[Uu]ser\b|auth_identities|auth_sessions).*(?:defineTable|mutation|query|schema)/,
    scope: "convex",
  },
  {
    name: "未标注迁移例外的 Convex 控制面 workspace 代码",
    pattern: /(?:[Ww]orkspaces?\s*[:=]|[Ww]orkspace[Mm]embers\b|[Ww]orkspace[Mm]ember\b).*(?:defineTable|mutation|query|schema)/,
    scope: "convex",
  },
  {
    name: "未标注迁移例外的 Convex 控制面 grants/access 代码",
    pattern: /\b(?:directory_grants|directoryGrants|access_policy|accessPolicy)\b/,
    scope: "convex",
  },
  {
    name: "未标注迁移例外的 Convex 控制面 share 代码",
    pattern: /\b(?:share_links|shareLinks)\b/,
    scope: "convex",
  },
  {
    name: "未标注迁移例外的 Convex 控制面 sync/audit/outbox 代码",
    pattern: /\b(?:sync_state|syncState|audit_log|auditLog|outbox_events|outboxEvents)\b/,
    scope: "convex",
  },
  {
    name: "未标注迁移例外的 Convex 控制面 ai_policies 代码",
    pattern: /\b(?:ai_policies|aiPolicies|ai_policy|aiPolicy)\b/,
    scope: "convex",
  },
  {
    name: "未标注迁移例外的 Convex 控制面 legacy_id_map 代码",
    pattern: /\b(?:legacy_id_map|legacyIdMap)\b/,
    scope: "convex",
  },
];

function usage() {
  return [
    "用法：",
    "  node scripts/check-local-first-convex-guard.js",
    "  node scripts/check-local-first-convex-guard.js --base origin/main",
    "  node scripts/check-local-first-convex-guard.js --staged",
    "  node scripts/check-local-first-convex-guard.js --files <path...>",
    "  node scripts/check-local-first-convex-guard.js --self-test",
    "",
    `如需保留明确的 cloud/compat 直连，请在同一新增行加入 ${WAIVER}。`,
    "根 convex/ functions 源码已退役；如确需新增显式 cloud/compat Convex functions，应先放到明确的辅助目录并更新 guard。",
    "infra/convex 自托管基础设施已退役到 recycle；不要重新引回 active 路径。",
    `SQLite 迁移期中新增 Convex 控制面代码（users/workspaces/grants/share 等）需加 ${MIGRATION_PERIOD_WAIVER} 标注迁移例外。`,
  ].join("\n");
}

function parseArgs(argv) {
  const result = {
    base: "HEAD",
    staged: false,
    files: [],
    selfTest: false,
  };
  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];
    if (arg === "--help" || arg === "-h") {
      console.log(usage());
      process.exit(0);
    }
    if (arg === "--staged") {
      result.staged = true;
      continue;
    }
    if (arg === "--self-test") {
      result.selfTest = true;
      continue;
    }
    if (arg === "--base") {
      const value = argv[index + 1];
      if (!value) throw new Error("--base 需要一个 git ref");
      result.base = value;
      index += 1;
      continue;
    }
    if (arg === "--files") {
      result.files = argv.slice(index + 1);
      break;
    }
    throw new Error(`未知参数：${arg}\n${usage()}`);
  }
  return result;
}

function isDesignPath(filePath) {
  return filePath.startsWith("design/") && !filePath.startsWith("design/old/") && filePath.endsWith(".md");
}

function isRuntimePath(filePath) {
  return filePath.startsWith("rust/crates/mnote-web/src/") && filePath.endsWith(".rs");
}

function isActiveConvexPath(filePath) {
  return filePath.startsWith("convex/") && filePath.endsWith(".ts") && !filePath.startsWith("convex/_generated/");
}

function isRetiredRootConvexPath(filePath) {
  return RETIRED_ROOT_CONVEX_PATHS.some((entry) => entry.pattern.test(filePath));
}

function isRetiredConvexInfraPath(filePath) {
  return RETIRED_CONVEX_INFRA_PATHS.some((entry) => entry.pattern.test(filePath));
}

function isAllowedRuntimeAdapter(filePath) {
  return (
    filePath === "rust/crates/mnote-web/src/transport/convex.rs" ||
    filePath === "rust/crates/mnote-web/src/routes/compat.rs"
  );
}

function normalizePath(filePath) {
  return filePath.split(path.sep).join("/");
}

function readDiffLines(options) {
  const args = ["diff", "--unified=0", "--no-ext-diff"];
  if (options.staged) {
    args.push("--cached");
  } else {
    args.push(options.base);
  }
  args.push("--");
  const output = execFileSync("git", args, { encoding: "utf8" });
  return output.split(/\r?\n/);
}

function collectAddedLinesFromDiff(lines) {
  const added = [];
  let currentFile = null;
  let newLine = 0;

  for (const line of lines) {
    const fileMatch = line.match(/^\+\+\+ b\/(.+)$/);
    if (fileMatch) {
      currentFile = fileMatch[1];
      continue;
    }
    if (line.startsWith("+++ /dev/null")) {
      currentFile = null;
      continue;
    }
    const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
    if (hunkMatch) {
      newLine = Number(hunkMatch[1]);
      continue;
    }
    if (!currentFile || line.startsWith("diff --git") || line.startsWith("index ")) {
      continue;
    }
    if (line.startsWith("+") && !line.startsWith("+++")) {
      added.push({ filePath: currentFile, lineNumber: newLine, text: line.slice(1) });
      newLine += 1;
      continue;
    }
    if (!line.startsWith("-")) {
      newLine += 1;
    }
  }

  return added;
}

function collectLinesFromFiles(files) {
  const rows = [];
  for (const rawPath of files) {
    const filePath = normalizePath(rawPath);
    const text = fs.readFileSync(filePath, "utf8");
    text.split(/\r?\n/).forEach((line, index) => {
      rows.push({ filePath, lineNumber: index + 1, text: line });
    });
  }
  return rows;
}

function isMigrationWaived(row) {
  return row.text.includes(WAIVER) || row.text.includes(MIGRATION_PERIOD_WAIVER);
}

function checkLine(row) {
  if (isMigrationWaived(row)) {
    return [];
  }

  if (isDesignPath(row.filePath)) {
    if (DESIGN_DEFAULT_CONVEX.test(row.text) && !DESIGN_NEGATION.test(row.text)) {
      return [
        {
          ...row,
          reason: "设计稿新增口径把 Convex 描述成默认主存储；请改成 LocalFS/WorkspaceSource 默认，或明确标注 cloud/control-plane/compat。",
        },
      ];
    }
    return [];
  }

  if (isRetiredRootConvexPath(row.filePath)) {
    return [
      {
        ...row,
        reason: `${RETIRED_ROOT_CONVEX_PATHS.find((entry) => entry.pattern.test(row.filePath)).name} 已随根 convex/ 退役；不要重新新增为默认控制面，请迁到 Rust SQLite control-plane 或明确的 cloud/compat 辅助目录。`,
      },
    ];
  }

  if (isRetiredConvexInfraPath(row.filePath)) {
    return [
      {
        ...row,
        reason: `${RETIRED_CONVEX_INFRA_PATHS.find((entry) => entry.pattern.test(row.filePath)).name} 已软删除到 recycle；不要重新引回 active 路径或默认启动链路。`,
      },
    ];
  }

  if (!isRuntimePath(row.filePath) || isAllowedRuntimeAdapter(row.filePath)) {
    if (!isActiveConvexPath(row.filePath)) {
      return [];
    }
    const violations = ACTIVE_CONVEX_FORBIDDEN.filter((entry) => entry.pattern.test(row.text)).map((entry) => ({
      ...row,
      reason: `${entry.name} 出现在 active convex 目录；新增 Convex 代码只能是 auth、membership、share grants、sync state、AI policy、cloud source、compat 或 sync replica，并需用 ${WAIVER} 标注例外。`,
    }));
    // 在迁移期内，新增控制面 Convex 代码也应标记例外
    if (violations.length === 0) {
      const migrationHit = MIGRATION_PERIOD_FORBIDDEN.filter((entry) => entry.pattern.test(row.text)).map((entry) => ({
        ...row,
        reason: `${entry.name} — SQLite 迁移期内新增 Convex 控制面代码应标记 ${MIGRATION_PERIOD_WAIVER} 迁移例外，或将逻辑迁移到 Rust control-plane SQLite 实现。`,
      }));
      violations.push(...migrationHit);
    }
    return violations;
  }

  return RUNTIME_FORBIDDEN.filter((entry) => entry.pattern.test(row.text)).map((entry) => ({
    ...row,
    reason: `${entry.name} 出现在非 Convex adapter 路径；请先走 WorkspaceSource / Rust kernel / LocalFS executor，或加 ${WAIVER} 并说明原因。`,
  }));
}

function runSelfTest() {
  const rows = [
    {
      filePath: "convex/schema.ts",
      lineNumber: 1,
      text: "  documents: defineTable({",
    },
    {
      filePath: "convex/aiSessions.ts",
      lineNumber: 1,
      text: "export const upsertRuntimeRun = mutation({",
    },
    {
      filePath: "convex/mediaAssets.ts",
      lineNumber: 1,
      text: "export const generateUploadUrl = mutation({",
    },
    {
      filePath: "convex/shareGrants.ts",
      lineNumber: 1,
      text: "export const upsertShareGrant = mutation({",
    },
    {
      filePath: "rust/crates/mnote-web/src/routes/example.rs",
      lineNumber: 1,
      text: '"documents:updateContent"',
    },
    {
      filePath: "rust/crates/mnote-web/src/transport/convex.rs",
      lineNumber: 1,
      text: '"documents:updateContent"',
    },
    {
      filePath: "convex/users.ts",
      lineNumber: 1,
      text: "  users: defineTable({",
    },
    {
      filePath: "convex/workspaces_migration.ts",
      lineNumber: 1,
      text: "  workspaces: defineTable({",
    },
    {
      filePath: "infra/convex/docker-compose.yml",
      lineNumber: 1,
      text: "services:",
    },
    {
      filePath: "storage-convex-bridge",
      lineNumber: 1,
      text: "",
    },
  ];
  const violations = rows.flatMap(checkLine);
  const labels = violations.map((item) => item.filePath);
  if (!labels.includes("convex/schema.ts")) {
    throw new Error("self-test 失败：未拦截 active convex documents 表");
  }
  if (!labels.includes("convex/aiSessions.ts")) {
    throw new Error("self-test 失败：未拦截退役根 convex AI session 函数");
  }
  if (!labels.includes("convex/mediaAssets.ts")) {
    throw new Error("self-test 失败：未拦截 active convex media 函数");
  }
  if (!labels.includes("rust/crates/mnote-web/src/routes/example.rs")) {
    throw new Error("self-test 失败：未拦截非 adapter Rust Convex 直连");
  }
  if (labels.includes("convex/shareGrants.ts")) {
    throw new Error("self-test 失败：误拦截控制面 share grants");
  }
  if (labels.includes("rust/crates/mnote-web/src/transport/convex.rs")) {
    throw new Error("self-test 失败：误拦截 Convex adapter");
  }
  if (!labels.includes("convex/users.ts")) {
    throw new Error("self-test 失败：未拦截迁移期新增 Convex 控制面 users 代码");
  }
  if (!labels.includes("convex/workspaces_migration.ts")) {
    throw new Error("self-test 失败：未拦截迁移期新增 Convex 控制面 workspaces 代码");
  }
  if (!labels.includes("infra/convex/docker-compose.yml")) {
    throw new Error("self-test 失败：未拦截退役的 Convex infra 回流");
  }
  if (!labels.includes("storage-convex-bridge")) {
    throw new Error("self-test 失败：未拦截根 storage-convex-bridge 占位文件回流");
  }
  console.log(JSON.stringify({ ok: true, guard: "local-first-convex", selfTest: true }, null, 2));
}

function main() {
  const options = parseArgs(process.argv.slice(2));
  if (options.selfTest) {
    runSelfTest();
    return;
  }
  const rows = options.files.length > 0 ? collectLinesFromFiles(options.files) : collectAddedLinesFromDiff(readDiffLines(options));
  const violations = rows.flatMap(checkLine);

  if (violations.length > 0) {
    console.error("local-first Convex guard 发现新增主存储绑定：");
    for (const violation of violations) {
      console.error(`- ${violation.filePath}:${violation.lineNumber} ${violation.reason}`);
      console.error(`  ${violation.text.trim()}`);
    }
    process.exit(1);
  }

  console.log(JSON.stringify({ ok: true, checkedLines: rows.length, guard: "local-first-convex" }, null, 2));
}

if (require.main === module) {
  try {
    main();
  } catch (error) {
    console.error(error instanceof Error ? error.message : String(error));
    process.exit(1);
  }
}
