"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.docsRouter = void 0;
const express_1 = require("express");
const multer_1 = __importDefault(require("multer"));
const promises_1 = require("fs/promises");
const path_1 = require("path");
const auth_middleware_1 = require("../../middleware/auth.middleware");
const rbac_middleware_1 = require("../../middleware/rbac.middleware");
const env_1 = require("../../config/env");
const roles_1 = require("../../utils/roles");
const logger_1 = require("../../utils/logger");
const health_check_1 = require("../../utils/health-check");
const metrics_1 = require("../../utils/metrics");
const docs_files_service_1 = require("./docs-files.service");
const docs_collab_service_1 = require("./docs-collab.service");
const mkdocs_config_service_1 = require("./mkdocs-config.service");
const header_builder_service_1 = require("./header-builder.service");
const header_builder_schemas_1 = require("./header-builder.schemas");
const router = (0, express_1.Router)();
router.use(auth_middleware_1.authenticate);
router.use(rbac_middleware_1.requireNonTemp);
// Removed duplicated isServiceOnline - now using shared utility from utils/health-check.ts
// GET /api/docs/status — check MkDocs and Code Server availability (content editors only)
router.get('/status', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
    try {
        const [mkdocsOnline, codeServerOnline, siteServerOnline] = await Promise.all([
            (0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_PREVIEW_URL),
            (0, health_check_1.isServiceOnline)(env_1.env.CODE_SERVER_URL),
            (0, health_check_1.isServiceOnline)(env_1.env.MKDOCS_SITE_SERVER_URL),
        ]);
        res.json({
            mkdocs: { online: mkdocsOnline, url: env_1.env.MKDOCS_PREVIEW_URL },
            codeServer: { online: codeServerOnline, url: env_1.env.CODE_SERVER_URL },
            siteServer: { online: siteServerOnline, url: env_1.env.MKDOCS_SITE_SERVER_URL },
        });
    }
    catch (err) {
        logger_1.logger.error('Failed to check docs status', err);
        next(err);
    }
});
// GET /api/docs/config — return public-facing port numbers for iframe URLs (content editors only)
router.get('/config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, _next) => {
    res.json({
        codeServerPort: env_1.env.CODE_SERVER_PORT,
        mkdocsPort: env_1.env.MKDOCS_PORT,
        mkdocsSitePort: env_1.env.MKDOCS_SITE_SERVER_PORT,
    });
});
// --- MkDocs Config Endpoints ---
// GET /api/docs/mkdocs-config — read raw mkdocs.yml content (content editors only)
router.get('/mkdocs-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
    try {
        const content = await mkdocs_config_service_1.mkdocsConfigService.readConfig();
        res.json({ content });
    }
    catch (err) {
        logger_1.logger.error('Failed to read mkdocs config', err);
        next(err);
    }
});
// PUT /api/docs/mkdocs-config — validate + write mkdocs.yml (SUPER_ADMIN only)
router.put('/mkdocs-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        const { content } = req.body;
        if (typeof content !== 'string') {
            res.status(400).json({ error: { message: 'Content string required', code: 'VALIDATION_ERROR' } });
            return;
        }
        await mkdocs_config_service_1.mkdocsConfigService.writeConfig(content);
        res.json({ success: true });
    }
    catch (err) {
        if (err.message?.startsWith('Invalid YAML')) {
            res.status(400).json({ error: { message: err.message, code: 'VALIDATION_ERROR' } });
            return;
        }
        logger_1.logger.error('Failed to write mkdocs config', err);
        next(err);
    }
});
// POST /api/docs/build — trigger mkdocs build in container
router.post('/build', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
    try {
        const result = await mkdocs_config_service_1.mkdocsConfigService.triggerBuild();
        res.json(result);
    }
    catch (err) {
        logger_1.logger.error('MkDocs build failed', err);
        next(err);
    }
});
// --- Header Builder ---
// GET /api/docs/header-config — read header nav bar config (content editors only)
router.get('/header-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (_req, res, next) => {
    try {
        const config = await header_builder_service_1.headerBuilderService.readConfig();
        res.json(config);
    }
    catch (err) {
        logger_1.logger.error('Failed to read header config', err);
        next(err);
    }
});
// PUT /api/docs/header-config — save header nav bar config + regenerate template
router.put('/header-config', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        const parsed = header_builder_schemas_1.headerConfigSchema.safeParse(req.body);
        if (!parsed.success) {
            res.status(400).json({
                error: { message: 'Invalid header config', code: 'VALIDATION_ERROR', details: parsed.error.flatten().fieldErrors },
            });
            return;
        }
        await header_builder_service_1.headerBuilderService.writeConfig(parsed.data);
        // Invalidate docs file tree cache so the new main.html shows up
        await docs_files_service_1.docsFilesService.invalidateTreeCache();
        res.json({ success: true });
    }
    catch (err) {
        logger_1.logger.error('Failed to save header config', err);
        next(err);
    }
});
// --- File Upload ---
const ALLOWED_UPLOAD_EXTENSIONS = new Set([
    '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico',
    '.pdf', '.zip',
]);
const upload = (0, multer_1.default)({
    storage: multer_1.default.diskStorage({}), // temp dir
    limits: { fileSize: 20 * 1024 * 1024 }, // 20MB
    fileFilter: (_req, file, cb) => {
        const ext = (0, path_1.extname)(file.originalname).toLowerCase();
        if (ALLOWED_UPLOAD_EXTENSIONS.has(ext)) {
            cb(null, true);
        }
        else {
            cb(new Error(`File type not allowed: ${ext}`));
        }
    },
});
// POST /api/docs/upload — upload binary file (image, pdf, etc.)
router.post('/upload', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), upload.single('file'), async (req, res, next) => {
    const tempPath = req.file?.path;
    try {
        metrics_1.cm_docs_operations.inc({ operation: 'upload' });
        if (!req.file) {
            res.status(400).json({ error: { message: 'No file provided', code: 'VALIDATION_ERROR' } });
            return;
        }
        const targetDir = req.body.path || '';
        const fileName = (0, path_1.basename)(req.file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_');
        const relativePath = targetDir ? `${targetDir}/${fileName}` : fileName;
        await docs_files_service_1.docsFilesService.uploadFile(relativePath, req.file.path);
        // Clean up temp file
        try {
            await (0, promises_1.rm)(req.file.path);
        }
        catch { /* ignore */ }
        res.json({ success: true, path: relativePath });
    }
    catch (err) {
        // Clean up temp file on error
        if (tempPath) {
            try {
                await (0, promises_1.rm)(tempPath);
            }
            catch { /* ignore */ }
        }
        handleFileError(err, res, next);
    }
});
// --- File Management Endpoints ---
// GET /api/docs/files — list file tree (content editors only)
router.get('/files', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        metrics_1.cm_docs_operations.inc({ operation: 'list' });
        if (req.query['force'] === 'true') {
            await docs_files_service_1.docsFilesService.invalidateTreeCache();
        }
        const tree = await docs_files_service_1.docsFilesService.listTree();
        res.json(tree);
    }
    catch (err) {
        logger_1.logger.error('Failed to list docs files', err);
        next(err);
    }
});
// GET /api/docs/files/search — search files by name/path (content editors only)
router.get('/files/search', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        const search = String(req.query['search'] ?? req.query['q'] ?? '').trim();
        if (!search) {
            res.json({ files: [] });
            return;
        }
        const limit = Math.min(Math.max(Number(req.query['limit']) || 5, 1), 20);
        const files = await docs_files_service_1.docsFilesService.searchFiles(search, limit);
        res.json({ files });
    }
    catch (err) {
        logger_1.logger.error('Failed to search docs files', err);
        next(err);
    }
});
// POST /api/docs/files/rename — rename/move file
router.post('/files/rename', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        metrics_1.cm_docs_operations.inc({ operation: 'rename' });
        const { from, to } = req.body;
        if (!from || !to) {
            res.status(400).json({ error: { message: 'Both "from" and "to" paths are required', code: 'VALIDATION_ERROR' } });
            return;
        }
        await docs_files_service_1.docsFilesService.renameFile(from, to);
        // Invalidate old path's collaboration state
        docs_collab_service_1.docsCollabService.invalidateDocument(from).catch(() => { });
        res.json({ success: true });
    }
    catch (err) {
        handleFileError(err, res, next);
    }
});
// GET /api/docs/files/* — read file content (content editors only)
router.get('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        metrics_1.cm_docs_operations.inc({ operation: 'read' });
        const filePath = extractWildcardPath(req);
        if (!filePath) {
            res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } });
            return;
        }
        const content = await docs_files_service_1.docsFilesService.readFileContent(filePath);
        res.json({ path: filePath, content });
    }
    catch (err) {
        handleFileError(err, res, next);
    }
});
// PUT /api/docs/files/* — write/update file content
router.put('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        metrics_1.cm_docs_operations.inc({ operation: 'write' });
        const filePath = extractWildcardPath(req);
        if (!filePath) {
            res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } });
            return;
        }
        const { content } = req.body;
        if (typeof content !== 'string') {
            res.status(400).json({ error: { message: 'Content string required', code: 'VALIDATION_ERROR' } });
            return;
        }
        await docs_files_service_1.docsFilesService.writeFileContent(filePath, content);
        // Invalidate collaboration state so next session starts fresh from disk
        docs_collab_service_1.docsCollabService.invalidateDocument(filePath).catch(() => { });
        res.json({ success: true, path: filePath });
    }
    catch (err) {
        handleFileError(err, res, next);
    }
});
// POST /api/docs/files/* — create new file or folder
router.post('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        metrics_1.cm_docs_operations.inc({ operation: 'create' });
        const filePath = extractWildcardPath(req);
        if (!filePath) {
            res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } });
            return;
        }
        const { content, isDirectory } = req.body;
        await docs_files_service_1.docsFilesService.createFile(filePath, content, isDirectory);
        res.status(201).json({ success: true, path: filePath });
    }
    catch (err) {
        handleFileError(err, res, next);
    }
});
// DELETE /api/docs/files/* — delete file or empty folder
router.delete('/files/*', (0, rbac_middleware_1.requireRole)(...roles_1.CONTENT_ROLES), async (req, res, next) => {
    try {
        metrics_1.cm_docs_operations.inc({ operation: 'delete' });
        const filePath = extractWildcardPath(req);
        if (!filePath) {
            res.status(400).json({ error: { message: 'File path required', code: 'VALIDATION_ERROR' } });
            return;
        }
        await docs_files_service_1.docsFilesService.deleteFile(filePath);
        // Invalidate collaboration state for deleted file
        docs_collab_service_1.docsCollabService.invalidateDocument(filePath).catch(() => { });
        res.json({ success: true });
    }
    catch (err) {
        handleFileError(err, res, next);
    }
});
/**
 * Extract the wildcard path from Express 5 req.params.
 * Express 5 uses params[0] for * routes.
 */
function extractWildcardPath(req) {
    // Express 5: req.params is array-like for * routes
    const params = req.params;
    const wildcardParam = params[0] || params['0'];
    if (Array.isArray(wildcardParam))
        return wildcardParam.join('/');
    return wildcardParam || '';
}
function handleFileError(err, res, next) {
    if (err instanceof docs_files_service_1.PathTraversalError) {
        res.status(403).json({ error: { message: 'Path traversal not allowed', code: 'FORBIDDEN' } });
        return;
    }
    if (err instanceof docs_files_service_1.FileNotFoundError) {
        res.status(404).json({ error: { message: err.message, code: 'NOT_FOUND' } });
        return;
    }
    if (err.message?.startsWith('Already exists')) {
        res.status(409).json({ error: { message: err.message, code: 'CONFLICT' } });
        return;
    }
    if (err.message === 'Directory is not empty') {
        res.status(400).json({ error: { message: 'Directory is not empty', code: 'VALIDATION_ERROR' } });
        return;
    }
    logger_1.logger.error('Docs file operation failed', err);
    next(err);
}
exports.docsRouter = router;
//# sourceMappingURL=docs.routes.js.map