/**
 * commands/design-lint.mjs — `apt-tools design-lint [paths]` (C11, Wave 3).
 *
 * Reads DESIGN.md (default: ./DESIGN.md) for the antiPatterns catalog,
 * then greps the supplied HTML/CSS files for matches.  Used by:
 *
 *   - `/apt:mockup` as a post-generation self-check
 *   - `/apt:verify-proof` as a pre-check for the proof report
 *   - CI (optional), by pointing it at generated output dirs
 *
 * Usage:
 *
 *   apt-tools design-lint foo.html bar.html
 *   apt-tools design-lint '.aperant/tasks/<task-id>/mockups/mockup_final.html'
 *   apt-tools design-lint --design-file ./DESIGN.md foo.html
 *
 * Wildcard expansion: the shell usually handles globs; when the shell
 * passes a literal pattern through (single-quoted), we fall back to a
 * minimal in-process expansion via readdir + test.  Not a full glob
 * implementation — just enough for the common `*.html` case under a
 * single directory.
 *
 * Output envelope (stdout):
 *
 *   {
 *     status: "ok" | "error",
 *     command: "design-lint",
 *     design_file: "<abs path>",
 *     files_scanned: 3,
 *     violations: [{ file, line, column, rule_id, category, match, why, rewrite }, ...],
 *     warnings: [string, ...],
 *     summary: "N violation(s) across M file(s) ..."
 *   }
 *
 * Exits non-zero when `violations.length > 0`.
 */

import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { basename, dirname, isAbsolute, resolve as resolvePath } from 'node:path'
import { lintAntiPatterns, summarize } from '../design/lint.mjs'
import { extractDesignFrontmatter } from '../design/validate.mjs'
import { parseFlags } from '../util/args.mjs'
import { err, exitWith, ok } from '../util/result.mjs'

/**
 * Minimal glob-lite: supports `dir/*.ext` — expands to all files in `dir`
 * matching the extension.  Anything else is returned as-is so callers can
 * feed explicit paths through.  Intentionally small; shells handle most
 * globs before the command ever sees them.
 */
function expandSimpleGlob(spec) {
	if (typeof spec !== 'string' || spec.length === 0) return [spec]
	const starIdx = spec.indexOf('*')
	if (starIdx === -1) return [spec]
	const dir = dirname(spec)
	const base = basename(spec)
	// Only handle a single `*` prefix pattern for now (`*.html`, `mockup-*.html`).
	if (!/^(?:[^*]*)\*(?:[^*]*)$/.test(base)) return [spec]
	if (!existsSync(dir)) return []
	const starInBase = base.indexOf('*')
	const prefix = base.slice(0, starInBase)
	const suffix = base.slice(starInBase + 1)
	let entries
	try {
		entries = readdirSync(dir)
	} catch {
		return []
	}
	return entries
		.filter((name) => name.startsWith(prefix) && name.endsWith(suffix))
		.map((name) => resolvePath(dir, name))
}

/**
 * Turn a list of positional file args into a deduped absolute-path list.
 * Directories are expanded one level (non-recursive) for .html and .css.
 */
function resolveFiles(positional) {
	const out = []
	const seen = new Set()
	for (const arg of positional) {
		const abs = isAbsolute(arg) ? arg : resolvePath(process.cwd(), arg)
		const expanded = arg.includes('*') ? expandSimpleGlob(abs) : [abs]
		for (const p of expanded) {
			if (!p || seen.has(p)) continue
			if (!existsSync(p)) {
				seen.add(p)
				out.push({ path: p, missing: true })
				continue
			}
			const st = statSync(p)
			if (st.isDirectory()) {
				// Shallow dir expansion: .html / .css children
				const names = readdirSync(p).filter((n) => n.endsWith('.html') || n.endsWith('.css'))
				for (const n of names) {
					const child = resolvePath(p, n)
					if (!seen.has(child)) {
						seen.add(child)
						out.push({ path: child, missing: false })
					}
				}
				continue
			}
			if (!seen.has(p)) {
				seen.add(p)
				out.push({ path: p, missing: false })
			}
		}
	}
	return out
}

/**
 * Split a mixed flag-and-positional arg list into { flags, positionals }.
 * Mirrors the parseFlags rule: a non-flag token immediately following a
 * `--flag` is consumed as that flag's value, not as a positional.  Needed
 * here because positional file paths can sit on either side of the flags.
 *
 * @param {string[]} args
 */
function splitArgs(args) {
	const flags = parseFlags(args)
	const positionals = []
	for (let i = 0; i < args.length; i++) {
		const a = args[i]
		if (typeof a !== 'string') continue
		if (a.startsWith('--')) {
			const next = args[i + 1]
			if (next !== undefined && typeof next === 'string' && !next.startsWith('--')) {
				i++ // skip value
			}
			continue
		}
		positionals.push(a)
	}
	return { flags, positionals }
}

export function cmdDesignLint(firstArg, extraArgs) {
	const all = firstArg === undefined ? [] : [firstArg, ...(extraArgs ?? [])]
	const { flags, positionals: positional } = splitArgs(all)

	const designFileFlag = flags.get('design-file')
	const designFile =
		typeof designFileFlag === 'string' && designFileFlag.length > 0
			? resolvePath(designFileFlag)
			: resolvePath(process.cwd(), 'DESIGN.md')

	if (!existsSync(designFile)) {
		return err(
			`design-lint: DESIGN.md not found at ${designFile}. Run \`/apt:design --new\` first, or pass --design-file <path>.`,
		)
	}

	let designText
	try {
		designText = readFileSync(designFile, 'utf8')
	} catch (e) {
		const msg = e instanceof Error ? e.message : String(e)
		return err(`design-lint: failed to read ${designFile}: ${msg}`)
	}

	const frontmatter = extractDesignFrontmatter(designText)
	if (!frontmatter) {
		return err(`design-lint: ${designFile} has no parseable YAML frontmatter`)
	}

	const rules = Array.isArray(frontmatter.antiPatterns) ? frontmatter.antiPatterns : []

	if (positional.length === 0) {
		return err(
			'Usage: apt-tools design-lint [--design-file <path>] <file> [<file>...]\nPass at least one HTML/CSS file or directory to lint.',
		)
	}

	const resolved = resolveFiles(positional)
	const missing = resolved.filter((r) => r.missing).map((r) => r.path)
	const existing = resolved.filter((r) => !r.missing)

	const fileEntries = existing.map((r) => {
		try {
			return { file: r.path, text: readFileSync(r.path, 'utf8') }
		} catch (e) {
			const msg = e instanceof Error ? e.message : String(e)
			return { file: r.path, text: '', readError: msg }
		}
	})

	const result = lintAntiPatterns({ rules, files: fileEntries })

	const warnings = [...result.warnings]
	for (const m of missing) {
		warnings.push(`file not found: ${m}`)
	}
	for (const f of fileEntries) {
		if (f.readError) warnings.push(`failed to read ${f.file}: ${f.readError}`)
	}

	const hasViolations = result.violations.length > 0
	const envelope = {
		status: hasViolations ? 'error' : 'ok',
		command: 'design-lint',
		design_file: designFile,
		files_scanned: result.files_scanned,
		rules_compiled: result.rules_compiled,
		rules_skipped: result.rules_skipped,
		violations: result.violations,
		warnings,
		summary: summarize(result),
	}
	return hasViolations ? exitWith(envelope, 1) : ok(envelope)
}
