/**
 * @internal
 * consistency/check.mjs — composer: parse every task artifact once, run every
 * applicable rule, aggregate into a single envelope (C15 subtask 5).
 *
 *   checkConsistency({taskDir, stage, ruleIds?}) -> {
 *     status: 'pass' | 'fail',     // fail iff any error-severity rule failed
 *     stage,
 *     errors: N,
 *     warnings: M,
 *     rules: [{id, severity, status, drift, reason}]
 *   }
 *
 * Missing artifact files produce empty-shaped parsed payloads + a `present:
 * false` flag so individual rules can decide whether to skip.
 */

import { createHash } from 'node:crypto'
import { existsSync, readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'

import { createCoordinationStore } from '../coordination/store.mjs'
import { parsePlan } from './parse-plan.mjs'
import { parsePlanApproval } from './parse-plan-approval.mjs'
import { parseBuildProgress } from './parse-progress.mjs'
import { parseQaSignoff } from './parse-qa.mjs'
import { parseReview } from './parse-review.mjs'
import { parseSpec } from './parse-spec.mjs'
import { listRules } from './registry.mjs'

// Eagerly populate the registry.
import './rules/index.mjs'

/**
 * @param {{taskDir: string, stage?: string, ruleIds?: string[]}} args
 */
export async function checkConsistency({ taskDir, stage = 'ship', ruleIds } = {}) {
	if (!taskDir || typeof taskDir !== 'string') {
		throw new Error('checkConsistency: taskDir is required')
	}
	const parsed = parseTaskArtifacts(taskDir)

	let rules = listRules({ stage })
	if (Array.isArray(ruleIds) && ruleIds.length > 0) {
		const wanted = new Set(
			ruleIds.map((r) => (typeof r === 'string' ? r.toLowerCase().trim() : '')).filter(Boolean),
		)
		rules = rules.filter((r) => {
			// allow matching by full id ('r1-ac-coverage') or short form ('r1').
			if (wanted.has(r.id.toLowerCase())) return true
			const short = r.id.toLowerCase().match(/^(r\d+)/)?.[1]
			return short ? wanted.has(short) : false
		})
	}

	const results = []
	for (const rule of rules) {
		let verdict
		try {
			verdict = await rule.check({ taskDir, parsed })
		} catch (err) {
			verdict = {
				status: 'fail',
				drift: [],
				reason: `rule threw: ${err?.message ?? String(err)}`,
			}
		}
		const status = verdict?.status ?? 'skip'
		const drift = Array.isArray(verdict?.drift) ? verdict.drift : []
		const reason = typeof verdict?.reason === 'string' ? verdict.reason : ''
		results.push({ id: rule.id, severity: rule.severity, status, drift, reason })
	}

	let errors = 0
	let warnings = 0
	for (const r of results) {
		if (r.status !== 'fail') continue
		if (r.severity === 'error') errors++
		else warnings++
	}
	const status = errors > 0 ? 'fail' : 'pass'
	return { status, stage, errors, warnings, rules: results }
}

function parseTaskArtifacts(taskDir) {
	const specPath = join(taskDir, 'spec.md')
	const planPath = join(taskDir, 'implementation_plan.json')
	const reviewPath = join(taskDir, 'review.json')
	const qaPath = join(taskDir, 'qa_signoff.json')
	const progressPath = join(taskDir, 'build-progress.json')
	const approvalPath = join(taskDir, 'plan_approval.json')

	const specMd = safeReadText(specPath)
	const plan = safeReadJson(planPath)
	const review = safeReadJson(reviewPath)
	const qa = safeReadJson(qaPath)
	const progress = safeReadJson(progressPath)
	const planApproval = safeReadJson(approvalPath)

	const specParsed = parseSpec(specMd ?? '')

	// C21 — surface the task record so rules (notably r6-plan-approval) can
	// branch on task.execution_mode without re-reading state.json. We resolve
	// the project root by walking up from the task dir (`.aperant/tasks/{id}`).
	const projectRoot = dirname(dirname(dirname(taskDir)))
	const taskRecord = readTaskRecord(projectRoot, taskDir)

	// Hashes used by r6 drift detection.
	const planHash = sha256OfBytes(planPath)
	const specHash = sha256OfBytes(specPath)

	return {
		spec: {
			...specParsed,
			rawLower: typeof specMd === 'string' ? specMd.toLowerCase() : '',
			present: specMd !== null,
			sha256: specHash,
		},
		plan: {
			...parsePlan(plan),
			present: plan !== null,
			sha256: planHash,
		},
		review: {
			...parseReview(review),
			present: review !== null,
		},
		qa: {
			...parseQaSignoff(qa),
			present: qa !== null,
		},
		progress: {
			...parseBuildProgress(progress),
			present: progress !== null,
		},
		plan_approval: {
			...parsePlanApproval(planApproval),
			present: planApproval !== null,
		},
		task: taskRecord,
	}
}

function readTaskRecord(projectRoot, taskDir) {
	// `.aperant/tasks/{id}` → derive id from final segment.
	const segments = taskDir.split(/[\\/]/).filter(Boolean)
	const taskId = segments[segments.length - 1] || ''
	// CLAUDE.md C31 R8 — go through CoordinationStore for state reads instead of
	// hand-rolling readFileSync(state.json). The store handles shared-root
	// resolution (worktree-aware) and consistent error handling.
	const state = createCoordinationStore(projectRoot).readState()
	if (!state) return { id: taskId, present: false, execution_mode: null }
	const record = state?.active_tasks?.[taskId] ?? null
	if (!record) return { id: taskId, present: false, execution_mode: null }
	return {
		id: taskId,
		present: true,
		execution_mode: typeof record.execution_mode === 'string' ? record.execution_mode : null,
		lifecycle_phase: typeof record.lifecycle_phase === 'string' ? record.lifecycle_phase : null,
	}
}

function sha256OfBytes(p) {
	if (!existsSync(p)) return null
	try {
		const buf = readFileSync(p)
		return createHash('sha256').update(buf).digest('hex')
	} catch {
		return null
	}
}

function safeReadText(p) {
	if (!existsSync(p)) return null
	try {
		return readFileSync(p, 'utf-8')
	} catch {
		return null
	}
}

function safeReadJson(p) {
	const text = safeReadText(p)
	if (text === null) return null
	try {
		return JSON.parse(text)
	} catch {
		return null
	}
}
