import { listDocuments, getDocument, createDocument, updateDocument } from '../../actions.js';
import { logEvent } from '../../security/audit.js';
import { validateTree } from '../../page-builder/validate.js';
import { auditAccessibility, fixAccessibility } from '../../page-builder/a11y-fix.js';
import { isResolvedIp } from '../../security/client-ip.js';
import { redactSecrets } from '../../security/redact.js';
import { getActuateConfig } from '../../config/runtime.js';
import { guardedDb } from '../guarded-db.js';
import { importAIPlugin, buildCoauthorPrompt, json, errorResponse, internalError, mediaUrl, deriveMediaTitleFromAlt, hasModel, modelNotAvailable, isAllowedStorageUrl, fetchImageForVision, requireAuth, requireCollectionScope, requireMediaScope, requirePageBuilderScope, buildActionContext, ADMIN_ROLES, requireRole, aiGenerateLimiter, aiAnalysisLimiter, AI_ANALYSIS_MAX_CHARS, checkRateLimitAsync, clientIp, } from '../route-helpers.js';
export function registerSavedSectionRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // Saved Sections
    // ---------------------------------------------------------------------------
    router.get('/saved-sections', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
            if (roleErr)
                return roleErr;
            const d = db();
            if (!hasModel(d, 'savedSection'))
                return modelNotAvailable('SavedSection');
            const url = new URL(request.url, 'http://localhost');
            const category = url.searchParams.get('category');
            const where = {};
            if (category)
                where.category = category;
            const sections = await d.savedSection.findMany({
                where,
                orderBy: { updatedAt: 'desc' },
            });
            return json({ data: sections });
        }
        catch (err) {
            return internalError(err, 'saved-sections list');
        }
    });
    router.get('/saved-sections/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
            if (roleErr)
                return roleErr;
            const d = db();
            if (!hasModel(d, 'savedSection'))
                return modelNotAvailable('SavedSection');
            const section = await d.savedSection.findUnique({ where: { id: params.id } });
            if (!section)
                return errorResponse('Saved section not found', 404);
            return json({ data: section });
        }
        catch (err) {
            return internalError(err, 'saved-sections get');
        }
    });
    router.post('/saved-sections', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
            if (roleErr)
                return roleErr;
            const d = db();
            if (!hasModel(d, 'savedSection'))
                return modelNotAvailable('SavedSection');
            const body = (await request.json());
            if (!body.name)
                return errorResponse('name is required', 400);
            if (!body.tree)
                return errorResponse('tree is required', 400);
            const wrappedTree = { id: '__validation_wrapper__', type: 'page', children: [body.tree] };
            const validation = validateTree(wrappedTree);
            if (!validation.valid) {
                return errorResponse(`Invalid section tree: ${validation.errors.join('; ')}`, 400);
            }
            const validCategories = ['header', 'footer', 'content', 'sidebar'];
            const category = body.category || 'content';
            if (!validCategories.includes(category)) {
                return errorResponse('category must be header, footer, content, or sidebar', 400);
            }
            const section = await d.savedSection.create({
                data: {
                    name: body.name,
                    description: body.description || null,
                    category,
                    tree: body.tree,
                    thumbnail: body.thumbnail || null,
                },
            });
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: { action: 'saved_section_created', sectionId: section.id, name: section.name },
            });
            return json({ data: section }, 201);
        }
        catch (err) {
            return internalError(err, 'saved-sections create');
        }
    });
    router.put('/saved-sections/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
            if (roleErr)
                return roleErr;
            const d = db();
            if (!hasModel(d, 'savedSection'))
                return modelNotAvailable('SavedSection');
            const existing = await d.savedSection.findUnique({ where: { id: params.id } });
            if (!existing)
                return errorResponse('Saved section not found', 404);
            const body = (await request.json());
            const update = {};
            if (body.name !== undefined)
                update.name = body.name;
            if (body.description !== undefined)
                update.description = body.description;
            if (body.thumbnail !== undefined)
                update.thumbnail = body.thumbnail;
            if (body.category !== undefined) {
                const validCategories = ['header', 'footer', 'content', 'sidebar'];
                if (!validCategories.includes(body.category)) {
                    return errorResponse('category must be header, footer, content, or sidebar', 400);
                }
                update.category = body.category;
            }
            if (body.tree !== undefined) {
                const wrappedTree = { id: '__validation_wrapper__', type: 'page', children: [body.tree] };
                const validation = validateTree(wrappedTree);
                if (!validation.valid) {
                    return errorResponse(`Invalid section tree: ${validation.errors.join('; ')}`, 400);
                }
                update.tree = body.tree;
            }
            const section = await d.savedSection.update({
                where: { id: params.id },
                data: update,
            });
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: { action: 'saved_section_updated', sectionId: section.id, name: section.name },
            });
            return json({ data: section });
        }
        catch (err) {
            return internalError(err, 'saved-sections update');
        }
    });
    router.delete('/saved-sections/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, ADMIN_ROLES);
            if (roleErr)
                return roleErr;
            const d = db();
            if (!hasModel(d, 'savedSection'))
                return modelNotAvailable('SavedSection');
            const existing = await d.savedSection.findUnique({ where: { id: params.id } });
            if (!existing)
                return errorResponse('Saved section not found', 404);
            await d.savedSection.delete({ where: { id: params.id } });
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: { action: 'saved_section_deleted', sectionId: existing.id, name: existing.name },
            });
            return json({ data: { deleted: true } });
        }
        catch (err) {
            return internalError(err, 'saved-sections delete');
        }
    });
    // ─── Page Builder AI Generation ─────────────────────────────────────
    // Hard caps for AI input to keep token cost bounded and to limit the
    // surface area for prompt injection. Adjust via the `ai.limits` block in
    // the CMS config if you want stricter values; never raise past 8k.
    const AI_PROMPT_MAX_CHARS = 4000;
    const AI_CONTEXT_MAX_CHARS = 8000;
    router.post('/page-builder/generate', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requirePageBuilderScope(auth.session);
            if (scopeErr)
                return scopeErr;
            // Per-user rate limit. AI generation is the single most expensive
            // operation in the CMS — without this, a compromised admin account
            // can drain a provider key in minutes.
            if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-gen:${auth.session.userId}`))) {
                return errorResponse('AI generation rate limit reached. Try again in an hour.', 429);
            }
            const body = await request.json();
            const { prompt, template, context, steps, tone } = body;
            if (!prompt || typeof prompt !== 'string') {
                return errorResponse('prompt is required', 400);
            }
            if (prompt.length > AI_PROMPT_MAX_CHARS) {
                return errorResponse(`prompt exceeds ${AI_PROMPT_MAX_CHARS} character limit`, 400);
            }
            if (typeof context === 'string' && context.length > AI_CONTEXT_MAX_CHARS) {
                return errorResponse(`context exceeds ${AI_CONTEXT_MAX_CHARS} character limit`, 400);
            }
            if (!steps || !Array.isArray(steps) || steps.length === 0) {
                return errorResponse('steps array is required', 400);
            }
            const validSteps = ['structure', 'content', 'seo', 'accessibility'];
            for (const s of steps) {
                if (!validSteps.includes(s)) {
                    return errorResponse(`Invalid step: ${s}. Valid steps: ${validSteps.join(', ')}`, 400);
                }
            }
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: {
                    action: 'page_generation_started',
                    // Redact secrets from the prompt before persisting to the audit log.
                    // Even an admin pasting a key into a prompt by mistake shouldn't
                    // result in that key being mirrored into permanent storage.
                    prompt: redactSecrets(prompt).slice(0, 500),
                    steps,
                    template,
                },
            });
            let generatePage = null;
            try {
                const aiModule = await importAIPlugin();
                generatePage = aiModule.generatePage;
            }
            catch {
                return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use page generation.', 501);
            }
            const result = await generatePage({ prompt, template, context, steps, tone });
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: {
                    action: 'page_generation_completed',
                    totalTokensUsed: result.totalTokensUsed,
                    totalDurationMs: result.totalDurationMs,
                    stepsCompleted: result.steps.map((s) => s.step),
                },
            });
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'page-builder generate');
        }
    });
    router.post('/page-builder/generate-block', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requirePageBuilderScope(auth.session);
            if (scopeErr)
                return scopeErr;
            if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-block:${auth.session.userId}`))) {
                return errorResponse('AI generation rate limit reached. Try again in an hour.', 429);
            }
            const body = await request.json();
            const { blockType, variant, pageContext, tone } = body;
            if (!blockType || typeof blockType !== 'string') {
                return errorResponse('blockType is required', 400);
            }
            // Limit caller-supplied context that flows directly into the prompt.
            if (pageContext) {
                const total = JSON.stringify(pageContext).length;
                if (total > AI_CONTEXT_MAX_CHARS) {
                    return errorResponse(`pageContext exceeds ${AI_CONTEXT_MAX_CHARS} character limit`, 400);
                }
            }
            let generateBlockContent = null;
            try {
                const aiModule = await importAIPlugin();
                generateBlockContent = aiModule.generateBlockContent;
            }
            catch {
                return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use block generation.', 501);
            }
            const data = await generateBlockContent({ blockType, variant, pageContext, tone });
            return json({ data });
        }
        catch (err) {
            return internalError(err, 'page-builder generate-block');
        }
    });
    /**
     * One-shot page creation: run the AI page generator and persist the result
     * as a new document in a single call. Designed for AI agents that want to
     * create a complete page from a prompt without orchestrating two requests
     * (generate, then create). Defaults to status=DRAFT so the human reviewer
     * can polish before publishing.
     */
    router.post('/page-builder/create', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requirePageBuilderScope(auth.session);
            if (scopeErr)
                return scopeErr;
            // The create path also writes to a collection, so the API key must hold
            // create scope on the destination collection (defaults to 'pages').
            const body = (await request.json());
            const targetCollection = body.collection ?? 'pages';
            const collectionScopeErr = requireCollectionScope(auth.session, targetCollection, 'create');
            if (collectionScopeErr)
                return collectionScopeErr;
            if (!body.prompt || typeof body.prompt !== 'string') {
                return errorResponse('prompt is required', 400);
            }
            if (body.prompt.length > AI_PROMPT_MAX_CHARS) {
                return errorResponse(`prompt exceeds ${AI_PROMPT_MAX_CHARS} character limit`, 400);
            }
            // Same rate-limit bucket as /generate — one create == one expensive LLM
            // run, so it should count against the same hourly cap.
            if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-gen:${auth.session.userId}`))) {
                return errorResponse('AI generation rate limit reached. Try again in an hour.', 429);
            }
            const steps = Array.isArray(body.steps) && body.steps.length > 0
                ? body.steps
                : ['structure', 'content', 'seo', 'accessibility'];
            const validSteps = ['structure', 'content', 'seo', 'accessibility'];
            for (const s of steps) {
                if (!validSteps.includes(s)) {
                    return errorResponse(`Invalid step: ${s}. Valid steps: ${validSteps.join(', ')}`, 400);
                }
            }
            let generatePage = null;
            try {
                const aiModule = await importAIPlugin();
                generatePage = aiModule.generatePage;
            }
            catch {
                return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use page generation.', 501);
            }
            const result = await generatePage({
                prompt: body.prompt,
                template: body.template,
                context: body.context,
                steps,
                tone: body.tone,
            });
            const tree = result?.tree;
            if (!tree) {
                return errorResponse('Page generation returned no tree', 502);
            }
            // Pull SEO metadata out of the generator output so we can store it on
            // the document alongside the layout tree.
            const seoStep = (result?.steps ?? []).find((s) => s.step === 'seo');
            const meta = seoStep?.data ?? {};
            const title = body.title ?? meta.title ?? meta.metaTitle ?? 'Untitled';
            const slug = body.slug ??
                title
                    .toLowerCase()
                    .replace(/[^a-z0-9]+/g, '-')
                    .replace(/^-|-$/g, '');
            // Determine final status — explicit body.status wins, then publish: true,
            // then DRAFT.
            const status = body.status === 'PUBLISHED' || body.publish === true ? 'PUBLISHED' : 'DRAFT';
            const docPayload = {
                title,
                slug,
                status,
                layout: tree,
                pageSettings: {
                    metaTitle: meta.metaTitle ?? meta.title ?? title,
                    metaDescription: meta.metaDescription ?? meta.description ?? '',
                    ...(meta.canonical ? { canonical: meta.canonical } : {}),
                    ...(meta.schemaType ? { schemaType: meta.schemaType } : {}),
                },
            };
            const ctx = buildActionContext(auth.session, db());
            const doc = await createDocument(targetCollection, docPayload, ctx);
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: {
                    action: 'page_create_from_prompt',
                    collection: targetCollection,
                    documentId: doc?.id,
                    prompt: redactSecrets(body.prompt).slice(0, 500),
                    totalTokensUsed: result.totalTokensUsed,
                    totalDurationMs: result.totalDurationMs,
                },
            });
            return json({
                data: {
                    document: doc,
                    generation: {
                        steps: result.steps,
                        totalTokensUsed: result.totalTokensUsed,
                        totalDurationMs: result.totalDurationMs,
                    },
                },
            }, 201);
        }
        catch (err) {
            return internalError(err, 'page-builder create');
        }
    });
    /**
     * Generic AI content authoring for ANY collection.
     *
     * `POST /collections/:slug/ai-create` is the non-page counterpart to
     * `/page-builder/create`. It takes a free-form prompt, looks up the
     * collection's field schema from the running config, asks the AI provider
     * to produce structured JSON matching that schema, validates / coerces the
     * output, and persists the result as a draft (or published) document.
     *
     * Used by:
     *   - the admin "Generate from prompt" affordance on the collection list
     *   - the MCP server's `create_in_collection` / `create_blog_post` / etc.
     *   - AI agents calling the REST API directly
     */
    router.post('/collections/:slug/ai-create', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const targetCollection = params.slug;
            const scopeErr = requireCollectionScope(auth.session, targetCollection, 'create');
            if (scopeErr)
                return scopeErr;
            if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-gen:${auth.session.userId}`))) {
                return errorResponse('AI generation rate limit reached. Try again in an hour.', 429);
            }
            const body = (await request.json());
            if (!body.prompt || typeof body.prompt !== 'string') {
                return errorResponse('prompt is required', 400);
            }
            if (body.prompt.length > AI_PROMPT_MAX_CHARS) {
                return errorResponse(`prompt exceeds ${AI_PROMPT_MAX_CHARS} character limit`, 400);
            }
            if (body.context && body.context.length > AI_CONTEXT_MAX_CHARS) {
                return errorResponse(`context exceeds ${AI_CONTEXT_MAX_CHARS} character limit`, 400);
            }
            const cfg = getActuateConfig();
            const collection = cfg?.collections?.[targetCollection];
            if (!collection) {
                return errorResponse(`Collection "${targetCollection}" not found`, 404);
            }
            // Refuse page-builder-driven collections — those have a layout tree
            // (not flat field data) and should use `/page-builder/create` instead.
            const hasLayoutField = Object.values(collection.fields).some((f) => f.type === 'blocks');
            if (hasLayoutField) {
                return errorResponse(`Collection "${targetCollection}" uses the page builder. Use POST /api/cms/page-builder/create instead.`, 400);
            }
            let generateDocumentContent = null;
            try {
                const aiModule = await importAIPlugin();
                generateDocumentContent = aiModule.generateDocumentContent;
            }
            catch {
                return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use content authoring.', 501);
            }
            if (!generateDocumentContent) {
                return errorResponse('AI plugin missing `generateDocumentContent`. Upgrade @actuate-media/plugin-ai to >= 0.2.0.', 501);
            }
            const result = await generateDocumentContent({
                prompt: body.prompt,
                collection: {
                    slug: collection.slug,
                    labels: collection.labels,
                    type: collection.type,
                    fields: collection.fields,
                    seo: collection.seo,
                },
                context: { existingContent: body.context, targetAudience: body.targetAudience },
                tone: body.tone,
                ...(typeof body.maxTokens === 'number' ? { maxTokens: body.maxTokens } : {}),
                ...(typeof body.temperature === 'number' ? { temperature: body.temperature } : {}),
            });
            const shouldPublish = body.status === 'PUBLISHED' || body.publish === true;
            const finalTitle = body.title ?? result.title;
            const finalSlug = body.slug ?? result.slug;
            // Spread AI data FIRST so explicit caller overrides (title, slug) win.
            // We don't use `pageSettings` here — that's a page-builder-specific
            // wire shape. For non-page collections SEO copy lives on regular
            // fields (metaTitle / metaDescription) declared via the seoFields
            // preset, and the AI generator already populates those inside
            // result.data when they exist on the schema.
            const docPayload = {
                ...result.data,
                title: finalTitle,
                slug: finalSlug,
            };
            const ctx = buildActionContext(auth.session, db());
            let doc = await createDocument(targetCollection, docPayload, ctx);
            // Optional publish: this path deliberately creates the document as a
            // DRAFT first (no `status` is passed to createDocument), then runs a
            // follow-up updateDocument to publish. Going through update — rather than
            // publishing in the single create call — is what lets a caller with only
            // `create` scope still land a draft and receive a partial-success warning
            // (below) instead of a hard failure, which is the safest fallback.
            if (shouldPublish && doc?.id) {
                try {
                    doc = await updateDocument(targetCollection, doc.id, { status: 'PUBLISHED' }, ctx);
                }
                catch (publishErr) {
                    // Surface a 207-style partial success: the draft was created but
                    // publish was denied. We return 201 with a `warning` field so the
                    // agent knows the doc landed but needs human publishing.
                    return json({
                        data: {
                            document: doc,
                            generation: {
                                usage: result.usage,
                                durationMs: result.durationMs,
                                seo: result.seo,
                            },
                        },
                        warning: `Document created as DRAFT — publish failed: ${publishErr instanceof Error ? publishErr.message : 'unknown'}`,
                    }, 201);
                }
            }
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: {
                    action: 'collection_ai_create',
                    collection: targetCollection,
                    documentId: doc?.id,
                    prompt: redactSecrets(body.prompt).slice(0, 500),
                    tokensUsed: result.usage,
                    durationMs: result.durationMs,
                },
            });
            return json({
                data: {
                    document: doc,
                    generation: {
                        usage: result.usage,
                        durationMs: result.durationMs,
                        seo: result.seo,
                    },
                },
            }, 201);
        }
        catch (err) {
            return internalError(err, 'collections/:slug/ai-create');
        }
    });
    router.post('/page-builder/audit-a11y', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requirePageBuilderScope(auth.session);
            if (scopeErr)
                return scopeErr;
            const body = await request.json();
            const tree = body.tree;
            if (!tree || tree.type !== 'page') {
                return errorResponse('A valid page tree is required', 400);
            }
            const issues = auditAccessibility(tree);
            return json({ data: { issues } });
        }
        catch (err) {
            return internalError(err, 'page-builder audit-a11y');
        }
    });
    router.post('/page-builder/fix-a11y', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requirePageBuilderScope(auth.session);
            if (scopeErr)
                return scopeErr;
            const body = await request.json();
            const tree = body.tree;
            if (!tree || tree.type !== 'page') {
                return errorResponse('A valid page tree is required', 400);
            }
            const result = fixAccessibility(tree);
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: {
                    action: 'a11y_auto_fix',
                    fixedCount: result.report.fixedCount,
                    remainingCount: result.report.remainingCount,
                },
            });
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'page-builder fix-a11y');
        }
    });
    /**
     * Composite document audit — Slice F.
     *
     * Runs every scoring dimension plugin-ai exposes (SEO, readability,
     * accessibility, freshness) plus structural sanity checks (missing
     * title, missing meta description, alt-less images, thin body) and
     * returns a single composite grade with per-dimension breakdowns.
     *
     * Two input modes:
     *  - `{ collection, id }` — load the document from the DB and audit
     *    its persisted content. Honors field-level access control.
     *  - `{ title, body, ... }` — audit ad-hoc content without persisting,
     *    used by inline AI co-author and live-preview drafts.
     *
     * Deterministic: makes no LLM calls, so it is cheap and safe to call
     * on every save / debounced field edit. The expensive AI features
     * (suggestions, rewrites) live in separate endpoints.
     */
    router.post('/ai/audit-document', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (!(await checkRateLimitAsync(aiAnalysisLimiter, `ai-audit:${auth.session.userId}`))) {
                return errorResponse('Document audit rate limit reached. Try again later.', 429);
            }
            const body = (await request.json());
            if (typeof body.body === 'string' && body.body.length > AI_ANALYSIS_MAX_CHARS) {
                return errorResponse(`body exceeds ${AI_ANALYSIS_MAX_CHARS} character limit`, 400);
            }
            let auditModule;
            try {
                auditModule = await importAIPlugin();
            }
            catch {
                return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use document audit.', 501);
            }
            if (typeof auditModule?.auditDocument !== 'function') {
                return errorResponse('AI plugin missing `auditDocument`. Upgrade @actuate-media/plugin-ai to >= 0.3.0.', 501);
            }
            const auditIp = clientIp(request);
            await logEvent({
                event: 'ai_audit_document',
                userId: auth.session.userId,
                ipAddress: isResolvedIp(auditIp) ? auditIp : undefined,
                details: body.collection && body.id
                    ? { collection: body.collection, id: body.id }
                    : { adhoc: true },
            });
            // Branch A — load from DB.
            if (body.collection && body.id) {
                const scopeErr = requireCollectionScope(auth.session, body.collection, 'read');
                if (scopeErr)
                    return scopeErr;
                const ctx = buildActionContext(auth.session, db());
                const doc = (await getDocument(body.collection, body.id, ctx));
                if (!doc)
                    return errorResponse('Document not found', 404);
                const data = (doc.data ?? {});
                const pageSettings = (doc.pageSettings ?? {});
                const titleStr = String(body.title ?? doc.title ?? data.title ?? data.name ?? '');
                const bodyStr = String(body.body ?? data.body ?? data.content ?? data.excerpt ?? data.description ?? '');
                const metaTitle = String(body.metaTitle ?? pageSettings.metaTitle ?? data.metaTitle ?? titleStr);
                const metaDescription = String(body.metaDescription ??
                    pageSettings.metaDescription ??
                    data.metaDescription ??
                    data.excerpt ??
                    '');
                const targetKeyword = body.targetKeyword
                    ? body.targetKeyword
                    : typeof pageSettings.targetKeyword === 'string'
                        ? pageSettings.targetKeyword
                        : typeof data.targetKeyword === 'string'
                            ? data.targetKeyword
                            : undefined;
                const result = auditModule.auditDocument({
                    title: titleStr,
                    body: bodyStr,
                    metaTitle,
                    metaDescription,
                    ...(targetKeyword ? { targetKeyword } : {}),
                    ...(body.url ? { url: body.url } : {}),
                    ...(body.headings ? { headings: body.headings } : {}),
                    ...(body.images ? { images: body.images } : {}),
                    ...(body.internalLinks ? { internalLinks: body.internalLinks } : {}),
                    ...(body.externalLinks ? { externalLinks: body.externalLinks } : {}),
                    ...(doc.publishedAt ? { publishedAt: doc.publishedAt } : {}),
                    ...(doc.updatedAt ? { updatedAt: doc.updatedAt } : {}),
                    ...(body.contentType ? { contentType: body.contentType } : {}),
                });
                return json({ data: result });
            }
            // Branch B — ad-hoc audit.
            if (!body.title && !body.body) {
                return errorResponse('Provide either { collection, id } to audit a stored document or at least { title } / { body } for an ad-hoc audit.', 400);
            }
            const result = auditModule.auditDocument({
                title: body.title ?? '',
                body: body.body ?? '',
                ...(body.metaTitle ? { metaTitle: body.metaTitle } : {}),
                ...(body.metaDescription ? { metaDescription: body.metaDescription } : {}),
                ...(body.targetKeyword ? { targetKeyword: body.targetKeyword } : {}),
                ...(body.url ? { url: body.url } : {}),
                ...(body.headings ? { headings: body.headings } : {}),
                ...(body.images ? { images: body.images } : {}),
                ...(body.internalLinks ? { internalLinks: body.internalLinks } : {}),
                ...(body.externalLinks ? { externalLinks: body.externalLinks } : {}),
                ...(body.publishedAt ? { publishedAt: body.publishedAt } : {}),
                ...(body.updatedAt ? { updatedAt: body.updatedAt } : {}),
                ...(body.contentType ? { contentType: body.contentType } : {}),
            });
            return json({ data: result });
        }
        catch (err) {
            return internalError(err, 'ai audit-document');
        }
    });
    /**
     * Internal link suggestions — Slice F.
     *
     * Given a body of content, scan published CMS documents for phrases the
     * author could link to (by title or declared keywords) and return a
     * ranked list of suggestions. The matching is deterministic (no LLM call)
     * but expensive enough that we cap the candidate set and the response
     * size to keep p99 under control.
     *
     * Optionally constrain candidates to a single collection (e.g. only
     * suggest links to `services` while editing a blog post).
     */
    /**
     * Inline AI co-author — text-transformation endpoints (Phase 2).
     *
     * Four micro-actions that operate on a chunk of text supplied by the
     * caller: rewrite, expand, compress, proofread. They wrap the existing
     * plugin-ai writing primitives so the admin and MCP clients have a
     * stable HTTP surface.
     *
     * Every action is rate-limited under the same bucket as content
     * generation (20 calls / hour / user) because each one hits the
     * configured LLM provider.
     */
    router.post('/ai/coauthor', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-coauthor:${auth.session.userId}`))) {
                return errorResponse('AI co-author rate limit reached. Try again in an hour.', 429);
            }
            const body = (await request.json());
            if (!body.text || typeof body.text !== 'string') {
                return errorResponse('text is required', 400);
            }
            if (body.text.length > AI_CONTEXT_MAX_CHARS) {
                return errorResponse(`text exceeds ${AI_CONTEXT_MAX_CHARS} character limit`, 400);
            }
            const action = body.action ?? 'rewrite';
            if (!['rewrite', 'expand', 'compress', 'proofread'].includes(action)) {
                return errorResponse(`Unknown action "${action}"`, 400);
            }
            let result;
            // Governed path first: route through the connection + model configured in
            // Settings > AI, enforcing the feature toggle/budget and recording usage.
            const { generateTextForFeature } = await import('../../ai/index.js');
            const { system, user, maxTokens } = buildCoauthorPrompt(action, body);
            const governed = await generateTextForFeature(db(), 'ai.features.contentWriting', {
                prompt: user,
                system,
                maxTokens,
                userId: auth.session.userId,
                useBrandVoice: true,
            });
            if (governed.ok) {
                result = { text: governed.text, ...(action === 'proofread' ? { changes: [] } : {}) };
            }
            else if (governed.code === 'feature_disabled') {
                return errorResponse(governed.message, 403);
            }
            else if (governed.code === 'budget_exceeded') {
                return errorResponse(governed.message, 429);
            }
            else if (governed.code === 'provider_error') {
                return errorResponse(`AI provider error: ${governed.message}`, 502);
            }
            else {
                // No governed provider/model/key yet → fall back to the legacy global
                // plugin-ai provider so existing installs keep working.
                let aiModule;
                try {
                    aiModule = await importAIPlugin();
                }
                catch {
                    return errorResponse('AI co-author is not configured. Connect a provider in Settings > AI ' +
                        'or install @actuate-media/plugin-ai.', 501);
                }
                try {
                    if (action === 'rewrite') {
                        if (typeof aiModule.rewrite !== 'function') {
                            return errorResponse('AI plugin missing `rewrite`.', 501);
                        }
                        const text = (await aiModule.rewrite(body.text, body.style, body.tone));
                        result = { text };
                    }
                    else if (action === 'expand') {
                        if (typeof aiModule.expand !== 'function') {
                            return errorResponse('AI plugin missing `expand`.', 501);
                        }
                        const text = (await aiModule.expand(body.text, body.instructions));
                        result = { text };
                    }
                    else if (action === 'compress') {
                        if (typeof aiModule.compress !== 'function') {
                            return errorResponse('AI plugin missing `compress`.', 501);
                        }
                        const text = (await aiModule.compress(body.text, body.targetLength));
                        result = { text };
                    }
                    else {
                        if (typeof aiModule.proofread !== 'function') {
                            return errorResponse('AI plugin missing `proofread`.', 501);
                        }
                        const r = (await aiModule.proofread(body.text));
                        result = { text: r.corrected, changes: r.changes };
                    }
                }
                catch (err) {
                    // Surface provider errors (missing API key, rate-limited upstream) with
                    // a meaningful status code instead of 500.
                    const msg = err instanceof Error ? err.message : 'unknown';
                    if (msg.toLowerCase().includes('not configured') ||
                        msg.toLowerCase().includes('missing')) {
                        return errorResponse(`AI provider is not configured: ${msg}`, 501);
                    }
                    throw err;
                }
            }
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: {
                    action: `ai_coauthor_${action}`,
                    inputChars: body.text.length,
                    outputChars: result.text.length,
                },
            });
            return json({ data: { action, ...result } });
        }
        catch (err) {
            return internalError(err, 'ai coauthor');
        }
    });
    router.post('/ai/suggest-internal-links', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (!(await checkRateLimitAsync(aiAnalysisLimiter, `ai-links:${auth.session.userId}`))) {
                return errorResponse('Internal-link suggestion rate limit reached. Try again later.', 429);
            }
            const body = (await request.json());
            if (!body.content || typeof body.content !== 'string') {
                return errorResponse('content is required', 400);
            }
            if (body.content.length > AI_ANALYSIS_MAX_CHARS) {
                return errorResponse(`content exceeds ${AI_ANALYSIS_MAX_CHARS} character limit`, 400);
            }
            const limit = Math.max(1, Math.min(50, typeof body.limit === 'number' ? body.limit : 10));
            let aiModule;
            try {
                aiModule = await importAIPlugin();
            }
            catch {
                return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to use internal link suggestions.', 501);
            }
            if (typeof aiModule?.suggestInternalLinks !== 'function') {
                return errorResponse('AI plugin missing `suggestInternalLinks`. Upgrade @actuate-media/plugin-ai to >= 0.3.0.', 501);
            }
            const linksIp = clientIp(request);
            await logEvent({
                event: 'ai_suggest_internal_links',
                userId: auth.session.userId,
                ipAddress: isResolvedIp(linksIp) ? linksIp : undefined,
                details: { ...(body.collection ? { collection: body.collection } : {}), limit },
            });
            const ctx = buildActionContext(auth.session, db());
            // Pull up to 200 published candidates from the requested collection
            // (or every readable collection when none is specified). We rely on
            // listDocuments' built-in access control so the agent can't surface
            // links to documents it isn't allowed to read.
            const cfg = getActuateConfig();
            const collectionSlugs = body.collection
                ? [body.collection]
                : Object.keys(cfg?.collections ?? {});
            const pages = [];
            for (const slug of collectionSlugs) {
                const scopeErr = requireCollectionScope(auth.session, slug, 'read');
                if (scopeErr)
                    continue;
                const listed = await listDocuments({ collection: slug, status: 'PUBLISHED', pageSize: 200 }, ctx);
                for (const doc of listed.docs) {
                    if (body.excludeId && doc.id === body.excludeId)
                        continue;
                    const data = (doc.data ?? {});
                    const titleStr = String(doc.title ?? data.title ?? data.name ?? '');
                    if (!titleStr.trim())
                        continue;
                    const slugStr = String(doc.slug ?? data.slug ?? doc.id);
                    const url = `/${slug}/${slugStr}`;
                    const keywords = [];
                    if (Array.isArray(data.keywords)) {
                        for (const k of data.keywords) {
                            if (typeof k === 'string' && k.trim())
                                keywords.push(k);
                        }
                    }
                    if (Array.isArray(data.tags)) {
                        for (const k of data.tags) {
                            if (typeof k === 'string' && k.trim())
                                keywords.push(k);
                        }
                    }
                    pages.push({
                        id: String(doc.id),
                        title: titleStr,
                        url,
                        ...(keywords.length > 0 ? { keywords } : {}),
                    });
                }
            }
            const suggestions = await aiModule.suggestInternalLinks(body.content, pages);
            const limited = suggestions.slice(0, limit);
            return json({
                data: {
                    suggestions: limited,
                    candidatesScanned: pages.length,
                },
            });
        }
        catch (err) {
            return internalError(err, 'ai suggest-internal-links');
        }
    });
    /**
     * Generate accessible alt text (and a derived title) for an image asset
     * using the AI vision provider. Replaces the old client-side filename mock.
     * Returns 501 when the AI plugin/provider isn't configured so the admin can
     * fall back gracefully instead of silently failing.
     */
    router.post('/ai/media-metadata', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireMediaScope(auth.session);
            if (scopeErr)
                return scopeErr;
            if (!(await checkRateLimitAsync(aiGenerateLimiter, `ai-media-metadata:${auth.session.userId}`))) {
                return errorResponse('AI media metadata rate limit reached. Try again in an hour.', 429);
            }
            const body = (await request.json());
            // Resolve a safe image URL. Prefer a media id we own; only accept a raw
            // URL if it passes the storage allowlist (SSRF guard).
            let imageUrl = '';
            let mimeType = '';
            if (body.mediaId) {
                const media = await db().media.findUnique({ where: { id: body.mediaId } });
                if (!media)
                    return errorResponse('Media not found', 404);
                mimeType = media.mimeType;
                imageUrl = media.publicUrl ?? mediaUrl(media.storageKey);
            }
            else if (body.imageUrl) {
                if (!isAllowedStorageUrl(body.imageUrl)) {
                    return errorResponse('Invalid image URL', 400);
                }
                imageUrl = body.imageUrl;
            }
            else {
                return errorResponse('mediaId or imageUrl is required', 400);
            }
            if (mimeType && !mimeType.startsWith('image/')) {
                return errorResponse('AI metadata is only available for images', 400);
            }
            let aiModule;
            try {
                aiModule = await importAIPlugin();
            }
            catch {
                return errorResponse('AI plugin is not installed. Install @actuate-media/plugin-ai to generate media metadata.', 501);
            }
            if (typeof aiModule.generateAltText !== 'function') {
                return errorResponse('AI plugin missing `generateAltText`.', 501);
            }
            // Fetch the image bytes through the SSRF-safe fetch (re-validates + DNS-
            // resolves the host, defeating rebinding, and disables redirects) so the
            // vision model analyzes the ACTUAL image instead of hallucinating from the
            // URL string. Best-effort: any failure falls back to a context heuristic.
            const image = await fetchImageForVision(imageUrl, mimeType);
            let alt = '';
            let alternatives = [];
            try {
                const r = (await aiModule.generateAltText(imageUrl, {
                    pageTitle: body.pageTitle,
                    ...(image ? { image } : {}),
                }));
                alt = (r?.altText ?? '').trim();
                alternatives = Array.isArray(r?.alternatives) ? r.alternatives : [];
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : 'unknown';
                if (msg.toLowerCase().includes('not configured') ||
                    msg.toLowerCase().includes('missing') ||
                    msg.toLowerCase().includes('api key')) {
                    return errorResponse(`AI provider is not configured: ${msg}`, 501);
                }
                throw err;
            }
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: { action: 'ai_media_metadata', mediaId: body.mediaId ?? null },
            });
            return json({ data: { alt, title: deriveMediaTitleFromAlt(alt), alternatives } });
        }
        catch (err) {
            return internalError(err, 'ai media-metadata');
        }
    });
}
//# sourceMappingURL=saved-sections.js.map