/**
 * DataScope 标准字段契约检查
 *
 * 仅检查本次新增的 Prisma model，存量 model 一律放过。
 * 规则来源：docs/standards/04-database-architecture.md「标准字段」
 *
 * 用法：
 *   npx ts-node --transpile-only testing/scripts/data-scope-fields-check.ts --staged
 *   npx ts-node --transpile-only testing/scripts/data-scope-fields-check.ts --base origin/develop
 */

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

const ROOT_DIR = path.resolve(__dirname, '..', '..');
const SCHEMA_DIR = path.join(ROOT_DIR, 'backend/prisma/schema');

const REQUIRED_FIELDS = [
  { name: 'id', typeHint: /String.*@id/ },
  { name: 'createdAt', typeHint: /DateTime.*@db\.Timestamptz\(3\)/ },
  { name: 'updatedAt', typeHint: /DateTime.*@db\.Timestamptz\(3\)/ },
  { name: 'createdById', typeHint: /String.*@db\.Uuid/ },
  { name: 'organizationId', typeHint: /String.*@db\.Uuid/ },
];

const FORBIDDEN_SYNONYMS: Record<string, string> = {
  creatorId: 'createdById',
  authorId: 'createdById',
  ownerId: 'createdById',
  submittedBy: 'createdById',
  submittedById: 'createdById',
  orgId: 'organizationId',
  tenantId: 'organizationId',
};

interface ModelBlock {
  name: string;
  body: string;
  skipReason: string | null;
  file: string;
  startLine: number;
}

interface Violation {
  file: string;
  model: string;
  line: number;
  severity: 'error' | 'warning';
  message: string;
}

function parseArgs() {
  const args = process.argv.slice(2);
  const mode = args.includes('--staged') ? 'staged' : 'base';
  const baseIdx = args.indexOf('--base');
  const base = baseIdx >= 0 ? args[baseIdx + 1] : 'origin/develop';
  return { mode, base };
}

function getChangedSchemaFiles(mode: string, base: string): string[] {
  const cmd =
    mode === 'staged'
      ? `git diff --cached --name-only --diff-filter=AM -- 'backend/prisma/schema/*.prisma'`
      : `git diff ${base}...HEAD --name-only --diff-filter=AM -- 'backend/prisma/schema/*.prisma'`;
  try {
    return execSync(cmd, { cwd: ROOT_DIR, encoding: 'utf-8' })
      .split('\n')
      .filter(Boolean);
  } catch {
    return [];
  }
}

function gitShow(ref: string, file: string): string {
  try {
    return execSync(`git show ${ref}:${file}`, {
      cwd: ROOT_DIR,
      encoding: 'utf-8',
      stdio: ['pipe', 'pipe', 'ignore'],
    });
  } catch {
    return '';
  }
}

function getBaseContent(file: string, mode: string, base: string): string {
  return gitShow(mode === 'staged' ? 'HEAD' : base, file);
}

function getNewContent(file: string, mode: string): string {
  if (mode === 'staged') return gitShow('', file);
  const absPath = path.join(ROOT_DIR, file);
  return fs.existsSync(absPath) ? fs.readFileSync(absPath, 'utf-8') : '';
}

function extractModels(content: string, file: string): ModelBlock[] {
  const lines = content.split('\n');
  const models: ModelBlock[] = [];
  let i = 0;
  while (i < lines.length) {
    const m = lines[i].match(/^model\s+(\w+)\s*\{/);
    if (!m) {
      i++;
      continue;
    }
    const name = m[1];
    const startLine = i + 1;
    let skipReason: string | null = null;
    for (let j = i - 1; j >= 0 && lines[j].trim().startsWith('///'); j--) {
      const skip = lines[j].match(/@skip-data-scope\s+(.+)/);
      if (skip) skipReason = skip[1].trim();
    }
    const bodyLines: string[] = [];
    i++;
    while (i < lines.length && !lines[i].startsWith('}')) {
      bodyLines.push(lines[i]);
      i++;
    }
    i++;
    models.push({ name, body: bodyLines.join('\n'), skipReason, file, startLine });
  }
  return models;
}

function hasField(body: string, fieldName: string): string | null {
  const re = new RegExp(`^\\s*${fieldName}\\s+.+$`, 'm');
  const match = body.match(re);
  return match ? match[0] : null;
}

function checkModel(model: ModelBlock): Violation[] {
  const violations: Violation[] = [];
  if (model.skipReason !== null) return violations;

  for (const field of REQUIRED_FIELDS) {
    const line = hasField(model.body, field.name);
    if (!line) {
      violations.push({
        file: model.file,
        model: model.name,
        line: model.startLine,
        severity: 'error',
        message: `缺少强制字段 \`${field.name}\``,
      });
      continue;
    }
    if (!field.typeHint.test(line)) {
      violations.push({
        file: model.file,
        model: model.name,
        line: model.startLine,
        severity: 'error',
        message: `字段 \`${field.name}\` 类型不符合要求（期望匹配 ${field.typeHint}）`,
      });
    }
  }

  for (const [bad, good] of Object.entries(FORBIDDEN_SYNONYMS)) {
    if (hasField(model.body, bad)) {
      violations.push({
        file: model.file,
        model: model.name,
        line: model.startLine,
        severity: 'error',
        message: `禁用同义词 \`${bad}\`，请使用 \`${good}\``,
      });
    }
  }

  if (!/@@index\(\[.*organizationId.*\]\)/.test(model.body)) {
    violations.push({
      file: model.file,
      model: model.name,
      line: model.startLine,
      severity: 'warning',
      message: `建议为 \`organizationId\` 加 \`@@index\`（DataScope 过滤的核心字段）`,
    });
  }

  return violations;
}

function main() {
  const { mode, base } = parseArgs();
  const files = getChangedSchemaFiles(mode, base);

  if (files.length === 0) {
    process.exit(0);
  }

  const violations: Violation[] = [];

  for (const file of files) {
    const newContent = getNewContent(file, mode);
    if (!newContent) continue;

    const baseContent = getBaseContent(file, mode, base);

    const baseModelNames = new Set(
      extractModels(baseContent, file).map((m) => m.name),
    );
    const newModels = extractModels(newContent, file).filter(
      (m) => !baseModelNames.has(m.name),
    );

    for (const model of newModels) {
      violations.push(...checkModel(model));
    }
  }

  const errors = violations.filter((v) => v.severity === 'error');
  const warnings = violations.filter((v) => v.severity === 'warning');

  if (errors.length > 0) {
    console.error('\n❌ DataScope 标准字段契约检查失败\n');
    for (const v of errors) {
      console.error(`  ${v.file}:${v.line}  model ${v.model}`);
      console.error(`    → ${v.message}`);
    }
    console.error(
      '\n规则详见 docs/standards/04-database-architecture.md「标准字段」',
    );
    console.error(
      '如确需豁免（关联表/字典表等），在 model 上方加注释：/// @skip-data-scope <原因>\n',
    );
  }

  if (warnings.length > 0) {
    console.warn('\n⚠️  DataScope 标准字段契约检查警告\n');
    for (const v of warnings) {
      console.warn(`  ${v.file}:${v.line}  model ${v.model}`);
      console.warn(`    → ${v.message}`);
    }
    console.warn('');
  }

  if (errors.length > 0) process.exit(1);
  process.exit(0);
}

main();
