/**
 * copy.mjs — filesystem copy + instruction-file injection helpers (C29).
 *
 * `copyDirRecursive` is a pure leaf. `injectInstructionFile` owns the
 * CLAUDE.md / AGENTS.md "<!-- APT:framework-* -->" section write path.
 *
 * The APT section TEXT is no longer assembled here — it is generated by
 * `util/aperant-section.mjs#buildAptSection` and passed in (FRAMEWORK-BUG-023).
 * Callers compute the section once (from the filesystem skill catalog) and
 * hand it to this helper. The defensive foreign-row check stays in this
 * file so the read/compare/write happens atomically against `filePath`.
 */

import {
	copyFileSync,
	existsSync,
	mkdirSync,
	readdirSync,
	readFileSync,
	writeFileSync,
} from 'node:fs'
import { join } from 'node:path'
import { APT_SECTION_RE, extractAptSectionRowIds } from './aperant-section.mjs'

/**
 * Return the row IDs present in `existing` that are NOT present in
 * `aptSection`. An empty array means no foreign rows (safe to overwrite).
 *
 * @param {string} existing   Current on-disk file contents.
 * @param {string} aptSection Output of buildAptSection().
 * @returns {string[]}
 */
function computeForeignRowIds(existing, aptSection) {
	const existingIds = new Set(extractAptSectionRowIds(existing))
	const generatedIds = new Set(extractAptSectionRowIds(aptSection))
	return [...existingIds].filter((id) => !generatedIds.has(id))
}

export function copyDirRecursive(src, dest) {
	mkdirSync(dest, { recursive: true })
	for (const entry of readdirSync(src, { withFileTypes: true })) {
		const srcPath = join(src, entry.name)
		const destPath = join(dest, entry.name)
		if (entry.isDirectory()) {
			copyDirRecursive(srcPath, destPath)
		} else {
			copyFileSync(srcPath, destPath)
		}
	}
}

/**
 * Preflight-only sibling of `injectInstructionFile`. Runs the same
 * foreign-row defensive check against `filePath` but NEVER writes — used by
 * the multi-target init flow to check every instruction target up-front so a
 * single aborting target leaves NO file partially written (US-03 / AC3).
 *
 * Returns the same status vocabulary as `injectInstructionFile` minus the
 * write side-effect: `'created'` (file absent → would be appended-to),
 * `'updated'` (markers present, no foreign rows → would be replaced),
 * `'aborted-foreign-rows'` (markers present with rows not in the generated
 * set → init must abort). `force` short-circuits the foreign-row gate exactly
 * as the writer does.
 *
 * @param {string} filePath
 * @param {Object} opts
 * @param {string} opts.aptSection   Output of buildAptSection().
 * @param {boolean} [opts.force=false]
 *                                    Bypass the foreign-row check.
 * @returns {{ status: 'created' | 'updated' | 'aborted-foreign-rows', foreignRowIds?: string[] }}
 */
export function checkInstructionFile(filePath, { aptSection, force = false }) {
	const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : ''

	if (existing.includes('<!-- APT:framework-start -->')) {
		if (!force) {
			const foreignRowIds = computeForeignRowIds(existing, aptSection)
			if (foreignRowIds.length > 0) {
				return { status: 'aborted-foreign-rows', foreignRowIds }
			}
		}
		return { status: 'updated' }
	}

	return { status: 'created' }
}

/**
 * Write the (already-generated) APT section into `filePath`. Creates the
 * file if missing; replaces the section in-place if the markers are
 * present.
 *
 * FRAMEWORK-BUG-023 — before overwriting an existing section, extract the
 * row IDs in the on-disk file and compare to the row IDs in the freshly
 * generated section. If the file contains rows that are NOT in the
 * generated set ("foreign rows"), the regen would silently clobber them.
 * In that case we abort and let the caller surface a structured error.
 * The `--force-overwrite-catalog` escape hatch flips `force: true`.
 *
 * @param {string} filePath
 * @param {Object} opts
 * @param {string} opts.aptSection   Output of buildAptSection() — wrapped
 *                                    in `<!-- APT:framework-* -->` markers.
 * @param {boolean} [opts.force=false]
 *                                    Bypass the foreign-row check.
 * @returns {{ status: 'created' | 'updated' | 'aborted-foreign-rows', written: boolean, foreignRowIds?: string[] }}
 */
export function injectInstructionFile(filePath, { aptSection, force = false }) {
	const existing = existsSync(filePath) ? readFileSync(filePath, 'utf-8') : ''

	if (existing.includes('<!-- APT:framework-start -->')) {
		if (!force) {
			const foreignRowIds = computeForeignRowIds(existing, aptSection)
			if (foreignRowIds.length > 0) {
				return { status: 'aborted-foreign-rows', written: false, foreignRowIds }
			}
		}
		const updated = existing.replace(APT_SECTION_RE, aptSection)
		writeFileSync(filePath, updated, 'utf-8')
		return { status: 'updated', written: true }
	}

	// Append (greenfield create — no foreign-row concern).
	const separator = existing.length > 0 ? '\n\n' : ''
	writeFileSync(filePath, existing + separator + aptSection + '\n', 'utf-8')
	return { status: 'created', written: true }
}
