import { getActuateConfig } from '../../config/runtime.js';
import { guardedDb } from '../guarded-db.js';
import { json, errorResponse, internalError, normalizeRedirect, hasModel, parseCsvLine, modelNotAvailable, requireAuth, WRITE_ROLES, ADMIN_ROLES, requireRole, } from '../route-helpers.js';
export function registerRedirectRoutes(router) {
    const db = guardedDb;
    // ---------------------------------------------------------------------------
    // Redirects routes
    // ---------------------------------------------------------------------------
    router.get('/redirects', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const redirects = await db().redirect.findMany({ orderBy: { createdAt: 'desc' } });
            return json({ data: redirects.map(normalizeRedirect) });
        }
        catch (err) {
            return internalError(err);
        }
    });
    router.post('/redirects', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (auth.session.role !== 'ADMIN')
                return errorResponse('Admin access required', 403);
            const body = (await request.json());
            const source = String(body.source ?? body.from ?? '').trim();
            const destination = String(body.destination ?? body.to ?? '').trim();
            const requestedStatus = Number(body.statusCode ?? body.type);
            if (!source || !destination) {
                return errorResponse('source and destination are required', 400);
            }
            // Open-redirect defence: relative destinations (`/foo`) are always
            // allowed; absolute destinations must point at an explicitly trusted
            // host. We compare on parsed origins (not string `startsWith`) so
            // `https://attacker.com.example.com` no longer passes a `startsWith`
            // check on `https://example.com`. Configure the allowlist via
            // `redirects.allowedExternalHosts` (string[] of hostnames).
            if (destination.startsWith('http://') || destination.startsWith('https://')) {
                let destUrl;
                try {
                    destUrl = new URL(destination);
                }
                catch {
                    return errorResponse('Invalid destination URL', 400);
                }
                if (!['http:', 'https:'].includes(destUrl.protocol)) {
                    return errorResponse('Invalid destination URL', 400);
                }
                const cmsConfig = getActuateConfig();
                const allowed = new Set([
                    ...(Array.isArray(cmsConfig?.redirects?.allowedExternalHosts)
                        ? cmsConfig.redirects.allowedExternalHosts.map((h) => h.toLowerCase())
                        : []),
                ]);
                const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
                if (siteUrl) {
                    try {
                        allowed.add(new URL(siteUrl).hostname.toLowerCase());
                    }
                    catch {
                        /* noop */
                    }
                }
                if (!allowed.has(destUrl.hostname.toLowerCase())) {
                    return errorResponse('External redirect destinations must be to an allowlisted host. Add the host to `redirects.allowedExternalHosts` in your CMS config.', 400);
                }
            }
            else if (!destination.startsWith('/')) {
                return errorResponse('Destination must be an absolute URL or a path beginning with /', 400);
            }
            const redirect = await db().redirect.create({
                data: {
                    source,
                    destination,
                    statusCode: [301, 302, 307, 308].includes(requestedStatus) ? requestedStatus : 301,
                    isRegex: body.isRegex === true,
                    notes: typeof body.notes === 'string' ? body.notes : null,
                },
            });
            return json({ data: normalizeRedirect(redirect) }, 201);
        }
        catch (err) {
            return internalError(err, 'create redirect');
        }
    });
    router.delete('/redirects/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (auth.session.role !== 'ADMIN')
                return errorResponse('Admin access required', 403);
            await db().redirect.delete({ where: { id: params.id } });
            return json({ data: { success: true } });
        }
        catch (err) {
            return internalError(err, 'delete redirect');
        }
    });
    // ---------------------------------------------------------------------------
    // Redirect engine — /seo/redirects/* (CRUD, chains/loops, 404 recovery, CSV)
    // ---------------------------------------------------------------------------
    const REDIRECT_STATUS_CODES = [301, 302, 307, 308, 410];
    function redirectToRule(r) {
        return {
            id: r.id,
            type: Number(r.statusCode ?? 301),
            fromPath: r.source ?? '',
            toPath: r.destination ?? '',
            enabled: r.enabled ?? true,
            priority: Number(r.priority ?? 0),
            preserveQuery: r.preserveQuery ?? false,
            hits: Number(r.hits ?? 0),
            lastHitAt: r.lastHitAt ?? null,
            stale: requireRedirectGraph().isRedirectStale(r.lastHitAt),
            source: r.source_kind ?? 'manual',
            notes: r.notes ?? null,
            createdAt: r.createdAt ?? null,
            updatedAt: r.updatedAt ?? null,
        };
    }
    // Lazily-loaded pure graph helpers (sync after first load is fine because
    // the module is tiny and the dynamic import resolves before any route runs
    // that needs it; we cache the namespace).
    let _graph = null;
    function requireRedirectGraph() {
        if (!_graph)
            throw new Error('redirect graph not loaded');
        return _graph;
    }
    async function ensureRedirectGraph() {
        if (!_graph)
            _graph = await import('../../redirects/graph.js');
        return _graph;
    }
    /** Validate a redirect destination (open-redirect defence). */
    function validateRedirectDestination(destination, statusCode) {
        if (statusCode === 410)
            return null; // Gone: destination is informational
        if (destination.startsWith('http://') || destination.startsWith('https://')) {
            let destUrl;
            try {
                destUrl = new URL(destination);
            }
            catch {
                return 'Invalid destination URL';
            }
            if (!['http:', 'https:'].includes(destUrl.protocol))
                return 'Invalid destination URL';
            const cmsConfig = getActuateConfig();
            const allowed = new Set(Array.isArray(cmsConfig?.redirects?.allowedExternalHosts)
                ? cmsConfig.redirects.allowedExternalHosts.map((h) => h.toLowerCase())
                : []);
            const siteUrl = process.env.NEXT_PUBLIC_SITE_URL;
            if (siteUrl) {
                try {
                    allowed.add(new URL(siteUrl).hostname.toLowerCase());
                }
                catch {
                    /* noop */
                }
            }
            if (!allowed.has(destUrl.hostname.toLowerCase())) {
                return 'External redirect destinations must be to an allowlisted host.';
            }
        }
        else if (!destination.startsWith('/')) {
            return 'Destination must be an absolute URL or a path beginning with /';
        }
        return null;
    }
    router.get('/seo/redirects', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            await ensureRedirectGraph();
            const rows = await db().redirect.findMany({
                orderBy: [{ priority: 'desc' }, { createdAt: 'desc' }],
            });
            return json({ data: { rules: rows.map(redirectToRule) } });
        }
        catch (err) {
            return internalError(err, 'seo/redirects');
        }
    });
    router.get('/seo/redirects/overview', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const graph = await ensureRedirectGraph();
            const rows = await db().redirect.findMany({});
            const active = rows.filter((r) => r.enabled !== false);
            const stale = active.filter((r) => graph.isRedirectStale(r.lastHitAt)).length;
            const chains = graph.detectChainsAndLoops(rows.map((r) => ({ id: r.id, from: r.source, to: r.destination, enabled: r.enabled })));
            let notFoundCount = 0;
            let notFoundHits = 0;
            if (hasModel(db(), 'redirect404Hit')) {
                const hits = await db().redirect404Hit.findMany({ where: { resolvedAt: null } });
                notFoundCount = hits.length;
                notFoundHits = hits.reduce((s, h) => s + Number(h.hits ?? 0), 0);
            }
            const trafficRecovered = active.reduce((s, r) => s + Number(r.hits ?? 0), 0);
            return json({
                data: {
                    activeRedirects: active.length,
                    staleRedirects: stale,
                    notFoundCount,
                    notFoundHits,
                    chainsAndLoops: chains.length,
                    trafficRecovered,
                },
            });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/overview');
        }
    });
    router.get('/seo/redirects/chains', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const graph = await ensureRedirectGraph();
            const rows = await db().redirect.findMany({});
            const chains = graph.detectChainsAndLoops(rows.map((r) => ({ id: r.id, from: r.source, to: r.destination, enabled: r.enabled })));
            return json({ data: { chains } });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/chains');
        }
    });
    router.post('/seo/redirects/flatten', 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 graph = await ensureRedirectGraph();
            const body = (await request.json().catch(() => ({})));
            const rows = await db().redirect.findMany({});
            const chains = graph.detectChainsAndLoops(rows.map((r) => ({ id: r.id, from: r.source, to: r.destination, enabled: r.enabled })));
            const chain = chains.find((c) => c.id === body.chainId);
            if (!chain)
                return errorResponse('Chain not found', 404);
            if (chain.kind === 'loop') {
                return errorResponse('Cannot auto-flatten a loop — break the cycle manually.', 400);
            }
            // Point the chain source directly at the terminal destination.
            const sourceRow = rows.find((r) => graph.normalizeRedirectPath(r.source) === chain.suggestedFrom);
            if (!sourceRow)
                return errorResponse('Chain source rule not found', 404);
            const updated = await db().redirect.update({
                where: { id: sourceRow.id },
                data: { destination: chain.suggestedTo, source_kind: 'chain-flatten' },
            });
            return json({ data: redirectToRule(updated) });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/flatten');
        }
    });
    router.get('/seo/redirects/suggestions', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            if (!hasModel(db(), 'redirectSuggestion'))
                return json({ data: { suggestions: [] } });
            const rows = await db().redirectSuggestion.findMany({
                where: { status: 'open' },
                orderBy: { confidence: 'desc' },
            });
            let hitsByPath = new Map();
            if (hasModel(db(), 'redirect404Hit')) {
                const hits = await db().redirect404Hit.findMany({});
                hitsByPath = new Map(hits.map((h) => [h.path, Number(h.hits ?? 0)]));
            }
            return json({
                data: {
                    suggestions: rows.map((s) => ({
                        id: s.id,
                        fromPath: s.fromPath,
                        toPath: s.toPath,
                        // Stored as a 0-100 Int; the UI contract is a 0-1 fraction.
                        confidence: Number(s.confidence ?? 0) / 100,
                        reason: s.reason ?? null,
                        status: s.status,
                        hits: hitsByPath.get(s.fromPath) ?? 0,
                        trend: null,
                    })),
                },
            });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/suggestions');
        }
    });
    router.post('/seo/redirects/suggest', 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(), 'redirectSuggestion') || !hasModel(db(), 'redirect404Hit')) {
                return modelNotAvailable('RedirectSuggestion');
            }
            const { suggestRedirectTarget } = await import('../../redirects/suggest.js');
            // Candidate set: published page/post URLs.
            const { gatherAuditEntities, frontEndContentCollectionTypes } = await import('../../seo/audit-runner.js');
            const entities = await gatherAuditEntities(db(), {
                collectionTypes: frontEndContentCollectionTypes(getActuateConfig()),
            });
            const candidates = entities.map((e) => ({ url: e.url, title: e.title }));
            const unresolved = await db().redirect404Hit.findMany({ where: { resolvedAt: null } });
            let created = 0;
            for (const hit of unresolved) {
                const existing = await db().redirectSuggestion.findFirst({
                    where: { fromPath: hit.path, status: 'open' },
                });
                if (existing)
                    continue;
                const suggestion = suggestRedirectTarget(hit.path, candidates);
                if (!suggestion)
                    continue;
                await db().redirectSuggestion.create({
                    data: {
                        fromPath: hit.path,
                        toPath: suggestion.toPath,
                        // DB column is an Int — store confidence as a 0-100 percentage.
                        // The GET endpoint converts back to the 0-1 fraction the UI expects.
                        confidence: Math.round(suggestion.confidence * 100),
                        reason: suggestion.reason,
                        status: 'open',
                        source: 'ai-404-recovery',
                    },
                });
                created++;
            }
            return json({ data: { created } });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/suggest');
        }
    });
    router.post('/seo/redirects/suggestions/:id/accept', 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;
            if (!hasModel(db(), 'redirectSuggestion'))
                return modelNotAvailable('RedirectSuggestion');
            const suggestion = await db().redirectSuggestion.findUnique({ where: { id: params.id } });
            if (!suggestion)
                return errorResponse('Suggestion not found', 404);
            const graph = await ensureRedirectGraph();
            const rows = await db().redirect.findMany({});
            if (graph.wouldCreateLoop(rows.map((r) => ({
                id: r.id,
                from: r.source,
                to: r.destination,
                enabled: r.enabled,
            })), suggestion.fromPath, suggestion.toPath)) {
                return errorResponse('Accepting this suggestion would create a redirect loop.', 400);
            }
            await db().redirect.create({
                data: {
                    source: suggestion.fromPath,
                    destination: suggestion.toPath,
                    statusCode: 301,
                    enabled: true,
                    source_kind: 'ai-404-recovery',
                    createdById: auth.session.userId,
                },
            });
            await db().redirectSuggestion.update({
                where: { id: suggestion.id },
                data: { status: 'accepted' },
            });
            // Mark the 404 resolved.
            if (hasModel(db(), 'redirect404Hit')) {
                await db()
                    .redirect404Hit.updateMany({
                    where: { path: suggestion.fromPath },
                    data: { resolvedAt: new Date() },
                })
                    .catch(() => { });
            }
            return json({ data: { success: true } });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/suggestions/accept');
        }
    });
    router.post('/seo/redirects/suggestions/:id/dismiss', 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;
            if (!hasModel(db(), 'redirectSuggestion'))
                return modelNotAvailable('RedirectSuggestion');
            await db().redirectSuggestion.update({
                where: { id: params.id },
                data: { status: 'dismissed' },
            });
            return json({ data: { success: true } });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/suggestions/dismiss');
        }
    });
    router.get('/seo/redirects/export', async (request) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            const rows = await db().redirect.findMany({ orderBy: { createdAt: 'desc' } });
            const header = 'from,to,type,enabled';
            const lines = rows.map((r) => {
                const esc = (v) => (/[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v);
                return [
                    esc(String(r.source ?? '')),
                    esc(String(r.destination ?? '')),
                    String(r.statusCode ?? 301),
                    r.enabled === false ? 'false' : 'true',
                ].join(',');
            });
            return json({ data: { csv: [header, ...lines].join('\n') } });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/export');
        }
    });
    router.post('/seo/redirects/import', 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 graph = await ensureRedirectGraph();
            const body = (await request.json().catch(() => ({})));
            const csv = String(body.csv ?? '').trim();
            if (!csv)
                return errorResponse('CSV body is required', 400);
            const lines = csv.split(/\r?\n/).filter((l) => l.trim().length > 0);
            // Skip an optional header row.
            if (lines[0] && /from\s*,\s*to/i.test(lines[0]))
                lines.shift();
            const errors = [];
            let imported = 0;
            const existing = await db().redirect.findMany({});
            const working = existing.map((r) => ({
                id: r.id,
                from: r.source,
                to: r.destination,
                enabled: r.enabled,
            }));
            for (let idx = 0; idx < lines.length; idx++) {
                const cols = parseCsvLine(lines[idx]);
                const from = (cols[0] ?? '').trim();
                const to = (cols[1] ?? '').trim();
                const type = Number(cols[2] ?? 301);
                if (!from || !to) {
                    errors.push(`Row ${idx + 1}: missing from/to`);
                    continue;
                }
                const destErr = validateRedirectDestination(to, type);
                if (destErr) {
                    errors.push(`Row ${idx + 1}: ${destErr}`);
                    continue;
                }
                if (graph.wouldCreateLoop(working, from, to)) {
                    errors.push(`Row ${idx + 1}: would create a redirect loop`);
                    continue;
                }
                try {
                    await db().redirect.upsert({
                        where: { source: from },
                        update: {
                            destination: to,
                            statusCode: REDIRECT_STATUS_CODES.includes(type) ? type : 301,
                        },
                        create: {
                            source: from,
                            destination: to,
                            statusCode: REDIRECT_STATUS_CODES.includes(type) ? type : 301,
                            source_kind: 'import',
                            createdById: auth.session.userId,
                        },
                    });
                    working.push({ id: `imp-${idx}`, from, to, enabled: true });
                    imported++;
                }
                catch (e) {
                    errors.push(`Row ${idx + 1}: ${e instanceof Error ? e.message : 'insert failed'}`);
                }
            }
            return json({ data: { imported, errors } });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/import');
        }
    });
    router.post('/seo/redirects', 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 graph = await ensureRedirectGraph();
            const body = (await request.json().catch(() => ({})));
            const source = String(body.fromPath ?? body.source ?? '').trim();
            const destination = String(body.toPath ?? body.destination ?? '').trim();
            const statusCode = Number(body.type ?? body.statusCode ?? 301);
            if (!source)
                return errorResponse('fromPath is required', 400);
            if (!destination && statusCode !== 410)
                return errorResponse('toPath is required', 400);
            if (!REDIRECT_STATUS_CODES.includes(statusCode)) {
                return errorResponse('type must be one of 301, 302, 307, 308, 410', 400);
            }
            const destErr = validateRedirectDestination(destination, statusCode);
            if (destErr)
                return errorResponse(destErr, 400);
            const rows = await db().redirect.findMany({});
            if (rows.some((r) => graph.normalizeRedirectPath(r.source) === graph.normalizeRedirectPath(source))) {
                return errorResponse('A redirect with this source already exists.', 409);
            }
            if (graph.wouldCreateLoop(rows.map((r) => ({
                id: r.id,
                from: r.source,
                to: r.destination,
                enabled: r.enabled,
            })), source, destination || source)) {
                return errorResponse('This redirect would create a loop.', 400);
            }
            const created = await db().redirect.create({
                data: {
                    source,
                    destination: destination || source,
                    statusCode,
                    enabled: body.enabled !== false,
                    preserveQuery: body.preserveQuery === true,
                    notes: typeof body.notes === 'string' ? body.notes : null,
                    source_kind: 'manual',
                    createdById: auth.session.userId,
                },
            });
            return json({ data: redirectToRule(created) }, 201);
        }
        catch (err) {
            return internalError(err, 'seo/redirects POST');
        }
    });
    router.get('/seo/redirects/:id', async (request, params) => {
        try {
            const auth = await requireAuth(request);
            if (auth.error)
                return auth.error;
            await ensureRedirectGraph();
            const row = await db().redirect.findUnique({ where: { id: params.id } });
            if (!row)
                return errorResponse('Redirect not found', 404);
            return json({ data: redirectToRule(row) });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/:id GET');
        }
    });
    router.put('/seo/redirects/: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 graph = await ensureRedirectGraph();
            const existing = await db().redirect.findUnique({ where: { id: params.id } });
            if (!existing)
                return errorResponse('Redirect not found', 404);
            const body = (await request.json().catch(() => ({})));
            const source = body.fromPath !== undefined || body.source !== undefined
                ? String(body.fromPath ?? body.source ?? '').trim()
                : existing.source;
            const destination = body.toPath !== undefined || body.destination !== undefined
                ? String(body.toPath ?? body.destination ?? '').trim()
                : existing.destination;
            const statusCode = body.type !== undefined || body.statusCode !== undefined
                ? Number(body.type ?? body.statusCode)
                : existing.statusCode;
            if (!source)
                return errorResponse('fromPath is required', 400);
            if (!REDIRECT_STATUS_CODES.includes(statusCode)) {
                return errorResponse('type must be one of 301, 302, 307, 308, 410', 400);
            }
            const destErr = validateRedirectDestination(destination, statusCode);
            if (destErr)
                return errorResponse(destErr, 400);
            const rows = await db().redirect.findMany({});
            if (graph.wouldCreateLoop(rows.map((r) => ({
                id: r.id,
                from: r.source,
                to: r.destination,
                enabled: r.enabled,
            })), source, destination || source, existing.id)) {
                return errorResponse('This change would create a redirect loop.', 400);
            }
            const updated = await db().redirect.update({
                where: { id: existing.id },
                data: {
                    source,
                    destination: destination || source,
                    statusCode,
                    ...(body.enabled !== undefined ? { enabled: body.enabled === true } : {}),
                    ...(body.preserveQuery !== undefined
                        ? { preserveQuery: body.preserveQuery === true }
                        : {}),
                    ...(body.notes !== undefined
                        ? { notes: typeof body.notes === 'string' ? body.notes : null }
                        : {}),
                },
            });
            return json({ data: redirectToRule(updated) });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/:id PUT');
        }
    });
    router.delete('/seo/redirects/: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;
            await db().redirect.delete({ where: { id: params.id } });
            return json({ data: { success: true } });
        }
        catch (err) {
            return internalError(err, 'seo/redirects/:id DELETE');
        }
    });
    // Public: resolve a path against enabled redirects. Used by the redirect
    // middleware. Increments hit counters on a match; logs a 404 otherwise.
    router.get('/redirects/resolve', async (request) => {
        try {
            const graph = await ensureRedirectGraph();
            const url = new URL(request.url);
            const path = url.searchParams.get('path') ?? '';
            if (!path)
                return json({ data: { match: null } });
            const norm = graph.normalizeRedirectPath(path);
            const rows = await db().redirect.findMany({
                where: { enabled: true },
                orderBy: [{ priority: 'desc' }, { createdAt: 'desc' }],
            });
            const match = rows.find((r) => graph.normalizeRedirectPath(r.source) === norm);
            if (!match) {
                return json({ data: { match: null } });
            }
            db()
                .redirect.update({
                where: { id: match.id },
                data: { hits: { increment: 1 }, lastHitAt: new Date() },
            })
                .catch(() => { });
            return json({
                data: {
                    match: {
                        to: match.destination,
                        statusCode: Number(match.statusCode ?? 301),
                        preserveQuery: match.preserveQuery ?? false,
                    },
                },
            });
        }
        catch (err) {
            return internalError(err, 'redirects/resolve');
        }
    });
    // Public: enabled redirects in the plugin-redirects middleware shape. Backs
    // `createRedirectMiddleware`'s `getRedirects` (cached by the factory).
    router.get('/redirects/public', async () => {
        try {
            const rows = await db().redirect.findMany({
                where: { enabled: true },
                orderBy: [{ priority: 'desc' }, { createdAt: 'desc' }],
                select: {
                    id: true,
                    source: true,
                    destination: true,
                    statusCode: true,
                    preserveQuery: true,
                },
            });
            return json({
                data: {
                    redirects: rows.map((r) => ({
                        id: r.id,
                        fromUrl: r.source,
                        toUrl: r.destination,
                        type: String([301, 302].includes(Number(r.statusCode)) ? r.statusCode : 301),
                        statusCode: Number(r.statusCode ?? 301),
                        preserveQuery: r.preserveQuery ?? false,
                        isActive: true,
                    })),
                },
            });
        }
        catch (err) {
            return internalError(err, 'redirects/public');
        }
    });
    // Public: increment hit counters for a matched source path. Fire-and-forget
    // from the middleware so request latency is unaffected.
    router.post('/redirects/record-hit', async (request) => {
        try {
            const graph = await ensureRedirectGraph();
            const body = (await request.json().catch(() => ({})));
            const norm = graph.normalizeRedirectPath(String(body.path ?? ''));
            if (!norm)
                return json({ data: { ok: false } });
            const rows = await db().redirect.findMany({
                where: { enabled: true },
                select: { id: true, source: true },
            });
            const match = rows.find((r) => graph.normalizeRedirectPath(r.source) === norm);
            if (!match)
                return json({ data: { ok: false } });
            await db()
                .redirect.update({
                where: { id: match.id },
                data: { hits: { increment: 1 }, lastHitAt: new Date() },
            })
                .catch(() => { });
            return json({ data: { ok: true } });
        }
        catch (err) {
            return internalError(err, 'redirects/record-hit');
        }
    });
    // Public: record a 404 for a path (called by the site when it renders
    // notFound). Aggregates per-path so the Redirects tab can surface dead URLs.
    router.post('/redirects/record-404', async (request) => {
        try {
            if (!hasModel(db(), 'redirect404Hit'))
                return json({ data: { ok: false } });
            const graph = await ensureRedirectGraph();
            const body = (await request.json().catch(() => ({})));
            const path = graph.normalizeRedirectPath(String(body.path ?? ''));
            // Bound the inputs: this endpoint is unauthenticated, so cap the stored
            // strings to keep a flood of distinct paths from bloating the table.
            if (!path || path === '/' || path.length > 2048)
                return json({ data: { ok: false } });
            const referrer = typeof body.referrer === 'string' && body.referrer.length > 0
                ? body.referrer.slice(0, 2048)
                : null;
            await db().redirect404Hit.upsert({
                where: { path },
                update: { hits: { increment: 1 }, lastSeenAt: new Date() },
                create: { path, hits: 1, referrer },
            });
            return json({ data: { ok: true } });
        }
        catch (err) {
            return internalError(err, 'redirects/record-404');
        }
    });
}
//# sourceMappingURL=redirects.js.map