/**
 * @internal
 * G9 tdd-iron-law — opt-in blocking gate for `apt:verify` (C3).
 *
 * When `config.tdd.iron_law === true`, this gate audits the git history for
 * every completed subtask in `implementation_plan.json` and asserts the
 * red-green-refactor commit order:
 *
 *   1. A commit whose subject starts with `test(` or `test:` touched the
 *      subtask's test file(s) first.
 *   2. A commit whose subject does NOT start with `test(`/`test:` touched
 *      at least one non-test implementation file in `subtask.files` after.
 *
 * When the flag is off/absent the gate returns `skip`. When a subtask has no
 * detectable test file (`*.test.*` / `*.spec.*`), the gate emits a `warn`
 * for that subtask rather than `fail` — this prevents false positives on
 * docs-only or config-only subtasks.
 */

import { execFile } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { promisify } from 'node:util'
import { registerGate } from '../registry.mjs'

const execFileP = promisify(execFile)

const TEST_FILE_RE = /\.(test|spec)\.[tj]sx?$/
const TEST_SUBJECT_RE = /^test[:(]/

/** Parse `git log --format=%H%x09%cI%x09%s` output into structured rows. */
function parseLog(stdout) {
	if (!stdout) return []
	return stdout
		.split('\n')
		.map((l) => l.trim())
		.filter(Boolean)
		.map((line) => {
			const parts = line.split('\t')
			return {
				sha: parts[0] || '',
				time: parts[1] || '',
				subject: parts.slice(2).join('\t'),
			}
		})
}

/**
 * Run `git log --follow --format=%H\t%cI\t%s -- <file>` in a repo dir.
 * Returns {sha, time, subject}[] newest-first, or [] on any failure.
 */
async function gitLogForFile(cwd, file, sinceIso) {
	try {
		const args = ['log', '--follow', '--format=%H%x09%cI%x09%s']
		if (sinceIso) args.push(`--since=${sinceIso}`)
		args.push('--', file)
		const { stdout } = await execFileP('git', args, { cwd, maxBuffer: 4 * 1024 * 1024 })
		return parseLog(stdout)
	} catch {
		return []
	}
}

/** Split subtask.files into test vs impl file lists. */
function partitionFiles(files) {
	const testFiles = []
	const implFiles = []
	for (const f of Array.isArray(files) ? files : []) {
		if (typeof f !== 'string') continue
		if (TEST_FILE_RE.test(f)) testFiles.push(f)
		else implFiles.push(f)
	}
	return { testFiles, implFiles }
}

/** Audit a single completed subtask; returns {status, reason, subtaskId}. */
async function auditSubtask(cwd, subtask, sinceIso) {
	const id = subtask?.id ?? '?'
	const { testFiles, implFiles } = partitionFiles(subtask?.files)

	if (testFiles.length === 0) {
		return {
			status: 'warn',
			reason: `no test file detected for subtask ${id}`,
			subtaskId: id,
		}
	}

	// Find the OLDEST test-prefixed commit touching ANY of the test files.
	let oldestTestCommit = null // {sha, time}
	for (const tf of testFiles) {
		const log = await gitLogForFile(cwd, tf, sinceIso)
		for (const c of log) {
			if (!TEST_SUBJECT_RE.test(c.subject)) continue
			if (!oldestTestCommit || (c.time && c.time < oldestTestCommit.time)) {
				oldestTestCommit = c
			}
		}
	}

	if (!oldestTestCommit) {
		return {
			status: 'fail',
			reason: `subtask ${id}: no test-prefixed commit found touching ${testFiles.join(', ')}`,
			subtaskId: id,
		}
	}

	// Test-only subtask: the test commit alone is sufficient.
	if (implFiles.length === 0) {
		return {
			status: 'pass',
			reason: `subtask ${id}: test commit ${oldestTestCommit.sha.slice(0, 7)} landed (no impl files declared)`,
			subtaskId: id,
		}
	}

	// Collect every non-test commit touching any impl file.
	const implCommits = [] // {sha, time}
	for (const f of implFiles) {
		const log = await gitLogForFile(cwd, f, sinceIso)
		for (const c of log) {
			if (TEST_SUBJECT_RE.test(c.subject)) continue
			implCommits.push(c)
		}
	}

	if (implCommits.length === 0) {
		return {
			status: 'fail',
			reason: `subtask ${id}: test commit ${oldestTestCommit.sha.slice(0, 7)} present but no implementation commit found on ${implFiles.join(', ')}`,
			subtaskId: id,
		}
	}

	// Fail if ANY impl commit is older than the oldest test commit.
	for (const ic of implCommits) {
		if (ic.time && oldestTestCommit.time && ic.time < oldestTestCommit.time) {
			return {
				status: 'fail',
				reason: `subtask ${id}: implementation commit ${ic.sha.slice(0, 7)} preceded test commit ${oldestTestCommit.sha.slice(0, 7)}`,
				subtaskId: id,
			}
		}
	}

	// Need at least one impl commit AT OR AFTER the test commit.
	const laterImpl = implCommits.find(
		(ic) => !ic.time || !oldestTestCommit.time || ic.time >= oldestTestCommit.time,
	)
	if (!laterImpl) {
		return {
			status: 'fail',
			reason: `subtask ${id}: no implementation commit followed test commit ${oldestTestCommit.sha.slice(0, 7)}`,
			subtaskId: id,
		}
	}

	return {
		status: 'pass',
		reason: `subtask ${id}: test ${oldestTestCommit.sha.slice(0, 7)} precedes impl ${laterImpl.sha.slice(0, 7)}`,
		subtaskId: id,
	}
}

const gate = {
	id: 'tdd-iron-law',
	description: 'enforce test-first commit order per subtask when tdd.iron_law is enabled',
	severity: 'blocking',
	skills: ['verify'],
	async check(ctx) {
		const flag = ctx?.config?.tdd?.iron_law === true
		if (!flag) {
			return {
				status: 'skip',
				reason: 'tdd-iron-law disabled (config.tdd.iron_law !== true)',
				inputs: [],
			}
		}

		const planPath = join(ctx.taskDir, 'implementation_plan.json')
		const inputs = [planPath]
		if (!existsSync(planPath)) {
			return {
				status: 'fail',
				reason: `implementation_plan.json missing at ${planPath}`,
				inputs,
			}
		}

		let plan
		try {
			plan = JSON.parse(readFileSync(planPath, 'utf-8'))
		} catch (err) {
			return {
				status: 'fail',
				reason: `implementation_plan.json does not parse: ${err.message}`,
				inputs,
			}
		}

		const subtasks = Array.isArray(plan.subtasks) ? plan.subtasks : []
		const completed = subtasks.filter((s) => s?.status === 'completed')
		if (completed.length === 0) {
			return {
				status: 'skip',
				reason: 'no completed subtasks to audit',
				inputs,
			}
		}

		const sinceIso = ctx?.taskRecord?.created_at || null
		const cwd = ctx.sharedRoot

		const results = []
		for (const st of completed) {
			results.push(await auditSubtask(cwd, st, sinceIso))
		}

		const fails = results.filter((r) => r.status === 'fail')
		const warns = results.filter((r) => r.status === 'warn')
		const passes = results.filter((r) => r.status === 'pass')

		if (fails.length > 0) {
			return {
				status: 'fail',
				reason: `${fails.length} subtask(s) violated test-first order: ${fails.map((f) => f.reason).join('; ')}`,
				evidence: { results },
				inputs,
			}
		}
		if (warns.length > 0) {
			return {
				status: 'warn',
				reason: warns.map((w) => w.reason).join('; '),
				evidence: { results },
				inputs,
			}
		}
		return {
			status: 'pass',
			reason: `${passes.length} subtask(s) honored test-first commit order`,
			evidence: { results },
			inputs,
		}
	},
}

registerGate(gate)
export default gate
