import { listDocuments, getDocument, createDocument, updateDocument } from '../../actions.js';
import { logEvent } from '../../security/audit.js';
import { notifyPreviewUpdate } from '../../preview/event-bus.js';
import { getSectionType, getSectionTypesForScope, getAllSectionTypes, buildSection, coerceSections, coercePostHeader, createPostTemplate, addSection as addSectionToList, removeSection as removeSectionFromList, reorderSections as reorderSectionList, setSectionVisibility, updateSectionContent as updateSectionContentInList, updateSectionSettings as updateSectionSettingsInList, updateSectionMeta as updateSectionMetaInList, validateSectionContent, } from '../../sections/index.js';
import { buildPostsOrderBy, buildPostsWhere, extractSeoScore as extractPostSeoScore, resolveQueryCollections, } from '../../posts/query.js';
import { getActuateConfig } from '../../config/runtime.js';
import { guardedDb } from '../guarded-db.js';
import { json, errorResponse, internalError, clampPageSize, requireAuth, requireCollectionScope, buildActionContext, collectionSupportsSections, } from '../route-helpers.js';
export function registerSectionRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // Page section routes (flat Page Editor model)
    //
    // These operate on a page document's `data.sections` JSON array — the same
    // model the visual Page Editor reads/writes — so an AI agent driving the
    // CMS from Cursor/VS Code via MCP produces real, editable, publishable
    // sections rather than disposable markup. All mutations go through the
    // audited `updateDocument` path (versions, hooks, cache invalidation,
    // live-preview push) and reuse collection scope checks.
    //
    // Discovery + validation are deterministic (no LLM) and only need auth.
    // ---------------------------------------------------------------------------
    // GET /page-sections/types — discover registered section types + schemas.
    // Optional `?scope=page|post` filters to types usable on that surface so an
    // agent only builds sections that have a working renderer in that context.
    router.get('/page-sections/types', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scope = new URL(request.url).searchParams.get('scope');
            const data = scope === 'page' || scope === 'post'
                ? getSectionTypesForScope(scope)
                : getAllSectionTypes();
            return json({ data });
        }
        catch (err) {
            return internalError(err, 'list section types');
        }
    });
    // POST /page-sections/validate — validate section content against its
    // type schema. Body: { sectionType, content, settings? }.
    router.post('/page-sections/validate', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const body = (await request.json().catch(() => null));
            const sectionType = typeof body?.sectionType === 'string' ? body.sectionType : '';
            if (!sectionType)
                return errorResponse('sectionType is required', 400);
            const content = body?.content && typeof body.content === 'object'
                ? body.content
                : {};
            const settings = body?.settings && typeof body.settings === 'object'
                ? body.settings
                : undefined;
            const result = validateSectionContent(sectionType, content, settings);
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'validate section');
        }
    });
    // Shared: load a page document's sections, or a Response on error.
    //
    // ADR 0002 ownership guard: a document authored in the visual page builder
    // carries a `_layout` tree and its `sections` are *compiled* from that tree on
    // every save. Editing those sections directly would be silently overwritten by
    // the next builder save, so for WRITE callers we refuse a builder-owned doc
    // with 409 instead. Read callers are unaffected.
    const loadPageSections = async (collection, id, ctx, opts) => {
        if (!collectionSupportsSections(collection)) {
            return errorResponse(`Collection "${collection}" does not declare a "sections" JSON field, so it cannot store page sections.`, 400);
        }
        const doc = await getDocument(collection, id, ctx);
        if (!doc)
            return errorResponse('Page not found', 404);
        // `getDocument` lifts the stored `_layout` out of `data` onto the wire
        // envelope (`doc.layout`), so the builder-ownership signal lives there.
        const layout = doc.layout;
        if (opts?.forWrite && layout && typeof layout === 'object') {
            return errorResponse(`Page "${id}" is owned by the visual page builder: its sections are compiled from the builder layout and would be overwritten on the next save. Edit it in the Page Builder instead (or clear its layout to convert it to a section-authored page).`, 409);
        }
        const data = doc.data ?? {};
        // Preserve sections whose type isn't registered (seed/code-owned, e.g.
        // `opal-*`) so a mutation through this API round-trips them instead of
        // silently dropping externally-managed content on the next persist.
        return {
            sections: coerceSections(data.sections, {
                onUnknown: 'preserve',
            }),
        };
    };
    // Shared: persist a mutated section array via the audited update path.
    const persistPageSections = async (collection, id, sections, ctx, userId) => {
        // Drop the derived `unmanaged` marker before persisting — it is recomputed
        // from the registry on every read, so storing it would only let it go stale.
        const toStore = sections.map(({ unmanaged: _unmanaged, ...rest }) => rest);
        const doc = await updateDocument(collection, id, { sections: toStore }, ctx);
        await logEvent({
            event: 'document_updated',
            userId,
            details: { collection, documentId: id, action: 'page_sections_updated' },
        });
        notifyPreviewUpdate(collection, id, {
            values: doc.data,
            status: doc.status,
            updatedAt: doc.updatedAt?.toISOString?.(),
        });
        const data = doc.data ?? {};
        return coerceSections(data.sections, { onUnknown: 'preserve' });
    };
    // GET /page-sections/:collection/:id — list a page's sections.
    router.get('/page-sections/:collection/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.collection, 'read');
            if (scopeErr)
                return scopeErr;
            const ctx = buildActionContext(auth.session, db());
            const loaded = await loadPageSections(params.collection, params.id, ctx);
            if (loaded instanceof Response)
                return loaded;
            return json({ data: loaded.sections });
        }
        catch (err) {
            return internalError(err, 'list page sections');
        }
    });
    // POST /page-sections/:collection/:id — add a section.
    // Body: { sectionType, name?, internalLabel?, visible?, content?, settings?, position? }.
    router.post('/page-sections/:collection/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.collection, 'update');
            if (scopeErr)
                return scopeErr;
            const body = (await request.json().catch(() => null));
            const sectionType = typeof body?.sectionType === 'string' ? body.sectionType : '';
            if (!getSectionType(sectionType)) {
                return errorResponse(`Unknown section type "${sectionType}". Call GET /page-sections/types for valid ids.`, 400);
            }
            const ctx = buildActionContext(auth.session, db());
            const loaded = await loadPageSections(params.collection, params.id, ctx, { forWrite: true });
            if (loaded instanceof Response)
                return loaded;
            const section = buildSection(sectionType, {
                name: typeof body?.name === 'string' ? body.name : undefined,
                internalLabel: typeof body?.internalLabel === 'string' ? body.internalLabel : undefined,
                visible: body?.visible !== false,
                content: body?.content && typeof body.content === 'object'
                    ? body.content
                    : undefined,
                settings: body?.settings && typeof body.settings === 'object'
                    ? body.settings
                    : undefined,
            });
            const position = typeof body?.position === 'number' ? body.position : undefined;
            const next = addSectionToList(loaded.sections, section, position);
            const saved = await persistPageSections(params.collection, params.id, next, ctx, auth.session.userId);
            const validation = validateSectionContent(section.sectionType, section.content, section.settings);
            return json({ data: { section, sections: saved, validation } }, 201);
        }
        catch (err) {
            return internalError(err, 'add page section');
        }
    });
    // POST /page-sections/:collection/:id/reorder — reorder by id list.
    // Registered BEFORE the :sectionId routes so "reorder" isn't captured as a
    // section id (the router matches in registration order).
    router.post('/page-sections/:collection/:id/reorder', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.collection, 'update');
            if (scopeErr)
                return scopeErr;
            const body = (await request.json().catch(() => null));
            const orderedIds = Array.isArray(body?.orderedIds)
                ? body.orderedIds.filter((x) => typeof x === 'string')
                : null;
            if (!orderedIds)
                return errorResponse('orderedIds (string[]) is required', 400);
            const ctx = buildActionContext(auth.session, db());
            const loaded = await loadPageSections(params.collection, params.id, ctx, { forWrite: true });
            if (loaded instanceof Response)
                return loaded;
            const next = reorderSectionList(loaded.sections, orderedIds);
            const saved = await persistPageSections(params.collection, params.id, next, ctx, auth.session.userId);
            return json({ data: saved });
        }
        catch (err) {
            return internalError(err, 'reorder page sections');
        }
    });
    // PATCH /page-sections/:collection/:id/:sectionId — update a section.
    // Body: { content?, settings?, name?, internalLabel?, visible? }.
    router.patch('/page-sections/:collection/:id/:sectionId', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.collection, 'update');
            if (scopeErr)
                return scopeErr;
            const body = (await request.json().catch(() => null));
            const ctx = buildActionContext(auth.session, db());
            const loaded = await loadPageSections(params.collection, params.id, ctx, { forWrite: true });
            if (loaded instanceof Response)
                return loaded;
            const sectionId = params.sectionId;
            if (!loaded.sections.some((s) => s.id === sectionId)) {
                return errorResponse(`Section "${sectionId}" not found on this page.`, 404);
            }
            let next = loaded.sections;
            if (body?.content && typeof body.content === 'object') {
                next = updateSectionContentInList(next, sectionId, body.content);
            }
            if (body?.settings && typeof body.settings === 'object') {
                next = updateSectionSettingsInList(next, sectionId, body.settings);
            }
            const meta = {};
            if (typeof body?.name === 'string')
                meta.name = body.name;
            if (typeof body?.internalLabel === 'string')
                meta.internalLabel = body.internalLabel;
            if (Object.keys(meta).length > 0)
                next = updateSectionMetaInList(next, sectionId, meta);
            if (typeof body?.visible === 'boolean') {
                next = setSectionVisibility(next, sectionId, body.visible);
            }
            const saved = await persistPageSections(params.collection, params.id, next, ctx, auth.session.userId);
            const updated = saved.find((s) => s.id === sectionId);
            const validation = updated
                ? validateSectionContent(updated.sectionType, updated.content, updated.settings)
                : undefined;
            return json({ data: { section: updated, sections: saved, validation } });
        }
        catch (err) {
            return internalError(err, 'update page section');
        }
    });
    // DELETE /page-sections/:collection/:id/:sectionId — remove a section.
    router.delete('/page-sections/:collection/:id/:sectionId', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.collection, 'update');
            if (scopeErr)
                return scopeErr;
            const ctx = buildActionContext(auth.session, db());
            const loaded = await loadPageSections(params.collection, params.id, ctx, { forWrite: true });
            if (loaded instanceof Response)
                return loaded;
            const sectionId = params.sectionId;
            if (!loaded.sections.some((s) => s.id === sectionId)) {
                return errorResponse(`Section "${sectionId}" not found on this page.`, 404);
            }
            const next = removeSectionFromList(loaded.sections, sectionId);
            const saved = await persistPageSections(params.collection, params.id, next, ctx, auth.session.userId);
            return json({ data: { sections: saved } });
        }
        catch (err) {
            return internalError(err, 'remove page section');
        }
    });
    // ---------------------------------------------------------------------------
    // Post template routes
    //
    // A post type's template is a single document in the hidden
    // `post-templates` collection, matched by `slug` == the post-type slug. It
    // stores the design-driven header layout (`data.header`) and the default
    // section list (`data.sections`) new posts inherit. Lets an external AI
    // agent (Cursor/VS Code) read a type's template and build it from
    // scope=post section types discovered via `/page-sections/types?scope=post`.
    // ---------------------------------------------------------------------------
    const POST_TEMPLATES_COLLECTION = 'post-templates';
    const findPostTemplateDoc = async (postType, ctx) => {
        const res = await listDocuments({ collection: POST_TEMPLATES_COLLECTION, pageSize: 200 }, ctx);
        const docs = res.docs ?? [];
        return (docs.find((d) => (d.slug ?? d.data?.slug) === postType) ?? null);
    };
    const templateResponse = (postType, doc) => {
        if (!doc) {
            // No saved template yet → hand back a usable starting template
            // (centered header + article body + related posts) rather than a blank
            // one, so new posts and the template editor are never empty. docId is
            // null so callers can still tell it has not been persisted.
            const seed = createPostTemplate(postType);
            return { postType, docId: null, header: seed.header, sections: seed.sections };
        }
        const data = doc.data ?? {};
        return {
            postType,
            docId: doc.id,
            header: coercePostHeader(data.header),
            sections: coerceSections(data.sections),
        };
    };
    // GET /post-templates/:type — read a post type's template (header +
    // default sections). Returns an unsaved default when none exists yet.
    router.get('/post-templates/:type', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, POST_TEMPLATES_COLLECTION, 'read');
            if (scopeErr)
                return scopeErr;
            const ctx = buildActionContext(auth.session, db());
            const doc = await findPostTemplateDoc(params.type, ctx);
            return json({ data: templateResponse(params.type, doc) });
        }
        catch (err) {
            return internalError(err, 'get post template');
        }
    });
    // PUT /post-templates/:type — create or replace a post type's template.
    // Body: { header?, sections? }. Header is coerced to a full PostHeaderConfig
    // and sections are coerced (unknown section types dropped) so the stored
    // template always renders.
    router.put('/post-templates/:type', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, POST_TEMPLATES_COLLECTION, 'update');
            if (scopeErr)
                return scopeErr;
            const body = (await request.json().catch(() => null));
            const header = coercePostHeader(body?.header);
            const sections = coerceSections(body?.sections);
            const ctx = buildActionContext(auth.session, db());
            const existing = await findPostTemplateDoc(params.type, ctx);
            const data = {
                title: `${params.type} template`,
                slug: params.type,
                postType: params.type,
                header,
                sections,
                status: 'PUBLISHED',
            };
            const doc = existing
                ? await updateDocument(POST_TEMPLATES_COLLECTION, existing.id, data, ctx)
                : await createDocument(POST_TEMPLATES_COLLECTION, data, ctx);
            await logEvent({
                event: existing ? 'document_updated' : 'document_created',
                userId: auth.session.userId,
                details: {
                    collection: POST_TEMPLATES_COLLECTION,
                    documentId: doc.id,
                    action: 'post_template_saved',
                    postType: params.type,
                },
            });
            return json({ data: templateResponse(params.type, doc) });
        }
        catch (err) {
            return internalError(err, 'save post template');
        }
    });
    // ---------------------------------------------------------------------------
    // GET /posts — aggregate list across post-type collections for the admin
    // Posts area. Filtering, sorting and pagination all happen at the DB level
    // (one query + one count) so the page scales past the old per-type client
    // cap. Respects per-collection read scope: API keys only see collections
    // their scope allows. Returns the post rows plus dataset-wide type counts
    // and the contributor list the toolbar needs.
    // ---------------------------------------------------------------------------
    router.get('/posts', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const cfg = getActuateConfig();
            const collections = cfg?.collections ?? {};
            // Post-type collections the caller may read. Hidden collections and ones
            // the API-key scope forbids are dropped so the aggregate never leaks rows
            // a direct /collections/:slug call would refuse.
            const postTypeMeta = [];
            const allowedSlugs = [];
            for (const [slug, col] of Object.entries(collections)) {
                const isPost = col?.type === 'post' ||
                    (typeof col?.admin?.group === 'string' && col.admin.group.toLowerCase() === 'posts');
                if (!isPost || col?.admin?.hidden)
                    continue;
                if (requireCollectionScope(auth.session, slug, 'read'))
                    continue;
                allowedSlugs.push(slug);
                postTypeMeta.push({
                    slug,
                    singular: col?.labels?.singular ?? slug,
                    plural: col?.labels?.plural ?? slug,
                    description: col?.admin?.description ?? null,
                    urlPrefix: col?.urlPrefix ?? slug,
                    color: col?.admin?.color ?? null,
                    icon: col?.admin?.icon ?? null,
                    fieldCount: col?.fields ? Object.keys(col.fields).length : 0,
                });
            }
            const url = new URL(request.url);
            const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
            const pageSize = clampPageSize(url.searchParams.get('pageSize'), 100, 10);
            if (allowedSlugs.length === 0) {
                return json({
                    data: { posts: [], total: 0, page, pageSize, types: [], authors: [] },
                });
            }
            const queryInput = {
                allowedCollections: allowedSlugs,
                postType: url.searchParams.get('postType') ?? undefined,
                status: url.searchParams.get('status') ?? undefined,
                author: url.searchParams.get('author') ?? undefined,
                search: url.searchParams.get('search') ?? undefined,
                sortBy: url.searchParams.get('sortBy') ?? undefined,
                sortDirection: url.searchParams.get('sortDirection') ?? undefined,
            };
            const where = buildPostsWhere(queryInput);
            const orderBy = buildPostsOrderBy(queryInput.sortBy, queryInput.sortDirection);
            const queryCollections = resolveQueryCollections(queryInput);
            // No collections to scan (e.g. a forbidden ?postType) → empty page, but
            // still return the type/author facets for the toolbar.
            const [docs, total] = queryCollections.length
                ? await Promise.all([
                    db().document.findMany({
                        where,
                        orderBy,
                        skip: (page - 1) * pageSize,
                        take: pageSize,
                        include: { createdBy: { select: { id: true, name: true, email: true } } },
                    }),
                    db().document.count({ where }),
                ])
                : [[], 0];
            // Dataset-wide facets (ignore the active filters) so the type tabs and
            // author dropdown reflect everything the user can see, not the query.
            const facetWhere = { collection: { in: allowedSlugs }, deletedAt: null };
            const [typeCounts, authorGroups] = await Promise.all([
                db().document.groupBy({
                    by: ['collection'],
                    where: facetWhere,
                    _count: { _all: true },
                }),
                db().document.groupBy({ by: ['createdById'], where: facetWhere }),
            ]);
            const countBySlug = new Map();
            for (const row of typeCounts) {
                countBySlug.set(row.collection, row._count._all);
            }
            const authorIds = authorGroups
                .map((g) => g.createdById)
                .filter((id) => Boolean(id));
            const authorUsers = authorIds.length
                ? await db().user.findMany({
                    where: { id: { in: authorIds } },
                    select: { id: true, name: true, email: true },
                })
                : [];
            const posts = docs.map((d) => ({
                id: d.id,
                collection: d.collection,
                title: d.title ?? null,
                slug: d.slug ?? null,
                status: d.status,
                publishedAt: d.publishedAt ? d.publishedAt.toISOString() : null,
                scheduledAt: d.scheduledAt ? d.scheduledAt.toISOString() : null,
                createdAt: d.createdAt.toISOString(),
                updatedAt: d.updatedAt.toISOString(),
                seoScore: extractPostSeoScore(d.data),
                author: d.createdBy
                    ? { id: d.createdBy.id, name: d.createdBy.name?.trim() || d.createdBy.email }
                    : null,
            }));
            return json({
                data: {
                    posts,
                    total,
                    page,
                    pageSize,
                    types: postTypeMeta.map((t) => ({ ...t, count: countBySlug.get(t.slug) ?? 0 })),
                    authors: authorUsers.map((u) => ({
                        id: u.id,
                        name: u.name?.trim() || u.email,
                    })),
                },
            });
        }
        catch (err) {
            return internalError(err, 'list posts');
        }
    });
}
//# sourceMappingURL=sections.js.map