#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";

const RED = "\x1b[0;31m";
const YELLOW = "\x1b[1;33m";
const NC = "\x1b[0m";
const IGNORE_FILE = ".secretscanignore";

// Fast-mode contract (sf-mp4w2dij-xm6cwj AC4): the regex-only path is the
// today-default fast path. SF_SECURITY_FAST=1 is the explicit opt-in for
// callers that want to assert "regex-only, no LLM escalation, sub-100ms"
// regardless of any future tiered reviewer landing in this script. When the
// LLM-backed review hook (AC1) lands, the absence of this env var becomes
// the trigger for escalation; setting it=1 keeps the offline / pre-commit
// fast path. Today the variable changes only the trailing status line so
// operators can verify the contract is observable.
const FAST_MODE = process.env.SF_SECURITY_FAST === "1";

const PATTERNS = [
	{ label: "AWS Access Key", regex: /AKIA[0-9A-Z]{16}/g },
	{
		label: "Generic API Key",
		regex:
			/(api[_-]?key|apikey|api[_-]?secret)[ \t]*[:=][ \t]*['"][0-9a-zA-Z_./-]{20,}['"]/gi,
	},
	{
		label: "Generic Secret",
		regex:
			/(secret|token|password|passwd|pwd|credential)[ \t]*[:=][ \t]*['"][^\s'"]{8,}['"]/gi,
	},
	{
		label: "Authorization Header",
		regex: /(authorization|bearer)[ \t]*[:=][ \t]*['"][^\s'"]{8,}['"]/gi,
	},
	{
		label: "Private Key",
		regex: /-----BEGIN\s+(RSA|DSA|EC|OPENSSH|PGP)\s+PRIVATE\s+KEY-----/g,
	},
	{
		label: "Database URL",
		regex:
			/(mysql|postgres|postgresql|mongodb|redis|amqp|mssql):\/\/[^\s'"]{8,}/gi,
	},
	{ label: "GitHub Token", regex: /gh[pousr]_[0-9a-zA-Z]{36,}/g },
	{ label: "GitLab Token", regex: /glpat-[0-9a-zA-Z-]{20,}/g },
	{ label: "Slack Token", regex: /xox[baprs]-[0-9a-zA-Z-]{10,}/g },
	{
		label: "Slack Webhook",
		regex:
			/hooks\.slack\.com\/services\/T[0-9A-Z]{8,}\/B[0-9A-Z]{8,}\/[0-9a-zA-Z]{20,}/g,
	},
	{ label: "Google API Key", regex: /AIza[0-9A-Za-z_-]{35}/g },
	{ label: "Stripe Key", regex: /[sr]k_(live|test)_[0-9a-zA-Z]{20,}/g },
	{ label: "npm Token", regex: /npm_[0-9a-zA-Z]{36,}/g },
	{
		label: "Hex Secret",
		regex:
			/(secret|key|token|password)[ \t]*[:=][ \t]*['"]?[0-9a-f]{32,}['"]?/gi,
	},
	{
		label: "Hardcoded Password",
		regex: /password[ \t]*[:=][ \t]*['"][^'"]{4,}['"]/gi,
	},
];

function runGit(args) {
	try {
		return execFileSync("git", args, {
			encoding: "utf8",
			shell: process.platform === "win32",
			stdio: ["ignore", "pipe", "ignore"],
		});
	} catch {
		return "";
	}
}

function parseArgs(argv) {
	if (argv[0] === "--diff") {
		return { mode: "diff", ref: argv[1] || "HEAD" };
	}
	if (argv[0] === "--file") {
		return { mode: "file", file: argv[1] || "" };
	}
	return { mode: "staged" };
}

function getFiles(options) {
	if (options.mode === "diff") {
		return runGit(["diff", "--name-only", "--diff-filter=ACMR", options.ref]);
	}
	if (options.mode === "file") {
		return options.file;
	}
	return runGit(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
}

function shouldScan(file) {
	const lower = file.toLowerCase();
	const skippedExtensions = [
		".png",
		".jpg",
		".jpeg",
		".gif",
		".ico",
		".svg",
		".woff",
		".woff2",
		".ttf",
		".eot",
		".zip",
		".tar",
		".gz",
		".tgz",
		".bz2",
		".7z",
		".rar",
		".exe",
		".dll",
		".so",
		".dylib",
		".o",
		".a",
		".pdf",
		".doc",
		".docx",
		".xls",
		".xlsx",
		".lock",
		".map",
		".node",
		".wasm",
	];
	if (skippedExtensions.some((ext) => lower.endsWith(ext))) return false;
	if (
		lower === ".secretscanignore" ||
		lower === ".gitignore" ||
		lower === ".gitattributes" ||
		lower.startsWith("license") ||
		lower.startsWith("changelog") ||
		lower.endsWith(".md") ||
		lower === "package-lock.json" ||
		lower === "pnpm-lock.yaml" ||
		lower === "bun.lock"
	) {
		return false;
	}
	if (
		lower.startsWith("node_modules/") ||
		lower.startsWith("dist/") ||
		lower.startsWith("coverage/") ||
		lower.startsWith(".sf/")
	) {
		return false;
	}
	if (lower.endsWith(".min.js") || lower.endsWith(".min.css")) return false;
	return true;
}

function getContent(file, mode) {
	if (mode === "staged") {
		const staged = runGit(["show", `:${file}`]);
		if (staged) return staged;
	}
	try {
		return readFileSync(file, "utf8");
	} catch {
		return "";
	}
}

function loadIgnorePatterns() {
	if (!existsSync(IGNORE_FILE)) return [];
	return readFileSync(IGNORE_FILE, "utf8")
		.split(/\r?\n/)
		.map((line) => line.trim())
		.filter((line) => line && !line.startsWith("#"));
}

function isIgnored(file, lineContent, ignorePatterns) {
	return ignorePatterns.some((pattern) => {
		const splitIndex = pattern.indexOf(":");
		if (splitIndex > 0) {
			const ignoreFile = pattern.slice(0, splitIndex);
			const ignoreRegex = pattern.slice(splitIndex + 1);
			if (file !== ignoreFile) return false;
			try {
				return new RegExp(ignoreRegex, "i").test(lineContent);
			} catch {
				return false;
			}
		}

		try {
			return new RegExp(pattern, "i").test(lineContent);
		} catch {
			return false;
		}
	});
}

function resetRegex(regex) {
	regex.lastIndex = 0;
	return regex;
}

const options = parseArgs(process.argv.slice(2));
const files = getFiles(options)
	.split(/\r?\n/)
	.map((file) => file.trim())
	.filter(Boolean);

if (files.length === 0) {
	process.stdout.write("secret-scan: no files to scan\n");
	process.exit(0);
}

const ignorePatterns = loadIgnorePatterns();
let findings = 0;

for (const file of files) {
	if (!shouldScan(file)) continue;
	const content = getContent(file, options.mode);
	if (!content) continue;

	const lines = content.split(/\r?\n/);
	for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
		const line = lines[lineIndex];
		for (const pattern of PATTERNS) {
			if (!resetRegex(pattern.regex).test(line)) continue;
			if (isIgnored(file, line, ignorePatterns)) continue;

			process.stdout.write(
				`${RED}[SECRET DETECTED]${NC} ${YELLOW}${pattern.label}${NC}\n`,
			);
			process.stdout.write(`  File: ${file}:${lineIndex + 1}\n`);
			process.stdout.write(`  Line: ${line.slice(0, 120)}...\n\n`);
			findings++;
		}
	}
}

if (findings > 0) {
	process.stdout.write(
		`${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n`,
	);
	process.stdout.write(
		`${RED}Found ${findings} potential secret(s) in scanned files.${NC}\n`,
	);
	process.stdout.write(
		`${RED}Commit blocked. Remove the secrets or add exceptions${NC}\n`,
	);
	process.stdout.write(
		`${RED}to .secretscanignore if these are false positives.${NC}\n`,
	);
	process.stdout.write(
		`${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n`,
	);
	process.exit(1);
}

if (FAST_MODE) {
	process.stdout.write(
		"secret-scan: fast mode (regex-only, no LLM escalation) ✓\n",
	);
} else {
	process.stdout.write("secret-scan: no secrets detected ✓\n");
}
