import { updateDocumentSeoFields } from '../../actions.js';
import { logEvent } from '../../security/audit.js';
import { processSeoScan } from '../../cron/index.js';
import { isResolvedIp } from '../../security/client-ip.js';
import { safeFetch, SsrfBlockedError } from '../../security/safe-fetch.js';
import { getActuateConfig } from '../../config/runtime.js';
import { guardedDb } from '../guarded-db.js';
import { json, errorResponse, internalError, asRecord, normalizeSeoPage, hasModel, formatCompactNumber, modelNotAvailable, requireAuth, buildActionContext, WRITE_ROLES, requireRole, linkHealthLimiter, seoAuditLimiter, checkRateLimitAsync, clientIp, enforceDocumentReadAccess, } from '../route-helpers.js';
// Explicit risk order. A plain `severity: 'asc'` DB sort orders the values
// alphabetically (critical, info, warning) which wrongly ranks `info` above
// `warning`. Unknown severities sort last.
const SEVERITY_RANK = { critical: 0, warning: 1, info: 2 };
function severityRank(severity) {
    return SEVERITY_RANK[String(severity)] ?? 99;
}
/** Sort issues by risk (critical → warning → info), then most-recent first. */
export function sortIssuesBySeverity(issues) {
    return [...issues].sort((a, b) => {
        const rankDiff = severityRank(a.severity) - severityRank(b.severity);
        if (rankDiff !== 0)
            return rankDiff;
        const at = a.lastDetectedAt ? new Date(a.lastDetectedAt).getTime() : 0;
        const bt = b.lastDetectedAt ? new Date(b.lastDetectedAt).getTime() : 0;
        return bt - at;
    });
}
export function registerSeoRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // SEO routes
    // ---------------------------------------------------------------------------
    router.get('/seo/pages', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const pages = await db().document.findMany({
                where: { deletedAt: null, status: 'PUBLISHED' },
                select: {
                    id: true,
                    collection: true,
                    data: true,
                    updatedAt: true,
                    structuredData: true,
                },
                orderBy: { updatedAt: 'desc' },
            });
            return json({ data: pages.map(normalizeSeoPage) });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.get('/seo/link-health', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            // EDITOR+ only — this endpoint hits arbitrary URLs and can be expensive.
            const roleErr = requireRole(auth.session.role, WRITE_ROLES);
            if (roleErr)
                return roleErr;
            // Tight rate limit because each call fans out to many outbound requests.
            const ip = clientIp(request);
            const rateKey = isResolvedIp(ip)
                ? `link-health:${ip}`
                : `link-health-user:${auth.session.userId}`;
            if (!(await checkRateLimitAsync(linkHealthLimiter, rateKey))) {
                return errorResponse('Too many link-health scans. Please try again later.', 429);
            }
            const MAX_LINKS_PER_PAGE = 50;
            const MAX_TOTAL_LINKS = 500;
            const PER_LINK_TIMEOUT_MS = 4000;
            const CONCURRENCY = 8;
            const docs = await db().document.findMany({
                where: { deletedAt: null, status: 'PUBLISHED' },
                select: { id: true, title: true, data: true, collection: true },
            });
            const urlRegex = /https?:\/\/[^\s"'<>]+/g;
            const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? '';
            const queue = [];
            const seenGlobal = new Set();
            outer: for (const doc of docs) {
                const pageTitle = doc.title ?? doc.data?.title ?? doc.id;
                const content = JSON.stringify(doc.data ?? {});
                const urls = content.match(urlRegex) ?? [];
                const seenInPage = new Set();
                let countInPage = 0;
                for (const url of urls) {
                    const clean = url.replace(/[",;)}\]]+$/, '');
                    if (seenInPage.has(clean))
                        continue;
                    seenInPage.add(clean);
                    if (seenGlobal.has(clean))
                        continue;
                    seenGlobal.add(clean);
                    if (countInPage >= MAX_LINKS_PER_PAGE)
                        break;
                    if (queue.length >= MAX_TOTAL_LINKS)
                        break outer;
                    const isInternal = !!siteUrl && clean.startsWith(siteUrl);
                    queue.push({ docId: doc.id, pageTitle, clean, isInternal });
                    countInPage++;
                }
            }
            const linkResults = [];
            let cursor = 0;
            const worker = async () => {
                while (cursor < queue.length) {
                    const idx = cursor++;
                    const job = queue[idx];
                    let status = 0;
                    try {
                        // safeFetch rejects private/loopback IPs and disables redirect
                        // following so 302->internal can't smuggle the scanner past SSRF.
                        const resp = await safeFetch(job.clean, {
                            method: 'HEAD',
                            timeoutMs: PER_LINK_TIMEOUT_MS,
                        });
                        status = resp.status;
                        // Drain the body so the connection can be reused.
                        try {
                            await resp.body?.cancel();
                        }
                        catch {
                            /* noop */
                        }
                    }
                    catch (err) {
                        status = err instanceof SsrfBlockedError ? -1 : 0;
                    }
                    if (status === -1 || status === 0 || status >= 300) {
                        linkResults.push({
                            id: `${job.docId}-${idx}`,
                            page: job.pageTitle,
                            url: job.clean,
                            status: status === -1 ? 0 : status,
                            type: job.isInternal ? 'internal' : 'external',
                        });
                    }
                }
            };
            await Promise.all(Array.from({ length: Math.min(CONCURRENCY, queue.length) }, worker));
            return json({
                data: {
                    truncated: queue.length >= MAX_TOTAL_LINKS,
                    checked: queue.length,
                    issues: linkResults,
                },
            });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.post('/seo/scan', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, WRITE_ROLES);
            if (roleErr)
                return roleErr;
            // Expensive: loads + regex-scans every published doc. Rate-limit it (same
            // budget as the full audit) so it can't be hammered into an OOM/CPU spike.
            const scanIp = clientIp(request);
            const scanKey = isResolvedIp(scanIp)
                ? `seo-scan:${scanIp}`
                : `seo-scan-user:${auth.session.userId}`;
            if (!(await checkRateLimitAsync(seoAuditLimiter, scanKey))) {
                return errorResponse('Too many scans. Please try again later.', 429);
            }
            // Scope the scan to configured front-end content collections so structural
            // (navigation/footer) and benchmark documents are never reported. Shares
            // the exact scan logic with the cron path via processSeoScan.
            const { frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
            const allowedCollections = Object.keys(frontEndContentCollectionTypes(getActuateConfig()));
            const result = await processSeoScan(db(), { allowedCollections, maxDocuments: 5000 });
            return json({ data: result });
        }
        catch (err) {
            return internalError(err);
        }
    });
    // ---------------------------------------------------------------------------
    // SEO Operations Center — audit engine, issues, overview, content records
    // ---------------------------------------------------------------------------
    function seoIssueToApi(i) {
        return {
            id: i.id,
            auditRunId: i.auditRunId ?? null,
            entityType: i.entityType ?? null,
            entityId: i.entityId ?? null,
            entityTitle: i.entityTitle ?? null,
            url: i.url ?? null,
            severity: i.severity,
            category: i.category,
            type: i.type,
            title: i.title,
            description: i.description ?? null,
            impact: i.impact ?? null,
            recommendation: i.recommendation ?? null,
            status: i.status,
            fixable: i.fixable ?? false,
            fixActionType: i.fixActionType ?? null,
            relatedEntityIds: Array.isArray(i.relatedEntityIds) ? i.relatedEntityIds : [],
            source: i.source ?? 'audit',
            firstDetectedAt: i.firstDetectedAt ?? null,
            lastDetectedAt: i.lastDetectedAt ?? null,
            resolvedAt: i.resolvedAt ?? null,
        };
    }
    async function buildSeoContentRecords() {
        const { gatherAuditEntities, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
        const { calculatePageSeoScore } = await import('../../seo/score.js');
        const collectionTypes = frontEndContentCollectionTypes(getActuateConfig());
        const entities = await gatherAuditEntities(db(), { collectionTypes });
        return entities.map((e) => {
            const score = calculatePageSeoScore(e);
            return {
                id: `${e.entityType}:${e.entityId}`,
                entityType: e.entityType,
                entityId: e.entityId,
                title: e.title,
                url: e.url,
                canonicalUrl: e.canonicalUrl ?? null,
                metaTitle: e.metaTitle ?? null,
                metaDescription: e.metaDescription ?? null,
                focusKeyword: e.focusKeyword ?? null,
                ogTitle: e.ogTitle ?? null,
                ogDescription: e.ogDescription ?? null,
                ogImageId: null,
                ogImage: e.ogImage ?? null,
                twitterTitle: null,
                twitterDescription: null,
                twitterImageId: null,
                noindex: e.noindex ?? false,
                nofollow: false,
                structuredDataType: e.structuredDataType ?? null,
                structuredData: null,
                seoScore: score.score,
                seoGrade: score.grade,
                updatedAt: null,
            };
        });
    }
    // Demo/seed search-performance rows live in a hidden collection so they can
    // be inserted by the dev seed script and never ship to npm. In production
    // with a connected Search Console these would come from the GSC API.
    const SEO_PERF_COLLECTION = '__seo_perf__';
    async function loadSearchPerformance(limit = 10) {
        try {
            const docs = await db().document.findMany({
                where: { collection: SEO_PERF_COLLECTION, deletedAt: null },
                select: { data: true },
            });
            const rows = docs
                .map((d) => {
                const data = asRecord(d.data);
                return {
                    url: String(data.url ?? ''),
                    title: String(data.title ?? ''),
                    impressions: Number(data.impressions ?? 0),
                    clicks: Number(data.clicks ?? 0),
                    ctr: Number(data.ctr ?? 0),
                    averagePosition: Number(data.averagePosition ?? 0),
                };
            })
                .filter((r) => r.url);
            rows.sort((a, b) => b.impressions - a.impressions);
            return rows.slice(0, limit);
        }
        catch {
            return [];
        }
    }
    router.post('/seo/audit', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, WRITE_ROLES);
            if (roleErr)
                return roleErr;
            if (!hasModel(db(), 'seoAuditRun') || !hasModel(db(), 'seoIssue')) {
                return modelNotAvailable('SeoAuditRun');
            }
            const ip = clientIp(request);
            const rateKey = isResolvedIp(ip) ? `seo-audit:${ip}` : `seo-audit-user:${auth.session.userId}`;
            if (!(await checkRateLimitAsync(seoAuditLimiter, rateKey))) {
                return errorResponse('Too many audits. Please try again later.', 429);
            }
            const body = (await request.json().catch(() => ({})));
            const scope = body?.scope ?? 'full';
            const { runAndPersistAudit, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
            const run = await runAndPersistAudit(db(), {
                scope,
                startedById: auth.session.userId,
                collectionTypes: frontEndContentCollectionTypes(getActuateConfig()),
            });
            try {
                await logEvent({
                    event: 'settings_changed',
                    userId: auth.session.userId,
                    details: { action: 'seo_audit', auditRunId: run.id, scope },
                });
            }
            catch {
                /* audit-log failures never block */
            }
            return json({ data: run });
        }
        catch (err) {
            return internalError(err, 'seo/audit');
        }
    });
    router.get('/seo/audits', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (!hasModel(db(), 'seoAuditRun'))
                return json({ data: { runs: [] } });
            const runs = await db().seoAuditRun.findMany({
                orderBy: { startedAt: 'desc' },
                take: 50,
            });
            return json({ data: { runs } });
        }
        catch (err) {
            return internalError(err, 'seo/audits');
        }
    });
    router.get('/seo/audits/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (!hasModel(db(), 'seoAuditRun'))
                return modelNotAvailable('SeoAuditRun');
            const run = await db().seoAuditRun.findUnique({ where: { id: params.id } });
            if (!run)
                return errorResponse('Audit run not found', 404);
            return json({ data: run });
        }
        catch (err) {
            return internalError(err, 'seo/audits/:id');
        }
    });
    router.get('/seo/issues', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (!hasModel(db(), 'seoIssue'))
                return json({ data: { issues: [] } });
            const url = new URL(request.url);
            const where = {};
            const severity = url.searchParams.get('severity');
            const category = url.searchParams.get('category');
            const status = url.searchParams.get('status');
            const auditRunId = url.searchParams.get('auditRunId');
            if (severity)
                where.severity = severity;
            if (category)
                where.category = category;
            if (status)
                where.status = status;
            else
                where.status = { in: ['open', 'recurring'] };
            if (auditRunId)
                where.auditRunId = auditRunId;
            const issues = await db().seoIssue.findMany({
                where,
                orderBy: { lastDetectedAt: 'desc' },
                take: 500,
            });
            return json({ data: { issues: sortIssuesBySeverity(issues).map(seoIssueToApi) } });
        }
        catch (err) {
            return internalError(err, 'seo/issues');
        }
    });
    router.get('/seo/issues/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (!hasModel(db(), 'seoIssue'))
                return modelNotAvailable('SeoIssue');
            const issue = await db().seoIssue.findUnique({ where: { id: params.id } });
            if (!issue)
                return errorResponse('Issue not found', 404);
            return json({ data: seoIssueToApi(issue) });
        }
        catch (err) {
            return internalError(err, 'seo/issues/:id');
        }
    });
    async function setIssueStatus(request, issueId, status) {
        const auth = await requireAuth(request);
        if (auth.error)
            return auth.error;
        const roleErr = requireRole(auth.session.role, WRITE_ROLES);
        if (roleErr)
            return roleErr;
        if (!hasModel(db(), 'seoIssue'))
            return modelNotAvailable('SeoIssue');
        const existing = await db().seoIssue.findUnique({ where: { id: issueId } });
        if (!existing)
            return errorResponse('Issue not found', 404);
        const issue = await db().seoIssue.update({
            where: { id: issueId },
            data: {
                status,
                resolvedAt: status === 'resolved' ? new Date() : null,
                resolvedById: status === 'resolved' ? auth.session.userId : null,
            },
        });
        try {
            await logEvent({
                event: 'settings_changed',
                userId: auth.session.userId,
                details: { action: 'seo_issue_status', issueId, status },
            });
        }
        catch {
            /* noop */
        }
        return json({ data: seoIssueToApi(issue) });
    }
    router.post('/seo/issues/:id/resolve', (request, params) => setIssueStatus(request, params.id, 'resolved').catch((e) => internalError(e)));
    router.post('/seo/issues/:id/ignore', (request, params) => setIssueStatus(request, params.id, 'ignored').catch((e) => internalError(e)));
    router.post('/seo/issues/:id/reopen', (request, params) => setIssueStatus(request, params.id, 'open').catch((e) => internalError(e)));
    router.get('/seo/overview', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const { resolveSeoModuleSettings, getSeoOverrides } = await import('../../seo/config-store.js');
            const { buildQuickWins } = await import('../../seo/audit-engine.js');
            const { gradeForSeoScore } = await import('../../seo/score.js');
            const overrides = await getSeoOverrides(db()).catch(() => null);
            const moduleSettings = resolveSeoModuleSettings(overrides);
            let lastRun = null;
            let openIssues = [];
            if (hasModel(db(), 'seoAuditRun')) {
                lastRun = await db().seoAuditRun.findFirst({
                    where: { status: 'COMPLETED' },
                    orderBy: { startedAt: 'desc' },
                });
            }
            if (hasModel(db(), 'seoIssue')) {
                const rawIssues = await db().seoIssue.findMany({
                    where: { status: { in: ['open', 'recurring'] } },
                    orderBy: { lastDetectedAt: 'desc' },
                    take: 200,
                });
                openIssues = sortIssuesBySeverity(rawIssues);
            }
            const records = await buildSeoContentRecords().catch(() => []);
            const indexable = records.filter((r) => !r.noindex).length;
            const perf = await loadSearchPerformance(10);
            const summary = {
                total: openIssues.length,
                critical: openIssues.filter((i) => i.severity === 'critical').length,
                warning: openIssues.filter((i) => i.severity === 'warning').length,
                info: openIssues.filter((i) => i.severity === 'info').length,
            };
            const scoreAfter = lastRun?.siteScoreAfter ?? null;
            const scoreBefore = lastRun?.siteScoreBefore ?? null;
            const breakdown = lastRun?.metadata?.breakdown ?? [];
            const avgPosition = perf.length > 0
                ? perf.reduce((s, r) => s + r.averagePosition, 0) / perf.length
                : null;
            const weeklyImpressions = perf.reduce((s, r) => s + r.impressions, 0);
            const metrics = [
                {
                    label: 'Pages Indexed',
                    value: String(indexable),
                    helper: `${records.length} total pages`,
                    trend: 'flat',
                },
                {
                    label: 'Open Issues',
                    value: String(summary.total),
                    helper: `${summary.critical} critical`,
                    trend: summary.critical > 0 ? 'down' : 'flat',
                },
                {
                    label: 'Avg. Position',
                    value: avgPosition !== null ? avgPosition.toFixed(1) : '—',
                    helper: moduleSettings.searchConsoleConnected ? 'last 28 days' : 'Connect Search Console',
                    trend: 'flat',
                },
                {
                    label: 'Weekly Impressions',
                    value: weeklyImpressions > 0 ? formatCompactNumber(weeklyImpressions) : '—',
                    helper: moduleSettings.searchConsoleConnected ? 'last 28 days' : 'Connect Search Console',
                    trend: 'flat',
                },
            ];
            const quickWins = buildQuickWins(openIssues.map((i) => ({
                issueKey: i.issueKey,
                entityType: i.entityType,
                entityId: i.entityId,
                entityTitle: i.entityTitle,
                url: i.url,
                severity: i.severity,
                category: i.category,
                type: i.type,
                title: i.title,
                description: i.description ?? '',
                impact: i.impact ?? 'Low',
                recommendation: i.recommendation ?? '',
                fixable: i.fixable ?? false,
                fixActionType: i.fixActionType ?? null,
                relatedEntityIds: Array.isArray(i.relatedEntityIds) ? i.relatedEntityIds : [],
            }))).map((w) => ({
                id: w.id,
                title: w.title,
                explanation: w.explanation,
                impact: w.impact,
                issueId: openIssues.find((i) => i.type === w.issueType)?.id ?? null,
                fixActionType: w.fixActionType,
            }));
            return json({
                data: {
                    siteScore: {
                        score: scoreAfter ?? 0,
                        grade: gradeForSeoScore(scoreAfter ?? 0),
                        delta: scoreAfter !== null && scoreBefore !== null ? scoreAfter - scoreBefore : null,
                        breakdown,
                    },
                    metrics,
                    issues: openIssues.slice(0, 7).map(seoIssueToApi),
                    issuesSummary: summary,
                    quickWins,
                    searchPerformance: perf,
                    searchConsoleConnected: moduleSettings.searchConsoleConnected,
                    lastAuditRunId: lastRun?.id ?? null,
                },
            });
        }
        catch (err) {
            return internalError(err, 'seo/overview');
        }
    });
    router.get('/seo/search-performance', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const url = new URL(request.url);
            const limit = Math.min(50, Math.max(1, Number(url.searchParams.get('limit') ?? 10)));
            const rows = await loadSearchPerformance(limit);
            return json({ data: { rows } });
        }
        catch (err) {
            return internalError(err, 'seo/search-performance');
        }
    });
    router.get('/seo/content', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const records = await buildSeoContentRecords();
            return json({ data: { records } });
        }
        catch (err) {
            return internalError(err, 'seo/content');
        }
    });
    router.get('/seo/content/:entityType/:entityId', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const records = await buildSeoContentRecords();
            const record = records.find((r) => r.entityType === params.entityType && r.entityId === params.entityId);
            if (!record)
                return errorResponse('Content record not found', 404);
            return json({ data: record });
        }
        catch (err) {
            return internalError(err, 'seo/content/:id');
        }
    });
    router.put('/seo/content/:entityType/:entityId', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const roleErr = requireRole(auth.session.role, WRITE_ROLES);
            if (roleErr)
                return roleErr;
            const body = (await request.json().catch(() => null));
            if (!body || typeof body !== 'object') {
                return errorResponse('Request body must be an object', 400);
            }
            const doc = await db().document.findFirst({
                where: { id: params.entityId, deletedAt: null },
                select: { id: true, collection: true },
            });
            if (!doc)
                return errorResponse('Document not found', 404);
            // String SEO fields: payload key -> stored data key, each with a max
            // length so we never persist unbounded/megabyte values into the content
            // JSON. Canonical is stored as `canonical` to match normalizeSeoPage.
            const TEXT_FIELDS = {
                metaTitle: { target: 'metaTitle', max: 300 },
                metaDescription: { target: 'metaDescription', max: 1000 },
                canonicalUrl: { target: 'canonical', max: 2000 },
                focusKeyword: { target: 'focusKeyword', max: 200 },
                ogTitle: { target: 'ogTitle', max: 300 },
                ogDescription: { target: 'ogDescription', max: 1000 },
                ogImage: { target: 'ogImage', max: 2000 },
                twitterTitle: { target: 'twitterTitle', max: 300 },
                twitterDescription: { target: 'twitterDescription', max: 1000 },
                twitterImage: { target: 'twitterImage', max: 2000 },
                structuredDataType: { target: 'structuredDataType', max: 100 },
            };
            const patch = {};
            for (const [key, { target, max }] of Object.entries(TEXT_FIELDS)) {
                if (!(key in body))
                    continue;
                const value = body[key];
                if (value === null || value === '') {
                    patch[target] = '';
                    continue;
                }
                if (typeof value !== 'string') {
                    return errorResponse(`Field "${key}" must be a string`, 400);
                }
                if (value.length > max) {
                    return errorResponse(`Field "${key}" exceeds the ${max}-character limit`, 400);
                }
                patch[target] = value;
            }
            if ('noindex' in body)
                patch.noindex = body.noindex === true;
            if ('nofollow' in body)
                patch.nofollow = body.nofollow === true;
            if ('structuredData' in body) {
                const sd = body.structuredData;
                if (sd !== null && (typeof sd !== 'object' || Array.isArray(sd))) {
                    return errorResponse('structuredData must be a JSON object', 400);
                }
                if (sd && JSON.stringify(sd).length > 50_000) {
                    return errorResponse('structuredData payload is too large', 400);
                }
                patch.structuredData = sd;
            }
            if (Object.keys(patch).length === 0) {
                return errorResponse('No recognized SEO fields in request body', 400);
            }
            // Route through the data layer so the edit gets per-collection access
            // control, a Version snapshot, search re-indexing and the afterUpdate
            // hook — never write document.data directly. (updateDocumentSeoFields
            // preserves the undeclared SEO convention keys that the generic
            // updateDocument field-access layer would strip.)
            const ctx = buildActionContext(auth.session, db());
            try {
                await updateDocumentSeoFields(doc.collection, doc.id, patch, ctx);
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : '';
                if (msg.includes('Access denied'))
                    return errorResponse('Insufficient permissions', 403);
                if (msg.includes('not found'))
                    return errorResponse('Document not found', 404);
                throw err;
            }
            try {
                await logEvent({
                    event: 'document_updated',
                    userId: auth.session.userId,
                    details: { action: 'seo_fields_update', documentId: doc.id },
                });
            }
            catch {
                /* noop */
            }
            const records = await buildSeoContentRecords();
            const record = records.find((r) => r.entityId === doc.id);
            return json({ data: record ?? { ok: true } });
        }
        catch (err) {
            return internalError(err, 'seo/content PUT');
        }
    });
    // ---------------------------------------------------------------------------
    // SEO analysis, readability, internal links, schema, meta
    // ---------------------------------------------------------------------------
    router.get('/seo/analysis/:documentId', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const doc = await db().document.findFirst({
                where: { id: params.documentId, deletedAt: null },
            });
            if (!doc)
                return errorResponse('Not found', 404);
            const accessErr = await enforceDocumentReadAccess(auth.session, doc.collection);
            if (accessErr)
                return accessErr;
            const { analyzeContent } = await import('../../seo/analysis.js');
            const data = doc.data || {};
            const result = analyzeContent({
                title: doc.title || data.title || '',
                slug: doc.slug || data.slug || '',
                content: data.content || data.body || '',
                metaTitle: data.metaTitle || data.seoTitle,
                metaDescription: data.metaDescription || data.seoDescription,
                focusKeyphrase: data.focusKeyphrase,
                canonical: data.canonical,
                ogTitle: data.ogTitle,
                ogDescription: data.ogDescription,
                ogImage: data.ogImage,
                isCornerstone: data.isCornerstone,
            });
            return new Response(JSON.stringify(result), {
                status: 200,
                headers: { 'Content-Type': 'application/json' },
            });
        }
        catch (err) {
            return internalError(err, 'seo/analysis');
        }
    });
    router.get('/seo/readability/:documentId', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const doc = await db().document.findFirst({
                where: { id: params.documentId, deletedAt: null },
            });
            if (!doc)
                return errorResponse('Not found', 404);
            const accessErr = await enforceDocumentReadAccess(auth.session, doc.collection);
            if (accessErr)
                return accessErr;
            const { calculateReadability, stripHtmlTags } = await import('../../seo/analysis.js');
            const data = doc.data || {};
            const text = stripHtmlTags(data.content || data.body || '');
            const result = calculateReadability(text);
            return new Response(JSON.stringify(result), {
                status: 200,
                headers: { 'Content-Type': 'application/json' },
            });
        }
        catch (err) {
            return internalError(err, 'seo/readability');
        }
    });
    router.get('/seo/internal-links/:documentId', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const doc = await db().document.findFirst({
                where: { id: params.documentId, deletedAt: null },
            });
            if (!doc)
                return errorResponse('Not found', 404);
            const accessErr = await enforceDocumentReadAccess(auth.session, doc.collection);
            if (accessErr)
                return accessErr;
            const data = doc.data || {};
            const title = doc.title || data.title || '';
            const keywords = title
                .split(/\s+/)
                .filter((w) => w.length > 3)
                .slice(0, 5);
            if (keywords.length === 0) {
                return new Response(JSON.stringify({ suggestions: [] }), {
                    status: 200,
                    headers: { 'Content-Type': 'application/json' },
                });
            }
            const suggestions = await db().document.findMany({
                where: {
                    id: { not: params.documentId },
                    status: 'PUBLISHED',
                    deletedAt: null,
                    OR: keywords.map((kw) => ({
                        title: { contains: kw, mode: 'insensitive' },
                    })),
                },
                select: { id: true, title: true, slug: true, collection: true },
                take: 10,
                orderBy: { updatedAt: 'desc' },
            });
            return new Response(JSON.stringify({ suggestions }), {
                status: 200,
                headers: { 'Content-Type': 'application/json' },
            });
        }
        catch (err) {
            return internalError(err, 'seo/internal-links');
        }
    });
    router.get('/llms.txt', async () => {
        try {
            // Public surface: must exclude soft-deleted docs and internal/system
            // collections (e.g. `__seo_config__`). Over-fetch then filter so the
            // 50-page cap still lands on real content after exclusion.
            const { isInternalCollection } = await import('../../seo/audit-runner.js');
            const rawDocs = await db().document.findMany({
                where: { status: 'PUBLISHED', deletedAt: null },
                select: { title: true, slug: true, collection: true, updatedAt: true, data: true },
                orderBy: { updatedAt: 'desc' },
                take: 200,
            });
            const docs = rawDocs.filter((d) => !isInternalCollection(d.collection)).slice(0, 50);
            const { generateLlmsTxt } = await import('../../seo/llms-txt.js');
            const pages = docs.map((d) => {
                const data = d.data || {};
                return {
                    title: d.title || data.title || 'Untitled',
                    url: `/${d.collection}/${d.slug || d.title?.toLowerCase().replace(/\s+/g, '-')}`,
                    description: data.metaDescription || data.seoDescription || '',
                    collection: d.collection,
                    updatedAt: d.updatedAt?.toISOString(),
                    isCornerstone: data.isCornerstone === true,
                };
            });
            const siteUrl = '';
            const txt = generateLlmsTxt({
                siteName: 'Actuate CMS',
                siteUrl,
                siteDescription: 'Content managed by Actuate CMS',
                maxPages: 50,
            }, pages);
            return new Response(txt, {
                status: 200,
                headers: { 'Content-Type': 'text/plain; charset=utf-8' },
            });
        }
        catch (err) {
            return internalError(err, 'llms.txt');
        }
    });
}
//# sourceMappingURL=seo.js.map