/**
 * @internal
 * audit.mjs — docs-coherence audit orchestrator (C33).
 *
 * Walks every candidate documented surface in a project, runs
 * `findStaleClaimsInMarkdown` against it, and buckets each finding by the
 * resolved policy:
 *
 *   - `auto-update` → enter `patch_plan` (reviewer applies in fix iterations 1-2)
 *   - `flag-drift`  → enter `findings` (surfaced only, never auto-edited)
 *   - `skip` or unmatched → dropped
 *
 * Surface discovery is driven by the policy map (every key becomes a
 * candidate) plus glob-walking the markdown surfaces that commonly appear:
 *   - packages/*\/README.md
 *   - docs/**\/*.md
 *   - root-level CLAUDE.md / AGENTS.md / README.md
 *
 * The module does not touch git — callers pass in `diffFiles` (for per-surface
 * stub creation) and the structured `diffFacts` shape. `apt-tools docs-audit`
 * is the CLI wrapper that actually computes `diffFacts` from git.
 */

import { existsSync, readdirSync, statSync } from 'node:fs'
import { join, relative, resolve } from 'node:path'
import { matchSurfaceToPolicy } from './policy.mjs'

/**
 * Run the docs-coherence audit.
 *
 * @param {object} opts
 * @param {string} opts.projectDir — absolute project root
 * @param {Array<string>} [opts.diffFiles] — files changed by the task
 * @param {object} [opts.diffFacts] — { deletedPaths, renamedPaths, addedExports, removedExports }
 * @param {object} opts.policy — resolved via loadDocsPolicy(projectDir)
 * @param {Array<string>} [opts.surfaces] — explicit surface list (testing hook)
 *
 * @returns {{findings: Array, patch_plan: Array, dropped: Array}}
 */
export async function auditDocsDrift(opts = {}) {
	const { findStaleClaimsInMarkdown } = await import('./grep-claims.mjs')
	const projectDir = opts.projectDir ? resolve(opts.projectDir) : process.cwd()
	const policy = opts.policy || {}
	const diffFacts = {
		deletedPaths: Array.isArray(opts.diffFacts?.deletedPaths) ? opts.diffFacts.deletedPaths : [],
		renamedPaths: Array.isArray(opts.diffFacts?.renamedPaths) ? opts.diffFacts.renamedPaths : [],
		addedExports: Array.isArray(opts.diffFacts?.addedExports) ? opts.diffFacts.addedExports : [],
		removedExports: Array.isArray(opts.diffFacts?.removedExports)
			? opts.diffFacts.removedExports
			: [],
	}

	const findings = []
	const patch_plan = []
	const dropped = []

	// Test-time shortcut: caller provides an explicit surface list.
	const surfaces = Array.isArray(opts.surfaces)
		? opts.surfaces
		: discoverDocumentedSurfaces(projectDir, policy)

	// Dedup in case discovery emitted the same path twice.
	const seen = new Set()
	for (const relPath of surfaces) {
		if (seen.has(relPath)) continue
		seen.add(relPath)
		const abs = resolve(projectDir, relPath)
		if (!existsSync(abs)) continue
		const policyValue = matchSurfaceToPolicy(relPath, policy)
		if (policyValue === 'skip' || policyValue === null) {
			// Scan nothing for skipped surfaces; record so callers can report "seen but skipped".
			dropped.push({ file: relPath, policy: policyValue })
			continue
		}
		const hits = findStaleClaimsInMarkdown(abs, diffFacts)
		for (const hit of hits) {
			// Normalize file to project-relative.
			const relFile = relative(projectDir, hit.file)
			const entry = {
				...hit,
				file: relFile,
				category: 'docs-drift',
				severity: policyValue === 'flag-drift' ? 'warning' : 'info',
				policy: policyValue,
			}
			if (policyValue === 'auto-update') {
				patch_plan.push(entry)
			} else {
				findings.push(entry)
			}
		}
	}

	return { findings, patch_plan, dropped }
}

/**
 * Enumerate documented surfaces to scan. Union of:
 *   - every literal key in `policy` (CLAUDE.md, etc)
 *   - every `packages/*\/README.md` that exists
 *   - every `*.md` under `docs/`
 *   - root README.md (if present)
 *
 * Returns project-relative POSIX-separator paths. Duplicates ok — caller dedups.
 */
function discoverDocumentedSurfaces(projectDir, policy) {
	const out = []
	// Literal keys in the policy are the primary surface hints.
	for (const key of Object.keys(policy)) {
		if (!key.includes('*')) out.push(key)
	}
	// Root markdowns commonly authored as claims.
	for (const name of ['CLAUDE.md', 'AGENTS.md', 'README.md']) {
		if (existsSync(join(projectDir, name))) out.push(name)
	}
	// packages/*/README.md
	const packagesDir = join(projectDir, 'packages')
	if (existsSync(packagesDir)) {
		for (const entry of safeReaddir(packagesDir)) {
			const pkgReadme = join(packagesDir, entry, 'README.md')
			if (existsSync(pkgReadme)) out.push(`packages/${entry}/README.md`)
		}
	}
	// docs/**/*.md — bounded-depth walk, 5 levels max to avoid runaway recursion.
	const docsDir = join(projectDir, 'docs')
	if (existsSync(docsDir)) {
		walkMarkdown(docsDir, projectDir, 0, 5, out)
	}
	return out
}

function safeReaddir(dir) {
	try {
		return readdirSync(dir)
	} catch {
		return []
	}
}

function walkMarkdown(dir, projectDir, depth, maxDepth, out) {
	if (depth > maxDepth) return
	for (const entry of safeReaddir(dir)) {
		const abs = join(dir, entry)
		let st
		try {
			st = statSync(abs)
		} catch {
			continue
		}
		if (st.isDirectory()) {
			walkMarkdown(abs, projectDir, depth + 1, maxDepth, out)
		} else if (st.isFile() && /\.md$/i.test(entry)) {
			out.push(relative(projectDir, abs).replace(/\\/g, '/'))
		}
	}
}
