/**
 * design/template.mjs — library IO primitives for C42 `design-template` CLI.
 *
 * Pure(-ish) exports. All I/O wrapped so callers can catch structured errors.
 * No process.exit here — the CLI layer (commands/design-template.mjs) owns
 * exit-code decisions.
 *
 * Exports:
 *   - resolveLibraryDir(flag?)        → abs path; default to shipped templates/design-library
 *   - listLibraryEntries({libraryDir}) → sorted { slug, name, description, origin, source }[]
 *   - loadLibraryEntry({libraryDir, slug}) → full design_md text + parsed frontmatter + provenance
 *   - addLibraryEntry({libraryDir, slug, projectDir, force}) → copy into project-root DESIGN.md
 *   - submitLibraryEntry({candidatePath, libraryDir, slug}) → validate + copy into library
 */
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from 'node:fs';
import { basename, dirname, resolve as resolvePath } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { extractDesignFrontmatter, validateDesignMarkdown } from './validate.mjs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// src/cli/design/template.mjs → ../../../templates/design-library
// (three `..`: design/ → cli/ → src/ → framework-root/templates/design-library)
const DEFAULT_LIBRARY_DIR = resolvePath(__dirname, '..', '..', '..', 'templates', 'design-library');
/** @typedef {{ slug: string, name: string, description: string, origin: string, source: string }} LibraryEntrySummary */
/**
 * Slugs must be simple directory-name tokens (alphanumerics, hyphen, or
 * underscore). Rejecting `/`, `..`, `\\`, and leading dots prevents path
 * traversal into arbitrary filesystem locations via `show`/`add`/`submit --as`
 * — slugs are user-supplied but are composed directly into paths under
 * `<libraryDir>/<slug>/DESIGN.md`. Without this guard, a slug like
 * `../../../etc/hosts` would escape the library root on read, and a
 * `submit --as ../../../tmp/evil` could write outside the library tree.
 *
 * Kept as simple and conservative as possible: the real typeui-sh corpus uses
 * `^[a-z][a-z0-9-]*$`, the three first-party seeds match the same pattern,
 * and community submissions should too.
 *
 * @param {string} slug
 * @param {string} context  — used only in the error message (e.g. "show slug")
 * @returns {void}
 */
function assertSafeSlug(slug, context) {
    if (typeof slug !== 'string' || slug.length === 0) {
        const err = new Error(`${context}: slug must be a non-empty string`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'EINVALID_SLUG';
        throw err;
    }
    // Max length is a belt-and-braces limit; 128 is far longer than any real slug.
    if (slug.length > 128) {
        const err = new Error(`${context}: slug is longer than 128 characters — pick something shorter.`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'EINVALID_SLUG';
        throw err;
    }
    if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(slug)) {
        const err = new Error(`${context}: slug "${slug}" contains unsupported characters. Allowed: letters, digits, hyphen, underscore, dot; must start with a letter or digit. Path separators and parent-directory references are not allowed.`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'EINVALID_SLUG';
        throw err;
    }
    if (slug === '.' || slug === '..' || slug.includes('..')) {
        const err = new Error(`${context}: slug "${slug}" is not a valid directory name (path traversal components rejected).`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'EINVALID_SLUG';
        throw err;
    }
}
/**
 * Resolve the library directory. Explicit flag wins; absolute paths are used
 * as-is, relative paths resolve against cwd. When no flag is passed, fall
 * back to the shipped templates/design-library path.
 *
 * @param {string | true | undefined | null} flag
 * @returns {string}
 */
export function resolveLibraryDir(flag) {
    if (typeof flag === 'string' && flag.length > 0) {
        return resolvePath(flag);
    }
    return DEFAULT_LIBRARY_DIR;
}
/**
 * Read the provenance.yaml sidecar for a given slug. Returns an empty object
 * when missing/unparseable — callers treat `origin: null, source: null` as
 * "unattributed".
 *
 * @param {string} libraryDir
 * @param {string} slug
 * @returns {Record<string, unknown>}
 */
function readProvenance(libraryDir, slug) {
    const p = resolvePath(libraryDir, slug, 'provenance.yaml');
    if (!existsSync(p))
        return {};
    try {
        const txt = readFileSync(p, 'utf8');
        const parsed = parseYaml(txt);
        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
            return /** @type {Record<string, unknown>} */ (parsed);
        }
    }
    catch {
        return {};
    }
    return {};
}
/**
 * List every slug under the library directory, sorted alphabetically.
 * Each entry carries the slug, meta.name, meta.description, provenance.origin,
 * provenance.source. Directories missing DESIGN.md are skipped. Warnings are
 * accumulated (not thrown) so `list` never fails wholesale on one bad entry.
 *
 * @param {{ libraryDir?: string }} [opts]
 * @returns {{ entries: LibraryEntrySummary[], warnings: string[] }}
 */
export function listLibraryEntries(opts = {}) {
    const libraryDir = resolveLibraryDir(opts.libraryDir);
    const warnings = [];
    const entries = [];
    if (!existsSync(libraryDir)) {
        return {
            entries,
            warnings: [`library directory not found at ${libraryDir}`],
        };
    }
    let names;
    try {
        names = readdirSync(libraryDir);
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        return { entries, warnings: [`failed to read library directory ${libraryDir}: ${msg}`] };
    }
    for (const name of names) {
        const slugDir = resolvePath(libraryDir, name);
        let st;
        try {
            st = statSync(slugDir);
        }
        catch {
            continue;
        }
        if (!st.isDirectory())
            continue;
        const designPath = resolvePath(slugDir, 'DESIGN.md');
        if (!existsSync(designPath)) {
            warnings.push(`slug "${name}": no DESIGN.md, skipping`);
            continue;
        }
        let text;
        try {
            text = readFileSync(designPath, 'utf8');
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            warnings.push(`slug "${name}": failed to read DESIGN.md: ${msg}`);
            continue;
        }
        const fm = extractDesignFrontmatter(text);
        const meta = /** @type {Record<string, unknown>} */ ((fm && fm.meta) || {});
        const provenance = readProvenance(libraryDir, name);
        const metaName = typeof meta.name === 'string' && meta.name.length > 0 ? meta.name : name;
        const description = typeof meta.description === 'string' && meta.description.length > 0 ? meta.description : '';
        const origin = typeof provenance.origin === 'string' && provenance.origin.length > 0
            ? /** @type {string} */ (provenance.origin)
            : 'unknown';
        const source = typeof provenance.source === 'string' && provenance.source.length > 0
            ? /** @type {string} */ (provenance.source)
            : '';
        entries.push({ slug: name, name: metaName, description, origin, source });
    }
    entries.sort((a, b) => (a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0));
    return { entries, warnings };
}
/**
 * Load a single library entry by slug. Returns the raw DESIGN.md text, the
 * parsed frontmatter object, and the parsed provenance object.
 *
 * Throws (caller translates to CLI exit) when the slug is missing or the
 * DESIGN.md can't be read.
 *
 * @param {{ libraryDir?: string, slug: string }} opts
 * @returns {{ slug: string, path: string, design_md: string, frontmatter: Record<string, unknown> | null, provenance: Record<string, unknown> }}
 */
export function loadLibraryEntry(opts) {
    if (!opts || typeof opts.slug !== 'string' || opts.slug.length === 0) {
        throw new Error('loadLibraryEntry: slug is required');
    }
    assertSafeSlug(opts.slug, 'loadLibraryEntry');
    const libraryDir = resolveLibraryDir(opts.libraryDir);
    const slugDir = resolvePath(libraryDir, opts.slug);
    const designPath = resolvePath(slugDir, 'DESIGN.md');
    if (!existsSync(designPath)) {
        const err = new Error(`Library entry "${opts.slug}" not found (expected at ${designPath}). Run \`apt-tools design-template list\` to see available slugs.`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'ENOENT_SLUG';
        throw err;
    }
    const text = readFileSync(designPath, 'utf8');
    const frontmatter = extractDesignFrontmatter(text);
    const provenance = readProvenance(libraryDir, opts.slug);
    return { slug: opts.slug, path: designPath, design_md: text, frontmatter, provenance };
}
/**
 * Copy a library entry's DESIGN.md into a target project directory as
 * `<projectDir>/DESIGN.md`. Refuses to overwrite unless `force` is set —
 * mirrors the `author-skill` safety rule.
 *
 * Throws (caller translates to CLI exit) on refusal or missing slug.
 *
 * @param {{ libraryDir?: string, slug: string, projectDir: string, force?: boolean }} opts
 * @returns {{ slug: string, from: string, to: string, overwritten: boolean }}
 */
export function addLibraryEntry(opts) {
    if (!opts || typeof opts.slug !== 'string' || opts.slug.length === 0) {
        throw new Error('addLibraryEntry: slug is required');
    }
    if (!opts.projectDir || typeof opts.projectDir !== 'string') {
        throw new Error('addLibraryEntry: projectDir is required');
    }
    assertSafeSlug(opts.slug, 'addLibraryEntry');
    const libraryDir = resolveLibraryDir(opts.libraryDir);
    const from = resolvePath(libraryDir, opts.slug, 'DESIGN.md');
    if (!existsSync(from)) {
        const err = new Error(`Library entry "${opts.slug}" not found (expected at ${from}). Run \`apt-tools design-template list\` to see available slugs.`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'ENOENT_SLUG';
        throw err;
    }
    const projectDir = resolvePath(opts.projectDir);
    mkdirSync(projectDir, { recursive: true });
    const to = resolvePath(projectDir, 'DESIGN.md');
    const overwritten = existsSync(to);
    if (overwritten && !opts.force) {
        const err = new Error(`Refusing to overwrite existing DESIGN.md at ${to}. Re-run with --force to overwrite, or edit the file directly.`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'EEXIST_DEST';
        throw err;
    }
    copyFileSync(from, to);
    return { slug: opts.slug, from, to, overwritten };
}
/**
 * Validate a candidate DESIGN.md and, when it passes, copy it into the
 * library under `<libraryDir>/<slug>/DESIGN.md` with a fresh provenance.yaml
 * whose `origin` is `community`. Does NOT open a PR — the author commits
 * + pushes themselves.
 *
 * Throws (caller translates to CLI exit) when:
 *   - the candidate file doesn't exist
 *   - the candidate fails `validateDesignMarkdown`
 *   - the target slug directory already exists (no --force here; community
 *     submissions go into a fresh directory, always)
 *
 * @param {{ libraryDir?: string, slug?: string, candidatePath: string, force?: boolean }} opts
 * @returns {{ slug: string, copied_to: string, provenance_path: string, warnings: string[] }}
 */
export function submitLibraryEntry(opts) {
    if (!opts || typeof opts.candidatePath !== 'string' || opts.candidatePath.length === 0) {
        throw new Error('submitLibraryEntry: candidatePath is required');
    }
    const candidatePath = resolvePath(opts.candidatePath);
    if (!existsSync(candidatePath)) {
        const err = new Error(`Candidate DESIGN.md not found at ${candidatePath}.`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'ENOENT_CANDIDATE';
        throw err;
    }
    const text = readFileSync(candidatePath, 'utf8');
    const validation = validateDesignMarkdown(text);
    if (!validation.ok) {
        const err = new Error(`Candidate DESIGN.md failed validation:\n  - ${validation.errors.join('\n  - ')}`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'EINVALID_CANDIDATE';
        // @ts-expect-error — attach details
        err.validation = validation;
        throw err;
    }
    // Resolve slug: explicit --as wins; else derive from the candidate's
    // parent directory name (e.g. `foo/DESIGN.md` → slug=foo).
    let slug = opts.slug;
    if (!slug || slug.length === 0) {
        slug = basename(dirname(candidatePath));
    }
    if (!slug || slug.length === 0 || slug === '.' || slug === '/') {
        throw new Error('submitLibraryEntry: could not infer slug from candidate path; pass --as <slug>');
    }
    // Reject slugs that would escape the library directory or otherwise
    // target a non-directory-name path. See assertSafeSlug() for the allowed
    // character class.
    assertSafeSlug(slug, 'submitLibraryEntry');
    const libraryDir = resolveLibraryDir(opts.libraryDir);
    const targetDir = resolvePath(libraryDir, slug);
    const targetDesign = resolvePath(targetDir, 'DESIGN.md');
    const targetProvenance = resolvePath(targetDir, 'provenance.yaml');
    if (existsSync(targetDir) && !opts.force) {
        const err = new Error(`Library already contains slug "${slug}" at ${targetDir}. Pick a different --as <slug> or use --force to replace (not recommended — community submissions should land in fresh slugs).`);
        // @ts-expect-error — attach code for CLI layer
        err.code = 'EEXIST_SLUG';
        throw err;
    }
    mkdirSync(targetDir, { recursive: true });
    copyFileSync(candidatePath, targetDesign);
    const provenance = {
        origin: 'community',
        source: 'user-submitted',
        source_path: candidatePath,
        source_license: 'unspecified',
        source_author: 'unspecified',
        submitted_at: new Date().toISOString().slice(0, 10),
        extractor_version: null,
        warnings: /** @type {string[]} */ (validation.warnings || []),
    };
    writeFileSync(targetProvenance, stringifyYaml(provenance, { lineWidth: 0 }), 'utf8');
    return {
        slug,
        copied_to: targetDesign,
        provenance_path: targetProvenance,
        warnings: validation.warnings || [],
    };
}
//# sourceMappingURL=template.mjs.map