"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRagAdminRoutes = createRagAdminRoutes;
/**
 * RAG admin routes — operator-side surface for the Plan 55 RAG
 * store. Lets a curator inspect index population per source kind,
 * delete blocked / stale chunks, and search the index directly
 * (without going through the agent).
 *
 * Endpoints (all under `/api/rag`):
 *   GET    /stats              per-kind chunk counts + last indexed
 *   GET    /chunks/:chunkId    fetch one chunk
 *   DELETE /chunks/:chunkId    remove a chunk (license-blocked
 *                              entries can be evicted permanently
 *                              once the curator decides to)
 *   POST   /search             body `{query, kinds?, topK?}` —
 *                              run a search like the agent would
 *
 * Ingestion endpoints (POST /ingest/blog, /ingest/aosp,
 * /ingest/oem) are intentionally NOT exposed in M2; ingesters are
 * called from operator scripts that supply the fetcher (with
 * authenticated source credentials) directly. M3 may add upload
 * endpoints once an authentication story exists.
 *
 * @module ragAdminRoutes
 */
const crypto_1 = require("crypto");
const express_1 = require("express");
const auth_1 = require("../middleware/auth");
const ragStore_1 = require("../services/ragStore");
const scopedKnowledgeStore_1 = require("../services/scopedKnowledgeStore");
const runtimePaths_1 = require("../runtimePaths");
const codebaseScopes_1 = require("../services/auth/codebaseScopes");
const defaultCodebaseServices_1 = require("../services/codebase/defaultCodebaseServices");
const pathSecurityGate_1 = require("../services/codebase/pathSecurityGate");
const appSourceIngester_1 = require("../services/rag/appSourceIngester");
const aospSourceIngester_1 = require("../services/rag/aospSourceIngester");
const kernelSourceIngester_1 = require("../services/rag/kernelSourceIngester");
const symbolResolver_1 = require("../services/symbol/symbolResolver");
const codeAwareFeature_1 = require("../services/codebase/codeAwareFeature");
const DEFAULT_STORAGE_PATH = (0, runtimePaths_1.backendLogPath)('rag_store.json');
let cachedStore = null;
function getDefaultStore() {
    if (!cachedStore)
        cachedStore = new ragStore_1.RagStore(DEFAULT_STORAGE_PATH);
    return cachedStore;
}
function snippetHash(snippet) {
    return (0, crypto_1.createHash)('sha256').update(snippet).digest('hex').slice(0, 12);
}
function isCodeAwareChunk(chunk) {
    return chunk.kind === 'app_source' ||
        chunk.kind === 'kernel_source' ||
        chunk.registryOrigin === 'codebase_registry';
}
function sanitizeChunk(chunk) {
    if (!isCodeAwareChunk(chunk))
        return chunk;
    const { snippet, ...rest } = chunk;
    return {
        ...rest,
        snippet: undefined,
        snippetHash: snippetHash(snippet),
        snippetLength: snippet.length,
    };
}
function sanitizeRetrieval(result) {
    return {
        ...result,
        results: result.results.map(hit => ({
            ...hit,
            ...(hit.chunk ? { chunk: sanitizeChunk(hit.chunk) } : {}),
        })),
    };
}
function sanitizeCodebase(ref) {
    const { rootPath: _rootPath, rootRealpath: _rootRealpath, consent, ...rest } = ref;
    return {
        ...rest,
        eligibleForSendToProvider: consent.sendToProvider,
        consent: {
            sendToProvider: consent.sendToProvider,
            consentedAt: consent.consentedAt,
            consentedBy: consent.consentedBy,
            consentHash: consent.consentHash,
        },
    };
}
function sanitizePreview(preview) {
    return {
        blocked: preview.blocked,
        ...(preview.blockedReason ? { blockedReason: preview.blockedReason } : {}),
        acceptedFileCount: preview.acceptedFiles.length,
        skippedFileCount: preview.skippedFiles.length,
        acceptedFiles: preview.acceptedFiles.slice(0, 200),
        skippedFiles: preview.skippedFiles.slice(0, 200),
    };
}
function routeParam(value) {
    return Array.isArray(value) ? value[0] ?? '' : value ?? '';
}
/** Test/factory hook. */
function createRagAdminRoutes(store, services = {}) {
    const s = store ?? getDefaultStore();
    const registry = services.registry ?? (0, defaultCodebaseServices_1.getDefaultCodebaseRegistry)();
    const gate = services.gate ?? new pathSecurityGate_1.PathSecurityGate();
    const appSourceIngester = services.appSourceIngester ?? new appSourceIngester_1.AppSourceIngester(s, registry, gate);
    const aospSourceIngester = services.aospSourceIngester ?? new aospSourceIngester_1.AospSourceIngester(s, registry, gate);
    const kernelSourceIngester = services.kernelSourceIngester ?? new kernelSourceIngester_1.KernelSourceIngester(s, registry, gate);
    const symbolResolver = new symbolResolver_1.SymbolResolver(s);
    const router = (0, express_1.Router)();
    router.use(auth_1.authenticate);
    router.get('/stats', (0, codebaseScopes_1.requireCodebaseScope)('codebase:read'), (req, res) => {
        const scope = (0, scopedKnowledgeStore_1.knowledgeScopeFromRequestContext)((0, auth_1.requireRequestContext)(req));
        res.json({ success: true, stats: s.getStats(scope) });
    });
    router.get('/chunks/:chunkId', (0, codebaseScopes_1.requireCodebaseScope)('codebase:read'), (req, res) => {
        const scope = (0, scopedKnowledgeStore_1.knowledgeScopeFromRequestContext)((0, auth_1.requireRequestContext)(req));
        const chunkId = routeParam(req.params.chunkId);
        const chunk = s.getChunk(chunkId, scope);
        if (!chunk) {
            return res.status(404).json({
                success: false,
                error: `Chunk '${chunkId}' not found`,
            });
        }
        res.json({ success: true, chunk: sanitizeChunk(chunk) });
    });
    router.delete('/chunks/:chunkId', (0, codebaseScopes_1.requireCodebaseScope)('codebase:admin'), (req, res) => {
        const scope = (0, scopedKnowledgeStore_1.knowledgeScopeFromRequestContext)((0, auth_1.requireRequestContext)(req));
        const chunkId = routeParam(req.params.chunkId);
        const removed = s.removeChunk(chunkId, scope);
        if (!removed) {
            return res.status(404).json({
                success: false,
                error: `Chunk '${chunkId}' not found`,
            });
        }
        res.json({ success: true });
    });
    router.post('/search', (0, codebaseScopes_1.requireCodebaseScope)('codebase:read'), (req, res) => {
        const scope = (0, scopedKnowledgeStore_1.knowledgeScopeFromRequestContext)((0, auth_1.requireRequestContext)(req));
        const { query, kinds, topK, codebaseIds, vendor, buildId, pathPrefix, symbolExact, filePathExact, languages } = (req.body ?? {});
        if (!query || typeof query !== 'string') {
            return res.status(400).json({
                success: false,
                error: '`query` (string) is required',
            });
        }
        const result = s.search(query, {
            ...(kinds ? { kinds } : {}),
            ...(topK ? { topK } : {}),
            ...(codebaseIds ? { codebaseIds } : {}),
            ...(vendor ? { vendor } : {}),
            ...(buildId ? { buildId } : {}),
            ...(pathPrefix ? { pathPrefix } : {}),
            ...(symbolExact ? { symbolExact } : {}),
            ...(filePathExact ? { filePathExact } : {}),
            ...(languages ? { languages } : {}),
            scope,
        });
        res.json({ success: true, result: sanitizeRetrieval(result) });
    });
    router.get('/codebases', (0, codebaseScopes_1.requireCodebaseScope)('codebase:read'), (_req, res) => {
        res.json({
            success: true,
            featureEnabled: (0, codeAwareFeature_1.codeAwareFeatureEnabled)(),
            codebases: registry.list(),
        });
    });
    router.post('/codebases/preview', (0, codebaseScopes_1.requireCodebaseScope)('codebase:manage'), async (req, res) => {
        const { rootPath } = (req.body ?? {});
        if (!rootPath || typeof rootPath !== 'string') {
            return res.status(400).json({ success: false, error: '`rootPath` is required' });
        }
        res.json({ success: true, preview: sanitizePreview(await gate.preview(rootPath)) });
    });
    router.post('/codebases/register', (0, codebaseScopes_1.requireCodebaseScope)('codebase:manage'), async (req, res) => {
        const { kind = 'app_source', displayName, rootPath, commitHash, vendor, buildId, pathFilters, excludeGlobs, symbolMapPaths, licenseTag, sendToProvider, } = (req.body ?? {});
        if (!displayName || typeof displayName !== 'string') {
            return res.status(400).json({ success: false, error: '`displayName` is required' });
        }
        if (!rootPath || typeof rootPath !== 'string') {
            return res.status(400).json({ success: false, error: '`rootPath` is required' });
        }
        const preview = await gate.preview(rootPath);
        if (preview.blocked) {
            return res.status(400).json({
                success: false,
                error: preview.blockedReason ?? 'root blocked by path security gate',
                preview: sanitizePreview(preview),
            });
        }
        const context = (0, auth_1.requireRequestContext)(req);
        try {
            const ref = registry.register({
                kind,
                displayName,
                rootPath,
                rootRealpath: preview.rootRealpath,
                ...(commitHash ? { commitHash } : {}),
                ...(vendor ? { vendor } : {}),
                ...(buildId ? { buildId } : {}),
                ...(Array.isArray(pathFilters) ? { pathFilters } : {}),
                ...(Array.isArray(excludeGlobs) ? { excludeGlobs } : {}),
                ...(Array.isArray(symbolMapPaths) ? { symbolMapPaths } : {}),
                ...(licenseTag ? { licenseTag } : {}),
                sendToProvider: Boolean(sendToProvider),
                consentedBy: context.userId,
                tenantId: context.tenantId,
                workspaceId: context.workspaceId,
                userId: context.userId,
            });
            res.json({ success: true, codebase: sanitizeCodebase(ref), preview: sanitizePreview(preview) });
        }
        catch (error) {
            res.status(400).json({ success: false, error: error instanceof Error ? error.message : String(error) });
        }
    });
    router.get('/codebases/:id', (0, codebaseScopes_1.requireCodebaseScope)('codebase:read'), (req, res) => {
        const codebaseId = routeParam(req.params.id);
        const ref = registry.get(codebaseId);
        if (!ref) {
            return res.status(404).json({ success: false, error: `Codebase '${codebaseId}' not found` });
        }
        res.json({ success: true, codebase: sanitizeCodebase(ref) });
    });
    router.get('/codebases/:id/symbols', (0, codebaseScopes_1.requireCodebaseScope)('codebase:read'), (req, res) => {
        const codebaseId = routeParam(req.params.id);
        const ref = registry.get(codebaseId);
        if (!ref) {
            return res.status(404).json({ success: false, error: `Codebase '${codebaseId}' not found` });
        }
        const symbol = typeof req.query.symbol === 'string'
            ? req.query.symbol
            : typeof req.query.query === 'string'
                ? req.query.query
                : '';
        if (!symbol) {
            return res.status(400).json({ success: false, error: '`symbol` or `query` is required' });
        }
        const common = {
            codebaseId,
            buildId: typeof req.query.buildId === 'string' ? req.query.buildId : undefined,
            topK: typeof req.query.topK === 'string' ? Number(req.query.topK) : undefined,
        };
        const result = ref.kind === 'kernel_source'
            ? symbolResolver.resolveKernel({
                symbol,
                vendor: ref.vendor,
                ...common,
            })
            : ref.kind === 'aosp'
                ? symbolResolver.resolveNative({
                    symbol,
                    ...common,
                })
                : symbolResolver.resolveApp({
                    symbol,
                    codebaseId,
                    buildId: common.buildId,
                    filePath: typeof req.query.filePath === 'string' ? req.query.filePath : undefined,
                });
        res.json({ success: true, result });
    });
    router.get('/codebases/:id/excerpt', (0, codebaseScopes_1.requireCodebaseScope)('codebase:read'), (req, res) => {
        const codebaseId = routeParam(req.params.id);
        const ref = registry.get(codebaseId);
        if (!ref) {
            return res.status(404).json({ success: false, error: `Codebase '${codebaseId}' not found` });
        }
        const chunkId = typeof req.query.chunkId === 'string' ? req.query.chunkId : '';
        if (!chunkId) {
            return res.status(400).json({ success: false, error: '`chunkId` is required' });
        }
        const scope = (0, scopedKnowledgeStore_1.knowledgeScopeFromRequestContext)((0, auth_1.requireRequestContext)(req));
        const chunk = s.getChunk(chunkId, scope);
        if (!chunk || chunk.codebaseId !== codebaseId || !isCodeAwareChunk(chunk)) {
            return res.status(404).json({ success: false, error: `Code excerpt '${chunkId}' not found` });
        }
        const maxLines = typeof req.query.maxLines === 'string'
            ? Math.max(1, Math.min(80, Number(req.query.maxLines) || 20))
            : 20;
        const lines = chunk.snippet.split(/\r?\n/).slice(0, maxLines);
        res.json({
            success: true,
            excerpt: {
                chunkId,
                codebaseId,
                filePath: chunk.filePath,
                lineRange: chunk.lineRange,
                symbol: chunk.symbol,
                language: chunk.language,
                text: lines.join('\n'),
                truncated: lines.length < chunk.snippet.split(/\r?\n/).length,
            },
        });
    });
    router.post('/codebases/:id/reindex', (0, codebaseScopes_1.requireCodebaseScope)('codebase:manage'), async (req, res) => {
        const codebaseId = routeParam(req.params.id);
        const ref = registry.get(codebaseId);
        if (!ref) {
            return res.status(404).json({ success: false, error: `Codebase '${codebaseId}' not found` });
        }
        try {
            const result = await (ref.kind === 'kernel_source'
                ? kernelSourceIngester.ingest(codebaseId, req.body ?? {})
                : ref.kind === 'aosp'
                    ? aospSourceIngester.ingest(codebaseId, req.body ?? {})
                    : appSourceIngester.ingest(codebaseId, req.body ?? {}));
            res.json({ success: true, result });
        }
        catch (error) {
            res.status(400).json({
                success: false,
                error: error instanceof Error ? error.message : String(error),
            });
        }
    });
    router.get('/codebases/:id/audit', (0, codebaseScopes_1.requireCodebaseScope)('codebase:read'), (req, res) => {
        const codebaseId = routeParam(req.params.id);
        const ref = registry.get(codebaseId);
        if (!ref) {
            return res.status(404).json({ success: false, error: `Codebase '${codebaseId}' not found` });
        }
        res.json({
            success: true,
            audit: {
                codebaseId: ref.codebaseId,
                kind: ref.kind,
                indexGeneration: ref.indexGeneration,
                lastIngestAt: ref.lastIngestAt,
                lastIngestStatus: ref.lastIngestStatus,
                lastIngestError: ref.lastIngestError,
                chunkCount: ref.chunkCount ?? 0,
                blockedFileCount: ref.blockedFileCount ?? 0,
                redactionHitCount: ref.redactionHitCount ?? 0,
            },
        });
    });
    return router;
}
const ragAdminRoutes = createRagAdminRoutes();
exports.default = ragAdminRoutes;
//# sourceMappingURL=ragAdminRoutes.js.map