const DESCRIPTION_MAX = 200;
const TITLE_MAX = 160;
/**
 * Coerce CMS-authored text into a safe single-line list-item fragment.
 *
 * These strings flow from editor fields into a file that AI agents read, so we
 * defend against two things: (1) markdown-breaking characters that would corrupt
 * the `- [title](url): desc` syntax, and (2) multi-line / control-character
 * content that a careless or hostile editor could use to smuggle instruction-like
 * text. This is not a complete prompt-injection defense (no text filter is) — it
 * forces single-line, length-capped, link-safe plain text, which removes the
 * most common corruption and obfuscation vectors.
 */
export function sanitizeLlmsText(raw, maxLen) {
    if (!raw)
        return '';
    let s = raw
        // Strip control characters (incl. newlines, tabs, zero-width) → space.
        .replace(/[\u0000-\u001F\u007F\u200B-\u200D\uFEFF]/g, ' ')
        // Remove characters that break markdown link syntax in the list item.
        .replace(/[[\]()]/g, '')
        // Collapse any remaining whitespace runs.
        .replace(/\s+/g, ' ')
        .trim();
    if (s.length > maxLen)
        s =
            s
                .slice(0, maxLen)
                .replace(/\s+\S*$/, '')
                .trim() || s.slice(0, maxLen);
    return s;
}
export function generateLlmsTxt(config, pages) {
    const maxPages = config.maxPages ?? 50;
    const maxPerSection = config.maxPerSection ?? 25;
    const lines = [];
    lines.push(`# ${sanitizeLlmsText(config.siteName, TITLE_MAX) || 'Site'}`);
    lines.push('');
    const summary = sanitizeLlmsText(config.siteDescription, DESCRIPTION_MAX);
    if (summary) {
        lines.push(`> ${summary}`);
        lines.push('');
    }
    // Free-text context block. It is a reviewed config field (not editor-freeform),
    // so multi-line markdown is allowed; we only trim and drop accidental headings
    // (which would corrupt the section structure the spec relies on).
    const intro = (config.intro ?? '')
        .split(/\r?\n/)
        .filter((l) => !/^\s{0,3}#/.test(l))
        .join('\n')
        .trim();
    if (intro) {
        lines.push(intro);
        lines.push('');
    }
    const limited = [...pages].sort(comparePages).slice(0, maxPages);
    const main = limited.filter((p) => !p.optional);
    const optional = limited.filter((p) => p.optional);
    for (const [section, group] of groupBySection(main)) {
        emitSection(lines, formatHeading(section), group, maxPerSection);
    }
    // The reserved `## Optional` section tells context-limited agents what to skip
    // first. Optional entries are flat (not sub-grouped) per common convention.
    if (optional.length > 0) {
        emitSection(lines, 'Optional', optional, maxPerSection);
    }
    lines.push('## Resources');
    lines.push('');
    lines.push(`- [Sitemap](${config.siteUrl}/sitemap.xml)`);
    lines.push(`- [API Documentation](${config.siteUrl}/api/cms/docs)`);
    lines.push('');
    return lines.join('\n');
}
/** Cornerstone first, then higher `priority`, then most-recently updated. */
function comparePages(a, b) {
    if (a.isCornerstone !== b.isCornerstone)
        return a.isCornerstone ? -1 : 1;
    const pa = a.priority ?? 0;
    const pb = b.priority ?? 0;
    if (pa !== pb)
        return pb - pa;
    if (a.updatedAt && b.updatedAt)
        return b.updatedAt.localeCompare(a.updatedAt);
    if (a.updatedAt)
        return -1;
    if (b.updatedAt)
        return 1;
    return 0;
}
/** Group entries by their resolved section, preserving first-seen order. */
function groupBySection(pages) {
    const groups = new Map();
    for (const page of pages) {
        const key = page.section ?? page.collection ?? 'Important Pages';
        let group = groups.get(key);
        if (!group) {
            group = [];
            groups.set(key, group);
        }
        group.push(page);
    }
    return groups;
}
function emitSection(lines, heading, group, maxPerSection) {
    lines.push(`## ${heading}`);
    lines.push('');
    // Cap per section so a single large collection can't crowd out the rest.
    for (const page of group.slice(0, maxPerSection)) {
        const title = sanitizeLlmsText(page.title, TITLE_MAX) || 'Untitled';
        const description = sanitizeLlmsText(page.description, DESCRIPTION_MAX);
        // Append a freshness marker so AI answer engines can prefer recent content.
        // `updatedAt` is an ISO string; emit the date portion only.
        const updated = page.updatedAt ? formatLlmsDate(page.updatedAt) : undefined;
        const suffix = [description, updated ? `Updated: ${updated}` : undefined]
            .filter(Boolean)
            .join(' — ');
        const entry = suffix ? `- [${title}](${page.url}): ${suffix}` : `- [${title}](${page.url})`;
        lines.push(entry);
    }
    lines.push('');
}
function formatHeading(collection) {
    return collection.replace(/[-_]/g, ' ').replace(/\b\w/g, (ch) => ch.toUpperCase());
}
/** Reduce an ISO timestamp to a `YYYY-MM-DD` date, or `undefined` if unparseable. */
function formatLlmsDate(iso) {
    const d = new Date(iso);
    return isNaN(d.getTime()) ? undefined : d.toISOString().slice(0, 10);
}
//# sourceMappingURL=llms-txt.js.map