/**
 * repo-profiler.ts - read-only repository shape profiler.
 *
 * Purpose: give harness evolution a factual repo snapshot without staging,
 * deleting, or claiming ownership of user files.
 */
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
import { basename, extname, join, sep } from "node:path";
import { GIT_NO_PROMPT_ENV } from "./git-constants.js";

const HASH_READ_LIMIT_BYTES = 1024 * 1024;
function git(args, cwd, allowFailure = true) {
	try {
		return execFileSync("git", args, {
			cwd,
			stdio: ["ignore", "pipe", "pipe"],
			encoding: "utf-8",
			env: GIT_NO_PROMPT_ENV,
		}).trimEnd();
	} catch {
		if (allowFailure) return "";
		throw new Error(`git ${args.join(" ")} failed in ${cwd}`);
	}
}
function sha256(input) {
	return createHash("sha256").update(input).digest("hex");
}
function normalizePath(path) {
	return path.split(sep).join("/");
}
function pathExists(basePath, relPath) {
	return existsSync(join(basePath, relPath));
}
function signal(items, item) {
	items.push(item);
}
function languageForPath(path) {
	const ext = extname(path).toLowerCase();
	const file = basename(path).toLowerCase();
	if (file === "go.mod" || ext === ".go") return "go";
	if (file === "package.json" || ext === ".ts" || ext === ".tsx")
		return "typescript";
	if (ext === ".js" || ext === ".jsx" || ext === ".mjs" || ext === ".cjs")
		return "javascript";
	if (file === "cargo.toml" || ext === ".rs") return "rust";
	if (file === "pyproject.toml" || ext === ".py") return "python";
	if (file === "flake.nix" || file === "shell.nix" || ext === ".nix")
		return "nix";
	if (ext === ".md" || ext === ".markdown") return "markdown";
	if (ext === ".sql") return "sql";
	if (file.includes("dockerfile")) return "dockerfile";
	if (ext === ".yml" || ext === ".yaml") return "yaml";
	if (ext === ".json") return "json";
	return null;
}
function summarizeFile(basePath, relPath) {
	const fullPath = join(basePath, relPath);
	try {
		const stat = statSync(fullPath);
		if (!stat.isFile()) {
			return { sizeBytes: stat.size, contentHash: null, summary: null };
		}
		if (stat.size > HASH_READ_LIMIT_BYTES) {
			return { sizeBytes: stat.size, contentHash: null, summary: null };
		}
		const content = readFileSync(fullPath);
		const contentHash = `sha256:${createHash("sha256").update(content).digest("hex")}`;
		const asText = content.toString("utf8");
		const firstLine = asText
			.split(/\r?\n/)
			.map((line) => line.trim())
			.find((line) => line.length > 0);
		return {
			sizeBytes: stat.size,
			contentHash,
			summary: firstLine ? firstLine.slice(0, 160) : null,
		};
	} catch {
		return { sizeBytes: 0, contentHash: null, summary: null };
	}
}
function parsePorcelainStatus(line, basePath, now) {
	if (line.length < 4) return null;
	const code = line.slice(0, 2);
	const rawPath = line.slice(3);
	const normalizedRawPath = normalizePath(rawPath);
	const path = normalizedRawPath.includes(" -> ")
		? normalizedRawPath.split(" -> ").at(-1)
		: normalizedRawPath;
	let gitStatus = "modified";
	if (code === "??") gitStatus = "untracked";
	else if (code.includes("D")) gitStatus = "deleted";
	else if (code.includes("R")) gitStatus = "renamed";
	const ownership = gitStatus === "untracked" ? "observed_only" : "user_owned";
	const fileSummary =
		gitStatus === "deleted"
			? { sizeBytes: 0, contentHash: null, summary: null }
			: summarizeFile(basePath, path);
	return {
		path,
		gitStatus,
		ownership,
		language: languageForPath(path),
		sizeBytes: fileSummary.sizeBytes,
		contentHash: fileSummary.contentHash,
		summary: fileSummary.summary,
		firstSeenAt: now,
		lastSeenAt: now,
		adoptedAt: null,
		adoptionUnitId: null,
	};
}
function detectStacks(basePath) {
	const stacks = [];
	if (pathExists(basePath, "package.json"))
		signal(stacks, { kind: "node", sourcePath: "package.json", confidence: 1 });
	if (pathExists(basePath, "go.mod"))
		signal(stacks, { kind: "go", sourcePath: "go.mod", confidence: 1 });
	if (pathExists(basePath, "Cargo.toml"))
		signal(stacks, { kind: "rust", sourcePath: "Cargo.toml", confidence: 1 });
	if (pathExists(basePath, "pyproject.toml"))
		signal(stacks, {
			kind: "python",
			sourcePath: "pyproject.toml",
			confidence: 1,
		});
	if (pathExists(basePath, "flake.nix"))
		signal(stacks, { kind: "nix", sourcePath: "flake.nix", confidence: 1 });
	if (pathExists(basePath, "shell.nix"))
		signal(stacks, { kind: "nix", sourcePath: "shell.nix", confidence: 0.9 });
	return stacks;
}
function detectEntrypoints(basePath) {
	const entrypoints = [];
	for (const path of [
		"src/index.ts",
		"src/main.ts",
		"src/cli.ts",
		"cmd",
		"main.go",
		"portal/main.go",
		"dr-agent/main.go",
	]) {
		if (pathExists(basePath, path))
			signal(entrypoints, { kind: "entrypoint", path, confidence: 0.8 });
	}
	return entrypoints;
}
function detectTests(basePath) {
	const tests = [];
	for (const path of ["tests", "test", "__tests__", "src/tests"]) {
		if (pathExists(basePath, path))
			signal(tests, { kind: "test-directory", path, confidence: 0.8 });
	}
	return tests;
}
function detectCi(basePath) {
	const ci = [];
	for (const path of [".github/workflows", ".gitlab-ci.yml", "Jenkinsfile"]) {
		if (pathExists(basePath, path))
			signal(ci, { kind: "ci", path, confidence: 0.9 });
	}
	return ci;
}
function detectDocs(basePath) {
	const docs = [];
	for (const path of [
		"SPEC.md",
		"ARCHITECTURE.md",
		"AGENTS.md",
		"README.md",
		"docs",
	]) {
		if (pathExists(basePath, path))
			signal(docs, { kind: "doc", path, confidence: 0.9 });
	}
	return docs;
}
function detectDataStores(basePath) {
	const stores = [];
	for (const path of ["migrations", "prisma", "db", "sql"]) {
		if (pathExists(basePath, path))
			signal(stores, { kind: "database", path, confidence: 0.8 });
	}
	return stores;
}
function detectNetworkSurfaces(basePath) {
	const surfaces = [];
	for (const path of ["portal", "gateway", "api", "web", "server"]) {
		if (pathExists(basePath, path))
			signal(surfaces, { kind: "network", path, confidence: 0.7 });
	}
	return surfaces;
}
function buildRiskHints(args) {
	const hints = [];
	if (args.networkSurfaces.length > 0) {
		hints.push({
			family: "web",
			reason: "network-facing directories detected",
			confidence: 0.7,
		});
	}
	if (args.dataStores.length > 0) {
		hints.push({
			family: "database",
			reason: "database or migration directories detected",
			confidence: 0.8,
		});
	}
	if (args.stacks.some((stack) => stack.kind === "nix")) {
		hints.push({
			family: "infrastructure",
			reason: "Nix project files detected",
			confidence: 0.8,
		});
	}
	if (args.changedFiles.some((file) => file.gitStatus === "untracked")) {
		hints.push({
			family: "harness-drift",
			reason: "untracked files observed in working tree",
			confidence: 0.6,
		});
	}
	if (args.tests.length === 0 || args.ci.length === 0) {
		hints.push({
			family: "verification-gap",
			reason: "tests or CI signals are missing from the repo profile",
			confidence: 0.5,
		});
	}
	return hints;
}
function canonicalRemote(remote) {
	return remote
		.trim()
		.replace(/^https?:\/\/([^/@]+@)?/i, "https://")
		.replace(/\.git$/i, "")
		.toLowerCase();
}
/**
 * Build a read-only repository profile from git status and well-known files.
 *
 * Purpose: seed harness evolution with observable repo facts while preserving
 * user ownership of untracked and modified files.
 *
 * Consumer: `/harness profile` and future pre-plan harness snapshots.
 */
export function profileRepository(basePath, options = {}) {
	const createdAt = options.now?.() ?? new Date().toISOString();
	const projectRoot = realpathSync(basePath);
	const head = git(["rev-parse", "HEAD"], projectRoot) || null;
	const branch = git(["branch", "--show-current"], projectRoot) || null;
	const remote = git(["remote", "get-url", "origin"], projectRoot) || "";
	const remoteHash = remote
		? `sha256:${sha256(canonicalRemote(remote))}`
		: null;
	const projectHash = remote
		? sha256(canonicalRemote(remote)).slice(0, 16)
		: sha256(projectRoot).slice(0, 16);
	const status = git(["status", "--porcelain=v1", "-uall"], projectRoot);
	const changedFiles = status
		.split(/\r?\n/)
		.map((line) => line.trimEnd())
		.filter(Boolean)
		.map((line) => parsePorcelainStatus(line, projectRoot, createdAt))
		.filter((obs) => obs !== null)
		.sort((a, b) => a.path.localeCompare(b.path));
	const stacks = detectStacks(projectRoot);
	const entrypoints = detectEntrypoints(projectRoot);
	const tests = detectTests(projectRoot);
	const ci = detectCi(projectRoot);
	const docs = detectDocs(projectRoot);
	const dataStores = detectDataStores(projectRoot);
	const networkSurfaces = detectNetworkSurfaces(projectRoot);
	const riskHints = buildRiskHints({
		stacks,
		tests,
		ci,
		dataStores,
		networkSurfaces,
		changedFiles,
	});
	const profileId = `rp_${sha256(
		`${projectHash}:${head ?? ""}:${branch ?? ""}:${createdAt}:${changedFiles
			.map((file) => `${file.gitStatus}:${file.path}:${file.contentHash ?? ""}`)
			.join("|")}`,
	).slice(0, 24)}`;
	return {
		profileId,
		projectHash,
		projectRoot,
		git: {
			head,
			branch,
			remoteHash,
			dirty: changedFiles.length > 0,
			changedFiles,
		},
		stacks,
		entrypoints,
		tests,
		ci,
		docs,
		dataStores,
		networkSurfaces,
		riskHints,
		createdAt,
	};
}
