import { listDocuments, getDocument, createDocument, updateDocument, deleteDocument, } from '../../actions.js';
import { logEvent } from '../../security/audit.js';
import { applyFieldAccess } from '../../security/access.js';
import { ACTUATE_TAG_ROOT, actuateTag, cachedRead } from '../../cache/http.js';
import { notifyPreviewUpdate } from '../../preview/event-bus.js';
import { getActuateConfig } from '../../config/runtime.js';
import { guardedDb } from '../guarded-db.js';
import { maybePopulate, json, jsonWithCache, errorResponse, internalError, actionError, clampPageSize, requireAuth, requireCollectionScope, buildActionContext, } from '../route-helpers.js';
/**
 * Parse `?filter.<field>=<value>` query params into validated data-field
 * filters (admin facet filters, taxonomy Phase B). Only fields declared in the
 * collection config with short-label types are honored — undeclared or
 * long-form fields are silently dropped so a stale URL never 500s a list view.
 */
function parseDataFilters(url, collectionSlug) {
    const fields = getActuateConfig()?.collections?.[collectionSlug]?.fields;
    if (!fields)
        return undefined;
    const out = [];
    for (const [param, raw] of url.searchParams.entries()) {
        if (!param.startsWith('filter.') || out.length >= 5)
            continue;
        const fieldName = param.slice('filter.'.length);
        const value = raw.trim().slice(0, 200);
        if (!value)
            continue;
        const field = fields[fieldName];
        if (!field)
            continue;
        if (field.type === 'text' || field.type === 'select') {
            out.push({ field: fieldName, value, kind: 'scalar' });
        }
        else if (field.type === 'array') {
            out.push({ field: fieldName, value, kind: 'array' });
        }
    }
    return out.length > 0 ? out : undefined;
}
export function registerDocumentRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // Document routes
    // ---------------------------------------------------------------------------
    router.get('/collections/:slug', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.slug, 'read');
            if (scopeErr)
                return scopeErr;
            return cachedRead(request, {
                cache: getActuateConfig()?.cache,
                tags: [ACTUATE_TAG_ROOT, actuateTag('collection', params.slug)],
                wrap: jsonWithCache,
                build: async () => {
                    const url = new URL(request.url);
                    const ctx = buildActionContext(auth.session, db(), url.searchParams.get('locale') ?? undefined);
                    const result = await listDocuments({
                        collection: params.slug,
                        page: Number(url.searchParams.get('page')) || 1,
                        pageSize: clampPageSize(url.searchParams.get('pageSize')),
                        sort: url.searchParams.get('sort') ?? undefined,
                        order: url.searchParams.get('order') ?? undefined,
                        status: url.searchParams.get('status') ?? undefined,
                        locale: url.searchParams.get('locale') ?? undefined,
                        folderId: url.searchParams.get('folderId') ?? undefined,
                        // Exact slug filter — powers slug→id resolution for the MCP
                        // relationship resolver (WS-C C2). Additive: omitted ⇒ no filter.
                        slug: url.searchParams.get('slug') ?? undefined,
                        dataFilters: parseDataFilters(url, params.slug),
                    }, ctx);
                    const collectionConfig = getActuateConfig()?.collections?.[params.slug];
                    const fields = collectionConfig?.fields;
                    if (fields && result.docs.length > 0) {
                        const user = { id: auth.session.userId, role: auth.session.role };
                        result.docs = await Promise.all(result.docs.map(async (doc) => {
                            if (doc.data && typeof doc.data === 'object') {
                                return {
                                    ...doc,
                                    data: await applyFieldAccess('read', fields, doc.data, user),
                                };
                            }
                            return doc;
                        }));
                    }
                    return { data: result };
                },
            });
        }
        catch (err) {
            return internalError(err);
        }
    });
    // Distinct values in use for one field across a collection — powers tag
    // autocomplete and category suggestions in the admin ("UpChat" taxonomy
    // feedback: avoid `AI` / `ai` / `Artificial Intelligence` drift). Registered
    // BEFORE `/collections/:slug/:id` so the literal segment wins the match.
    router.get('/collections/:slug/field-values', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.slug, 'read');
            if (scopeErr)
                return scopeErr;
            const url = new URL(request.url);
            const fieldName = (url.searchParams.get('field') ?? '').trim();
            const query = (url.searchParams.get('q') ?? '').trim().slice(0, 100).toLowerCase();
            const collectionConfig = getActuateConfig()?.collections?.[params.slug];
            if (!collectionConfig)
                return errorResponse('Collection not found', 404);
            const field = collectionConfig.fields?.[fieldName];
            // Only fields whose values are short editorial labels are aggregatable.
            // Rejecting undeclared fields keeps this from probing arbitrary JSON.
            if (!field || !['text', 'select', 'array'].includes(field.type)) {
                return errorResponse('Field is not suggestible', 400);
            }
            const rows = await db().document.findMany({
                where: { collection: params.slug, deletedAt: null },
                select: { data: true },
                orderBy: { updatedAt: 'desc' },
                // Aggregation cap: recent documents dominate active vocabularies.
                take: 1000,
            });
            // For array fields, values live on each row — either plain strings or
            // objects keyed by the array's first text sub-field (e.g. `{ tag }`).
            const subKey = field.type === 'array'
                ? Object.entries(field.fields ?? {}).find(([, def]) => def?.type === 'text')?.[0]
                : undefined;
            const counts = new Map();
            const record = (raw) => {
                const value = typeof raw === 'string' ? raw.trim() : '';
                if (!value)
                    return;
                const key = value.toLowerCase();
                const existing = counts.get(key);
                if (existing)
                    existing.count += 1;
                else
                    counts.set(key, { value, count: 1 });
            };
            for (const row of rows) {
                const data = row.data;
                const raw = data?.[fieldName];
                if (field.type === 'array') {
                    if (!Array.isArray(raw))
                        continue;
                    for (const item of raw) {
                        if (typeof item === 'string')
                            record(item);
                        else if (item && typeof item === 'object' && subKey) {
                            record(item[subKey]);
                        }
                    }
                }
                else {
                    record(raw);
                }
            }
            const values = [...counts.values()]
                .filter((v) => !query || v.value.toLowerCase().includes(query))
                .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value))
                .slice(0, 25);
            return json({ data: { field: fieldName, values } });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.get('/collections/:slug/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.slug, 'read');
            if (scopeErr)
                return scopeErr;
            return cachedRead(request, {
                cache: getActuateConfig()?.cache,
                tags: [
                    ACTUATE_TAG_ROOT,
                    actuateTag('collection', params.slug),
                    actuateTag('doc', params.id),
                ],
                wrap: jsonWithCache,
                build: async () => {
                    const ctx = buildActionContext(auth.session, db());
                    const doc = await getDocument(params.slug, params.id, ctx);
                    if (!doc) {
                        // Returning a Response short-circuits the cache layer — we
                        // never cache 404s. The next request gets a fresh DB lookup
                        // so a doc that's restored from soft-delete is visible
                        // immediately.
                        return errorResponse('Document not found', 404);
                    }
                    // `getDocument` already lifts `_layout` / `_pageSettings` to
                    // the top-level page builder envelope and strips internal keys
                    // from `data`. Field-level access has been applied as well.
                    //
                    // Opt-in relation population (?populate=author,relatedPosts |
                    // *) replaces stored IDs with reference summaries. The
                    // `?populate=` param is part of the cache key (URL-derived),
                    // so populated and non-populated reads cache independently.
                    const populated = await maybePopulate(request, params.slug, doc, db());
                    return { data: populated };
                },
            });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.post('/collections/:slug', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.slug, 'create');
            if (scopeErr)
                return scopeErr;
            const body = (await request.json());
            const ctx = buildActionContext(auth.session, db());
            const doc = await createDocument(params.slug, body, ctx);
            await logEvent({
                event: 'document_created',
                userId: auth.session.userId,
                details: { collection: params.slug, documentId: doc?.id },
            });
            return json({ data: doc }, 201);
        }
        catch (err) {
            return actionError(err, 'create document');
        }
    });
    router.put('/collections/:slug/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.slug, 'update');
            if (scopeErr)
                return scopeErr;
            const body = (await request.json());
            const ctx = buildActionContext(auth.session, db());
            const doc = await updateDocument(params.slug, params.id, body, ctx);
            await logEvent({
                event: 'document_updated',
                userId: auth.session.userId,
                details: { collection: params.slug, documentId: params.id },
            });
            // Push to any open live-preview SSE streams. No-op when no admin is
            // watching the document; the bus drops the call cheaply.
            notifyPreviewUpdate(params.slug, params.id, {
                values: doc.data,
                status: doc.status,
                updatedAt: doc.updatedAt?.toISOString?.(),
            });
            return json({ data: doc });
        }
        catch (err) {
            return actionError(err, 'update document');
        }
    });
    router.delete('/collections/:slug/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const scopeErr = requireCollectionScope(auth.session, params.slug, 'delete');
            if (scopeErr)
                return scopeErr;
            const ctx = buildActionContext(auth.session, db());
            await deleteDocument(params.slug, params.id, ctx);
            await logEvent({
                event: 'document_deleted',
                userId: auth.session.userId,
                details: { collection: params.slug, documentId: params.id },
            });
            return json({ data: { success: true } });
        }
        catch (err) {
            return internalError(err, 'delete document');
        }
    });
}
//# sourceMappingURL=documents.js.map