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

const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');

const ROOT = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();

const TEXT_EXTENSIONS = new Set([
  '.cjs', '.css', '.csv', '.env', '.example', '.html', '.js', '.json', '.jsonl',
  '.jsx', '.mjs', '.md', '.py', '.rb', '.sh', '.sql', '.svg', '.test', '.toml',
  '.ts', '.tsx', '.txt', '.xml', '.yaml', '.yml',
]);

const RESERVED_EMAIL_DOMAINS = new Set([
  'b.com',
  'domain.co',
  'example.com',
  'example.net',
  'example.org',
  'github.com',
  'new.com',
  'openai.com',
  'saved.example.com',
  'test',
  'test.com',
  'test.org',
  'test.invalid',
  'vendor.com',
  'localhost',
]);

const RESERVED_USER_PATHS = new Set([
  '...',
  'alice',
  'bob',
  'example',
  'foo',
  'me',
  'someone',
  'test',
  'tester',
  'runner',
  'u',
  'user',
  'x',
  'you',
]);

const CONSUMER_EMAIL_DOMAINS = new Set([
  'gmail.com',
  'googlemail.com',
  'hotmail.com',
  'icloud.com',
  'live.com',
  'me.com',
  'outlook.com',
  'yahoo.com',
]);

const BLOCKED_TRACKED_PATHS = [
  /^builder-journal\.md$/,
  /(^|\/)builder-journal\.md$/,
  /^wall-e\/docs\/ab-screenshots\//,
];

const SECRET_PATTERNS = [
  { name: 'hardcoded Anthropic key', regex: /\bsk-ant-[A-Za-z0-9_-]{12,}\b/g },
  { name: 'hardcoded OpenAI key', regex: /\bsk-proj-[A-Za-z0-9_-]{12,}\b|\bsk-[A-Za-z0-9_-]{24,}\b/g },
  { name: 'Slack token', regex: /\bxox[baprs]-[A-Za-z0-9-]{12,}\b/g },
  { name: 'GitHub token', regex: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/g },
  { name: 'private key block', regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
  { name: 'JWT-like token', regex: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g },
];

function gitFiles() {
  const out = execFileSync('git', ['ls-files', '-z'], { cwd: ROOT });
  return out.toString('utf8').split('\0').filter(Boolean);
}

function isProbablyText(file) {
  const ext = path.extname(file).toLowerCase();
  if (TEXT_EXTENSIONS.has(ext)) return true;
  if (file.endsWith('LICENSE') || file.endsWith('Makefile') || file.endsWith('.gitignore')) return true;
  return false;
}

function readOptionalDenylist(file) {
  try {
    return fs.readFileSync(file, 'utf8')
      .split(/\r?\n/)
      .map((line) => line.trim())
      .filter((line) => line && !line.startsWith('#'));
  } catch {
    return [];
  }
}

function loadPrivateTerms() {
  const files = [
    path.join(ROOT, '.private-data-denylist'),
    path.join(ROOT, '.private-data-denylist.local'),
    path.join(os.homedir(), '.codex', 'private-data-denylist'),
    path.join(os.homedir(), '.codex', 'memories', 'private-data-denylist'),
  ];
  return files.flatMap(readOptionalDenylist);
}

function lineColumn(text, index) {
  const before = text.slice(0, index);
  const lines = before.split(/\n/);
  return { line: lines.length, column: lines[lines.length - 1].length + 1 };
}

function addFinding(findings, file, reason, text, index = 0) {
  const loc = lineColumn(text, index);
  findings.push({ file, line: loc.line, column: loc.column, reason });
}

function scanEmails(findings, file, text) {
  const emailRe = /\b[A-Z0-9._%+-]+@([A-Z0-9.-]+\.[A-Z]{2,}|localhost|test)\b/gi;
  for (const match of text.matchAll(emailRe)) {
    const domain = match[1].toLowerCase();
    if (RESERVED_EMAIL_DOMAINS.has(domain)) continue;
    if (domain.endsWith('.example')) continue;
    if (domain.endsWith('.example.com') || domain.endsWith('.example.net') || domain.endsWith('.example.org')) continue;
    if (domain.endsWith('.invalid')) continue;
    if (CONSUMER_EMAIL_DOMAINS.has(domain)) {
      addFinding(findings, file, `consumer email address: ${match[0]}`, text, match.index);
    }
  }
}

function scanLocalPaths(findings, file, text) {
  const pathRe = /\/Users\/([A-Za-z0-9._-]+)(?=\/)/g;
  for (const match of text.matchAll(pathRe)) {
    if (RESERVED_USER_PATHS.has(match[1])) continue;
    addFinding(findings, file, `non-generic local user path: /Users/${match[1]}`, text, match.index);
  }
}

function scanSecrets(findings, file, text) {
  for (const pattern of SECRET_PATTERNS) {
    for (const match of text.matchAll(pattern.regex)) {
      const normalized = match[0].toLowerCase();
      if (match[0] === 'sk-ant-...') continue;
      if (/fake|test|unused|example|dummy|placeholder|redacted|bearer|from|token|key|stale|rotated/.test(normalized)) continue;
      if (/abc|123/.test(normalized)) continue;
      addFinding(findings, file, pattern.name, text, match.index);
    }
  }
}

function scanPrivateTerms(findings, file, text, terms) {
  for (const term of terms) {
    const idx = text.toLowerCase().indexOf(term.toLowerCase());
    if (idx !== -1) addFinding(findings, file, `local private denylist term: ${term}`, text, idx);
  }
}

function main() {
  const files = gitFiles();
  const privateTerms = loadPrivateTerms();
  const findings = [];

  for (const file of files) {
    for (const pattern of BLOCKED_TRACKED_PATHS) {
      if (pattern.test(file)) {
        findings.push({ file, line: 1, column: 1, reason: 'blocked private/generated path is tracked' });
      }
    }

    if (!isProbablyText(file)) continue;

    let text = '';
    try {
      text = fs.readFileSync(path.join(ROOT, file), 'utf8');
    } catch {
      continue;
    }

    scanEmails(findings, file, text);
    scanLocalPaths(findings, file, text);
    scanSecrets(findings, file, text);
    scanPrivateTerms(findings, file, text, privateTerms);
  }

  if (findings.length) {
    console.error('Private-data scan failed:');
    for (const finding of findings) {
      console.error(`- ${finding.file}:${finding.line}:${finding.column} ${finding.reason}`);
    }
    process.exit(1);
  }

  console.log(`Private-data scan passed (${files.length} tracked files checked).`);
}

main();
