/**
 * drift-detection-gate.js — ADR-0075 verification gate for task-state drift.
 *
 * Purpose: catch completed task rows whose recorded artifacts, completion prose,
 * or source imports no longer match the working tree before downstream UOK flows
 * trust stale completion evidence.
 *
 * Consumer: UOK gate-runner in single-task verification and sweep audits.
 */
import { existsSync, readFileSync } from "node:fs";
import { dirname, extname, isAbsolute, resolve } from "node:path";

const OUTCOME_RANK = {
	pass: 0,
	retry: 1,
	"manual-attention": 2,
	fail: 3,
};

const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
const RESOLUTION_EXTENSIONS = [
	".ts",
	".tsx",
	".js",
	".jsx",
	".mjs",
	".cjs",
	".json",
];

/**
 * @typedef {import("./contracts.js").GateResult} GateResult
 */

function passResult(rationale = "No drift detected.") {
	return {
		outcome: "pass",
		failureClass: "verification",
		rationale,
	};
}

function normalizeArrayColumn(value) {
	if (Array.isArray(value))
		return value.filter((entry) => typeof entry === "string");
	if (typeof value !== "string" || value.trim() === "") return [];
	try {
		const parsed = JSON.parse(value);
		if (Array.isArray(parsed)) {
			return parsed.filter((entry) => typeof entry === "string");
		}
		if (typeof parsed === "string" && parsed.trim()) return [parsed.trim()];
	} catch {
		// Legacy rows may contain comma-separated paths.
	}
	return value
		.split(",")
		.map((entry) => entry.trim())
		.filter(Boolean);
}

function getTaskArtifacts(taskRow) {
	return normalizeArrayColumn(taskRow?.output_artifacts ?? taskRow?.key_files);
}

function isCompleteTask(taskRow) {
	return taskRow?.status === "complete";
}

function taskLabel(taskRow) {
	const parts = [taskRow?.milestone_id, taskRow?.slice_id, taskRow?.id].filter(
		(value) => typeof value === "string" && value.length > 0,
	);
	return parts.length > 0 ? parts.join("/") : "task";
}

function artifactAbsolutePath(artifactPath, basePath) {
	return isAbsolute(artifactPath)
		? artifactPath
		: resolve(basePath, artifactPath);
}

function isSrcCodeArtifact(artifactPath, basePath) {
	const absPath = artifactAbsolutePath(artifactPath, basePath);
	const relPath = absPath.startsWith(resolve(basePath))
		? absPath.slice(resolve(basePath).length + 1)
		: artifactPath;
	return (
		relPath.startsWith("src/") && CODE_EXTENSIONS.has(extname(artifactPath))
	);
}

function checkArtifactsExist(taskRow, basePath) {
	if (!isCompleteTask(taskRow)) return passResult("Task is not complete.");
	const missing = getTaskArtifacts(taskRow).filter(
		(artifactPath) => !existsSync(artifactAbsolutePath(artifactPath, basePath)),
	);
	if (missing.length === 0)
		return passResult("Completed task artifacts exist.");
	return {
		outcome: "fail",
		failureClass: "verification",
		rationale: `task marked complete but artifact at ${missing[0]} missing on disk`,
		findings: missing,
		recommendation: "revert task status to in_progress + re-dispatch",
	};
}

function proseFields(taskRow) {
	return [
		taskRow?.summary,
		taskRow?.result,
		taskRow?.narrative,
		taskRow?.verification_result,
		taskRow?.known_issues,
		taskRow?.deviations,
		taskRow?.full_summary_md,
	]
		.filter((value) => typeof value === "string" && value.trim().length > 0)
		.join("\n");
}

function detectProseStatusMismatch(taskRow) {
	if (!isCompleteTask(taskRow)) return passResult("Task is not complete.");
	const prose = proseFields(taskRow);
	if (!prose) return passResult("Completed task has no drift prose markers.");
	const blockedPattern = /\b(blocked|blocker|unable to|deferred|todo)\b/i;
	const needsPattern =
		/\bneeds\s+(manual|follow[- ]?up|work|attention|implementation|fix|review)\b/i;
	if (!blockedPattern.test(prose) && !needsPattern.test(prose)) {
		return passResult(
			"Completed task prose does not indicate unfinished work.",
		);
	}
	return {
		outcome: "manual-attention",
		failureClass: "verification",
		rationale: `task marked complete but ${taskLabel(taskRow)} prose indicates blocked or unfinished work`,
		findings: prose
			.split("\n")
			.map((line) => line.trim())
			.filter((line) => blockedPattern.test(line) || needsPattern.test(line))
			.slice(0, 5),
		recommendation:
			"review task summary and either update prose or reopen the task",
	};
}

function extractImportsWithRegex(source) {
	const imports = [];
	const lines = source.split("\n");
	const pattern =
		/(?:import\s+(?:[^'"]*?\s+from\s+)?|export\s+[^'"]*?\s+from\s+|require\s*\(\s*)(['"])([^'"]+)\1/g;
	let inBlockComment = false;
	for (let i = 0; i < lines.length; i += 1) {
		const line = lines[i];
		const trimmed = line.trimStart();
		if (inBlockComment) {
			if (line.includes("*/")) inBlockComment = false;
			continue;
		}
		const blockStart = line.indexOf("/*");
		const blockEnd = line.indexOf("*/");
		if (blockStart !== -1 && (blockEnd === -1 || blockEnd < blockStart)) {
			inBlockComment = true;
			continue;
		}
		if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
		pattern.lastIndex = 0;
		let match;
		while ((match = pattern.exec(line)) !== null) {
			if (line.slice(0, match.index).includes("//")) continue;
			imports.push({ specifier: match[2], line: i + 1 });
		}
	}
	return imports;
}

function candidateImportPaths(specifier, sourceFilePath, basePath) {
	const sourceDir = dirname(sourceFilePath);
	const rawPath = specifier.startsWith("/")
		? resolve(basePath, specifier.slice(1))
		: resolve(sourceDir, specifier);
	const candidates = [rawPath];
	const currentExt = extname(rawPath);
	if (currentExt) {
		candidates.push(rawPath);
		if ([".js", ".jsx", ".mjs", ".cjs"].includes(currentExt)) {
			candidates.push(rawPath.slice(0, -currentExt.length));
		}
	} else {
		for (const ext of RESOLUTION_EXTENSIONS) candidates.push(rawPath + ext);
	}
	const baseWithoutRuntimeExt = rawPath.replace(/\.(js|jsx|mjs|cjs)$/u, "");
	for (const ext of RESOLUTION_EXTENSIONS) {
		candidates.push(baseWithoutRuntimeExt + ext);
		candidates.push(resolve(baseWithoutRuntimeExt, `index${ext}`));
	}
	return [...new Set(candidates)];
}

function importResolves(specifier, sourceFilePath, basePath) {
	return candidateImportPaths(specifier, sourceFilePath, basePath).some(
		(candidate) => existsSync(candidate),
	);
}

function checkImportsResolve(filePath, basePath) {
	const absolutePath = artifactAbsolutePath(filePath, basePath);
	if (!isSrcCodeArtifact(filePath, basePath) || !existsSync(absolutePath)) {
		return passResult("No source imports to check.");
	}
	let source = "";
	try {
		source = readFileSync(absolutePath, "utf-8");
	} catch {
		return passResult("Source file could not be read for import drift check.");
	}
	const unresolved = extractImportsWithRegex(source)
		.filter(
			(entry) =>
				entry.specifier.startsWith(".") || entry.specifier.startsWith("/"),
		)
		.filter(
			(entry) => !importResolves(entry.specifier, absolutePath, basePath),
		);
	if (unresolved.length === 0) return passResult("Source imports resolve.");
	const first = unresolved[0];
	return {
		outcome: "fail",
		failureClass: "verification",
		rationale: `import to ${first.specifier} resolves to missing file`,
		findings: unresolved.map(
			(entry) => `${filePath}:${entry.line} -> ${entry.specifier}`,
		),
		recommendation: "restore the imported file or update the import path",
	};
}

function checkTaskImports(taskRow, basePath) {
	const codeArtifacts = getTaskArtifacts(taskRow).filter((artifactPath) =>
		isSrcCodeArtifact(artifactPath, basePath),
	);
	return aggregateResults(
		codeArtifacts.map((artifactPath) =>
			checkImportsResolve(artifactPath, basePath),
		),
		"No source import drift detected.",
	);
}

function aggregateResults(results, passRationale) {
	const actionable = results.filter(Boolean);
	if (actionable.length === 0) return passResult(passRationale);
	const worst = actionable.reduce((current, next) =>
		OUTCOME_RANK[next.outcome] > OUTCOME_RANK[current.outcome] ? next : current,
	);
	if (worst.outcome === "pass") return passResult(passRationale);
	return {
		...worst,
		findings: actionable
			.filter((result) => result.outcome === worst.outcome)
			.flatMap((result) =>
				Array.isArray(result.findings)
					? result.findings
					: result.findings
						? [result.findings]
						: [],
			),
	};
}

function runTaskChecks(taskRow, basePath) {
	return aggregateResults(
		[
			checkArtifactsExist(taskRow, basePath),
			detectProseStatusMismatch(taskRow),
			checkTaskImports(taskRow, basePath),
		],
		`No drift detected for ${taskLabel(taskRow)}.`,
	);
}

async function fetchSweepTasks(dbRead) {
	if (typeof dbRead === "function") {
		const rows = await dbRead({
			kind: "recently-completed-tasks",
			sql: "SELECT * FROM tasks WHERE status = 'complete' ORDER BY completed_at DESC LIMIT 50",
		});
		return Array.isArray(rows) ? rows : [];
	}
	if (dbRead?.prepare && typeof dbRead.prepare === "function") {
		const stmt = dbRead.prepare(
			"SELECT * FROM tasks WHERE status = 'complete' ORDER BY completed_at DESC LIMIT 50",
		);
		if (stmt?.all && typeof stmt.all === "function") return stmt.all();
	}
	return [];
}

export const driftDetectionGate = {
	id: "drift-detection",
	type: "verification",
	async execute(ctx) {
		const basePath = ctx?.basePath ?? process.cwd();
		if (ctx?.scope === "sweep") {
			const rows = await fetchSweepTasks(ctx.dbRead);
			return aggregateResults(
				rows.map((taskRow) => runTaskChecks(taskRow, basePath)),
				`No drift detected across ${rows.length} recently completed task(s).`,
			);
		}
		if (!ctx?.taskRow) {
			return {
				outcome: "manual-attention",
				failureClass: "verification",
				rationale: "drift detection requires ctx.taskRow in single-task mode",
				recommendation: "provide taskRow or run with scope sweep and dbRead",
			};
		}
		return runTaskChecks(ctx.taskRow, basePath);
	},
};

export default driftDetectionGate;
