/**
 * commands/coverage-check.mjs — plan-to-spec verification coverage validator (C29).
 *
 * v0.8.6 adds a second gate: `## User Outcomes` parsing + gate (see
 * coverage-check/user-outcomes.mjs). The new gate is ADDITIVE — it does
 * NOT change the existing AC parsing branch and does NOT block when
 * only Acceptance Criteria are present (outcomes and ACs are
 * independent concerns per spec §ID-01). QUICK-track plans
 * (`complexity: "simple"` OR no `vertical_slice_schema_version`) skip
 * the User Outcomes branch entirely per Fast Path Guarantee (ID-08).
 */

import { existsSync, readFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { parseAcceptanceCriteria, resolveSpecPath } from '../config/coverage-ac.mjs'
import { parseUserOutcomes, userOutcomesGateResult } from '../coverage-check/user-outcomes.mjs'
import { err, ok } from '../util/result.mjs'

export function cmdCoverageCheck(subcommand, projectDir, extraArgs) {
	if (!subcommand)
		return err('Usage: apt-tools coverage-check <validate> <project-dir> [--plan <path>]')

	switch (subcommand) {
		case 'validate': {
			if (!projectDir)
				return err(
					'Usage: apt-tools coverage-check validate <project-dir> [--plan <path>] [--spec <path>]',
				)
			const targetDir = resolve(projectDir)

			// Determine plan path
			const planIdx = extraArgs.indexOf('--plan')
			const planPath =
				planIdx >= 0 && extraArgs[planIdx + 1]
					? resolve(extraArgs[planIdx + 1])
					: join(targetDir, 'implementation_plan.json')

			// Determine explicit --spec flag (optional)
			const specFlagIdx = extraArgs.indexOf('--spec')
			const explicitSpecFlag =
				specFlagIdx >= 0 && extraArgs[specFlagIdx + 1] ? extraArgs[specFlagIdx + 1] : null

			// Read and validate plan JSON (T-11-05)
			if (!existsSync(planPath)) return err(`Plan file not found: ${planPath}`)
			let plan
			try {
				plan = JSON.parse(readFileSync(planPath, 'utf-8'))
			} catch (e) {
				return err(`Invalid JSON in plan file: ${e.message}`)
			}

			if (!plan.subtasks || !Array.isArray(plan.subtasks)) {
				return err('Plan must contain a "subtasks" array')
			}

			// Collect acceptance criteria from multiple sources
			const criteria = []

			// Source 1: spec.md — resolved via precedence helper
			const resolvedSpec = resolveSpecPath(targetDir, planPath, explicitSpecFlag)
			const sourcesChecked = []
			if (resolvedSpec.path) {
				const specContent = readFileSync(resolvedSpec.path, 'utf-8')
				const parsed = parseAcceptanceCriteria(specContent)
				for (const c of parsed) {
					if (!criteria.includes(c)) criteria.push(c)
				}
				// Record each candidate that was checked, annotating the one
				// we actually read with its criteria_count.
				for (const entry of resolvedSpec.checked) {
					sourcesChecked.push({
						path: entry.path,
						present: entry.present,
						source: entry.source,
						criteria_count: entry.path === resolvedSpec.path ? parsed.length : 0,
					})
				}
			} else {
				for (const entry of resolvedSpec.checked) {
					sourcesChecked.push({
						path: entry.path,
						present: entry.present,
						source: entry.source,
						criteria_count: 0,
					})
				}
			}

			// Source 2: plan-level acceptance_criteria
			let planLevelCount = 0
			if (Array.isArray(plan.acceptance_criteria)) {
				for (const c of plan.acceptance_criteria) {
					if (!criteria.includes(c)) {
						criteria.push(c)
						planLevelCount++
					}
				}
			}
			sourcesChecked.push({
				path: planPath,
				present: true,
				source: 'plan.acceptance_criteria',
				criteria_count: planLevelCount,
			})

			// Source 3: per-subtask acceptance_criteria
			let subtaskLevelCount = 0
			for (const st of plan.subtasks) {
				if (Array.isArray(st.acceptance_criteria)) {
					for (const c of st.acceptance_criteria) {
						if (!criteria.includes(c)) {
							criteria.push(c)
							subtaskLevelCount++
						}
					}
				}
			}
			sourcesChecked.push({
				path: planPath,
				present: true,
				source: 'plan.subtasks[].acceptance_criteria',
				criteria_count: subtaskLevelCount,
			})

			// Read autonomy level from config (needed early for zero-criteria branch)
			let autonomyLevelEarly = 1
			const configPathEarly = join(targetDir, '.aperant', 'config.json')
			if (existsSync(configPathEarly)) {
				try {
					const cfg = JSON.parse(readFileSync(configPathEarly, 'utf-8'))
					autonomyLevelEarly = cfg?.autonomy?.default ?? 1
				} catch {
					/* use default */
				}
			}

			// Build the User Outcomes payload — additive, ID-08 QUICK-exempt.
			// Plans with `complexity: "simple"` OR no
			// `vertical_slice_schema_version` are QUICK-track per the Fast
			// Path Guarantee — skip the gate entirely (the payload's
			// `user_outcomes` key is OMITTED).
			const isQuickTrack = plan.complexity === 'simple' || !plan.vertical_slice_schema_version
			let userOutcomesPayload = null
			if (!isQuickTrack && resolvedSpec.path) {
				const specContentForOutcomes = readFileSync(resolvedSpec.path, 'utf-8')
				const parsedOutcomes = parseUserOutcomes(specContentForOutcomes)
				const frameworkVersion = readFrameworkVersion()
				const gateResult = userOutcomesGateResult(parsedOutcomes, frameworkVersion)
				userOutcomesPayload = {
					header_present: parsedOutcomes.header_present,
					body_kind: parsedOutcomes.body_kind,
					outcomes_count: parsedOutcomes.outcomes.length,
					gate_result: gateResult,
				}
				if (parsedOutcomes.epic) userOutcomesPayload.epic = parsedOutcomes.epic
				if (parsedOutcomes.validation_warnings && parsedOutcomes.validation_warnings.length > 0) {
					userOutcomesPayload.validation_warnings = parsedOutcomes.validation_warnings
				}
				if (gateResult === 'warn') {
					userOutcomesPayload.migration_deadline =
						'## User Outcomes section becomes a hard block in v0.9.0 — add it now, or use the empty-with-note escape (`_No user-observable changes — pure refactor._`) for refactor-only specs.'
				}
			}

			// Zero-criteria guard — do NOT silently report ok when there is
			// nothing to verify. This is the motivating bug: zero criteria
			// trivially pass unmapped.length === 0, which produced a false
			// green signal. Treat it as incomplete with an explicit reason.
			if (criteria.length === 0) {
				return ok(
					withUserOutcomes(
						{
							status: 'incomplete',
							command: 'coverage-validate',
							criteria_count: 0,
							mapped_count: 0,
							unmapped_criteria: [],
							coverage: [],
							subtask_verifications: plan.subtasks.map((st) => ({
								id: st.id,
								title: st.title || st.id,
								verification: st.verification || '',
								acceptance_criteria: st.acceptance_criteria || [],
							})),
							reason: 'no-criteria-found',
							suggestion:
								'Add acceptance criteria to spec.md under ## Acceptance Criteria, or add plan.acceptance_criteria to implementation_plan.json',
							sources_checked: sourcesChecked,
							spec_path: resolvedSpec.path,
							autonomy_action: autonomyLevelEarly <= 1 ? 'stop-and-ask' : 'generate-stubs',
						},
						userOutcomesPayload,
					),
				)
			}

			// Classify verification methods for each subtask (D-19)
			function classifyVerification(verification) {
				if (!verification || verification.trim() === '') return 'unmapped'
				const v = verification.toLowerCase()
				if (v.includes('test') || v.includes('spec')) {
					if (v.includes('integration') || v.includes('e2e')) return 'integration-test'
					return 'unit-test'
				}
				if (v.includes('curl') || v.includes('http') || v.includes('fetch')) return 'api-response'
				if (
					v.includes('echo') ||
					v.includes('grep') ||
					v.includes('node ') ||
					v.includes('pnpm ') ||
					v.includes('npm ') ||
					v.includes('npx ')
				)
					return 'cli-output'
				// If it looks like a command (contains spaces and no sentence-like structure), it's cli-output
				if (v.includes(' ') && !v.includes('.') && v.length < 100) return 'cli-output'
				return 'manual'
			}

			const subtaskVerifications = plan.subtasks.map((st) => ({
				id: st.id,
				title: st.title || st.id,
				verification: st.verification || '',
				type: classifyVerification(st.verification),
				acceptance_criteria: st.acceptance_criteria || [],
			}))

			// Build coverage mapping: for each criterion, find subtasks that cover it
			const coverage = criteria.map((criterion) => {
				const criterionLower = criterion.toLowerCase()
				const mappedSubtasks = subtaskVerifications.filter((sv) => {
					// Check if subtask's acceptance_criteria mention this criterion
					if (sv.acceptance_criteria.some((ac) => ac.toLowerCase() === criterionLower)) return true
					// Check if subtask title relates to criterion (fuzzy match: shared words)
					const criterionWords = criterionLower.split(/\s+/).filter((w) => w.length > 3)
					const titleLower = sv.title.toLowerCase()
					const matchCount = criterionWords.filter((w) => titleLower.includes(w)).length
					if (matchCount >= 2 || (criterionWords.length <= 2 && matchCount >= 1)) return true
					return false
				})

				return {
					criterion,
					mapped: mappedSubtasks.length > 0,
					subtasks: mappedSubtasks.map((sv) => sv.id),
					verification_types: [...new Set(mappedSubtasks.map((sv) => sv.type))],
				}
			})

			const unmapped = coverage.filter((c) => !c.mapped)
			const mappedCount = coverage.length - unmapped.length

			// Read autonomy level from config
			let autonomyLevel = 1
			const configPath = join(targetDir, '.aperant', 'config.json')
			if (existsSync(configPath)) {
				try {
					const config = JSON.parse(readFileSync(configPath, 'utf-8'))
					autonomyLevel = config?.autonomy?.default ?? 1
				} catch {
					/* use default */
				}
			}

			if (unmapped.length > 0) {
				return ok(
					withUserOutcomes(
						{
							status: 'incomplete',
							command: 'coverage-validate',
							criteria_count: criteria.length,
							mapped_count: mappedCount,
							unmapped_criteria: unmapped.map((u) => u.criterion),
							coverage,
							subtask_verifications: subtaskVerifications,
							suggestion: 'generate test stubs',
							autonomy_action: autonomyLevel <= 1 ? 'stop-and-ask' : 'generate-stubs',
							spec_path: resolvedSpec.path,
							sources_checked: sourcesChecked,
						},
						userOutcomesPayload,
					),
				)
			}
			return ok(
				withUserOutcomes(
					{
						status: 'ok',
						command: 'coverage-validate',
						criteria_count: criteria.length,
						mapped_count: mappedCount,
						coverage,
						subtask_verifications: subtaskVerifications,
						spec_path: resolvedSpec.path,
						sources_checked: sourcesChecked,
					},
					userOutcomesPayload,
				),
			)
		}
		default:
			return err(`Unknown coverage-check subcommand: ${subcommand}. Available: validate`)
	}
}

/**
 * Attach the `user_outcomes` payload to a coverage-check envelope when
 * present (i.e. STANDARD/DEEP plan with a spec on disk). QUICK plans
 * pass `null` and the key is OMITTED from the envelope entirely — Fast
 * Path Guarantee (ID-08).
 */
function withUserOutcomes(envelope, userOutcomesPayload) {
	if (userOutcomesPayload === null) return envelope
	return { ...envelope, user_outcomes: userOutcomesPayload }
}

/**
 * Read the running kernel's framework version from its own package.json.
 * Used to gate the `## User Outcomes` missing-header behavior — warn in
 * v0.8.x, hard-block from v0.9.0 forward. Returns `'0.0.0'` on failure
 * so the gate fails-soft to warn (callers treat unknown version as
 * pre-0.9.0).
 */
function readFrameworkVersion() {
	try {
		const here = dirname(fileURLToPath(import.meta.url))
		const pkgPath = resolve(here, '..', '..', '..', 'package.json')
		const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
		return typeof pkg.version === 'string' ? pkg.version : '0.0.0'
	} catch {
		return '0.0.0'
	}
}
