// CVE Management Backend API
// Install: npm install express pg multer cors dotenv bcryptjs cookie-parser

require('dotenv').config();

const express = require('express');
const multer = require('multer');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const path = require('path');
const fs = require('fs');

// PostgreSQL connection pool
const pool = require('./db');

// Auth imports
const { requireAuth, requireGroup } = require('./middleware/auth');
const createAuthRouter = require('./routes/auth');
const createUsersRouter = require('./routes/users');
const createAuditLogRouter = require('./routes/auditLog');
const logAudit = require('./helpers/auditLog');
const createNvdLookupRouter = require('./routes/nvdLookup');
const createKnowledgeBaseRouter = require('./routes/knowledgeBase');
const createArcherTicketsRouter = require('./routes/archerTickets');
const createIvantiWorkflowsRouter = require('./routes/ivantiWorkflows');
const createIvantiFindingsRouter = require('./routes/ivantiFindings');
const createIvantiTodoQueueRouter = require('./routes/ivantiTodoQueue');
const createIvantiArchiveRouter       = require('./routes/ivantiArchive');
const createIvantiFpWorkflowRouter   = require('./routes/ivantiFpWorkflow');
const { createComplianceRouter }       = require('./routes/compliance');
const { createVCLMultiVerticalRouter } = require('./routes/vclMultiVertical');
const createAtlasRouter               = require('./routes/atlas');
const createJiraTicketsRouter         = require('./routes/jiraTickets');
const createCardApiRouter             = require('./routes/cardApi');
const createFeedbackRouter            = require('./routes/feedback');
const createWebhooksRouter            = require('./routes/webhooks');
const createNotificationsRouter       = require('./routes/notifications');

const app = express();
const PORT = process.env.PORT || 3001;
const API_HOST = process.env.API_HOST || 'localhost';
const SESSION_SECRET = process.env.SESSION_SECRET;
if (!SESSION_SECRET) {
    console.error('FATAL: SESSION_SECRET environment variable must be set');
    process.exit(1);
}
const CORS_ORIGINS = process.env.CORS_ORIGINS
    ? process.env.CORS_ORIGINS.split(',')
    : ['http://localhost:3000'];

// ========== SECURITY HELPERS ==========

// Allowed file extensions for document uploads (documents only, no executables)
const ALLOWED_EXTENSIONS = new Set([
    '.pdf', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.tif',
    '.txt', '.md', '.csv', '.log', '.msg', '.eml',
    '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
    '.odt', '.ods', '.odp',
    '.rtf', '.html', '.htm', '.xml', '.json', '.yaml', '.yml',
    '.zip', '.gz', '.tar', '.7z'
]);

// Allowed MIME type prefixes
const ALLOWED_MIME_PREFIXES = [
    'image/', 'text/', 'application/pdf',
    'application/msword', 'application/vnd.openxmlformats',
    'application/vnd.ms-', 'application/vnd.oasis.opendocument',
    'application/rtf', 'application/json', 'application/xml',
    'application/vnd.ms-outlook', 'message/rfc822',
    'application/zip', 'application/gzip', 'application/x-7z',
    'application/x-tar', 'application/octet-stream'
];

// Sanitize a single path segment (cveId, vendor, filename) to prevent traversal
function sanitizePathSegment(segment) {
    if (!segment || typeof segment !== 'string') return '';
    // Remove path separators, null bytes, and .. sequences
    return segment
        .replace(/\0/g, '')
        .replace(/\.\./g, '')
        .replace(/[\/\\]/g, '')
        .trim();
}

// Validate that a resolved path is within the uploads directory
function isPathWithinUploads(targetPath) {
    const uploadsRoot = path.resolve('uploads');
    const resolved = path.resolve(targetPath);
    return resolved.startsWith(uploadsRoot + path.sep) || resolved === uploadsRoot;
}

// Validate CVE ID format
const CVE_ID_PATTERN = /^CVE-\d{4}-\d{4,}$/;
function isValidCveId(cveId) {
    return typeof cveId === 'string' && CVE_ID_PATTERN.test(cveId);
}

// Allowed enum values
const VALID_SEVERITIES = ['Critical', 'High', 'Medium', 'Low'];
const VALID_STATUSES = ['Open', 'Addressed', 'In Progress', 'Resolved'];
const VALID_DOC_TYPES = ['advisory', 'email', 'screenshot', 'patch', 'other'];
const VALID_TICKET_STATUSES = ['Open', 'In Progress', 'Closed'];

// Validate vendor name - printable chars, reasonable length
function isValidVendor(vendor) {
    return typeof vendor === 'string' && vendor.trim().length > 0 && vendor.length <= 200;
}

// Log all incoming requests
app.use((req, res, next) => {
    console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
    next();
});

// Security headers
app.use((req, res, next) => {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-Frame-Options', 'SAMEORIGIN');  // Allow iframes from same origin
    res.setHeader('X-XSS-Protection', '1; mode=block');
    res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
    res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
    next();
});

// Middleware
app.use(cors({
  origin: CORS_ORIGINS,
  credentials: true
}));
// Only parse JSON for requests with application/json content type
app.use(express.json({
  limit: '1mb',
  type: 'application/json'
}));
app.use(cookieParser());
app.use('/uploads', express.static('uploads', {
    dotfiles: 'deny',
    index: false
}));

// Health check endpoint (public — used by CI/CD pipeline verification)
app.get('/api/health', (req, res) => {
    res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Auth routes (public)
app.use('/api/auth', createAuthRouter(logAudit));

// User management routes (admin only)
app.use('/api/users', createUsersRouter(requireAuth, requireGroup, logAudit));

// Audit log routes (admin only)
app.use('/api/audit-logs', createAuditLogRouter());

// NVD lookup routes (authenticated users)
app.use('/api/nvd', createNvdLookupRouter());

// Simple storage - upload to temp directory first
const storage = multer.diskStorage({
    destination: (req, file, cb) => {
        const tempDir = 'uploads/temp';
        if (!fs.existsSync(tempDir)) {
            fs.mkdirSync(tempDir, { recursive: true });
        }
        cb(null, tempDir);
    },
    filename: (req, file, cb) => {
        const timestamp = Date.now();
        // Sanitize original filename - strip path components and dangerous chars
        const safeName = sanitizePathSegment(file.originalname).replace(/[^a-zA-Z0-9._-]/g, '_');
        cb(null, `${timestamp}-${safeName}`);
    }
});

// File filter - reject executables and non-allowed types
function fileFilter(req, file, cb) {
    const ext = path.extname(file.originalname).toLowerCase();
    if (!ALLOWED_EXTENSIONS.has(ext)) {
        return cb(new Error(`File type '${ext}' is not allowed. Allowed types: ${[...ALLOWED_EXTENSIONS].join(', ')}`));
    }
    const mimeAllowed = ALLOWED_MIME_PREFIXES.some(prefix => file.mimetype.startsWith(prefix));
    if (!mimeAllowed) {
        return cb(new Error(`MIME type '${file.mimetype}' is not allowed.`));
    }
    cb(null, true);
}

const upload = multer({
    storage: storage,
    fileFilter: fileFilter,
    limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
});

// Knowledge base routes (editor/admin for upload/delete, all authenticated for view)
app.use('/api/knowledge-base', createKnowledgeBaseRouter(upload));

// Archer tickets routes (editor/admin for create/update/delete, all authenticated for view)
app.use('/api/archer-tickets', createArcherTicketsRouter());

// Ivanti / RiskSense workflow routes (all authenticated users)
app.use('/api/ivanti/workflows', createIvantiWorkflowsRouter());

// Ivanti / RiskSense host findings routes (all authenticated users)
// Pool imported directly inside the module; first arg kept for signature compat
app.use('/api/ivanti/findings', createIvantiFindingsRouter(pool, requireAuth));

// Ivanti queue routes — per-user staging queue for FP / Archer workflows
app.use('/api/ivanti/todo-queue', createIvantiTodoQueueRouter());

// Ivanti archive routes — finding archive tracking for severity score drift
app.use('/api/ivanti/archive', createIvantiArchiveRouter());

// Ivanti FP workflow routes — submit False Positive workflows to Ivanti API
app.use('/api/ivanti/fp-workflow', createIvantiFpWorkflowRouter());

// VCL multi-vertical routes — cross-organizational compliance reporting
// Must be mounted BEFORE the general compliance router since both share the /api/compliance prefix
app.use('/api/compliance/vcl-multi', createVCLMultiVerticalRouter(upload));

// AEO compliance routes — xlsx upload, non-compliant item tracking, notes
app.use('/api/compliance', createComplianceRouter(upload));

// Atlas InfoSec action plan routes — proxy CRUD to Atlas API, local cache for badges
app.use('/api/atlas', createAtlasRouter());

// Jira ticket routes — local CRUD + Jira REST API integration (lookup, sync, create)
app.use('/api/jira-tickets', createJiraTicketsRouter());

// CARD Asset Ownership API routes — proxy CARD operations, mutation flow, asset search
app.use('/api/card', createCardApiRouter());

// Feedback routes — bug reports and feature requests to GitLab
app.use('/api/feedback', createFeedbackRouter());

// In-app notifications routes (authenticated users)
app.use('/api/notifications', createNotificationsRouter());

// GitLab webhook routes — receives issue lifecycle events (no auth required)
app.use('/api/webhooks', createWebhooksRouter());

// ========== CVE ENDPOINTS ==========

// Get all CVEs with optional filters (authenticated users)
app.get('/api/cves', requireAuth(), async (req, res) => {
    const { search, vendor, severity, status } = req.query;

    let query = `
        SELECT c.*, COUNT(d.id) as document_count
        FROM cves c
        LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor
        WHERE 1=1
    `;

    const params = [];
    let paramIndex = 1;

    if (search) {
        query += ` AND (c.cve_id ILIKE $${paramIndex} OR c.description ILIKE $${paramIndex + 1})`;
        params.push(`%${search}%`, `%${search}%`);
        paramIndex += 2;
    }
    if (vendor && vendor !== 'All Vendors') {
        query += ` AND c.vendor = $${paramIndex++}`;
        params.push(vendor);
    }
    if (severity && severity !== 'All Severities') {
        query += ` AND c.severity = $${paramIndex++}`;
        params.push(severity);
    }
    if (status) {
        query += ` AND c.status = $${paramIndex++}`;
        params.push(status);
    }

    query += ` GROUP BY c.id ORDER BY c.published_date DESC`;

    try {
        const { rows } = await pool.query(query, params);
        res.json(rows);
    } catch (err) {
        console.error('Error fetching CVEs:', err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Get distinct CVE IDs for NVD sync (authenticated users)
app.get('/api/cves/distinct-ids', requireAuth(), async (req, res) => {
    try {
        const { rows } = await pool.query('SELECT DISTINCT cve_id FROM cves ORDER BY cve_id');
        res.json(rows.map(r => r.cve_id));
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Check if CVE exists and get its status - UPDATED FOR MULTI-VENDOR (authenticated users)
app.get('/api/cves/check/:cveId', requireAuth(), async (req, res) => {
    const { cveId } = req.params;

    const query = `
        SELECT c.*,
               COUNT(d.id) as total_documents,
               COUNT(CASE WHEN d.type = 'email' THEN 1 END) as has_email,
               COUNT(CASE WHEN d.type = 'screenshot' THEN 1 END) as has_screenshot
        FROM cves c
        LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor
        WHERE c.cve_id = $1
        GROUP BY c.id
    `;

    try {
        const { rows } = await pool.query(query, [cveId]);
        if (!rows || rows.length === 0) {
            return res.json({
                exists: false,
                message: 'CVE not found - not yet addressed'
            });
        }

        // Return all vendor entries for this CVE
        res.json({
            exists: true,
            vendors: rows.map(row => ({
                vendor: row.vendor,
                severity: row.severity,
                status: row.status,
                total_documents: parseInt(row.total_documents),
                doc_types: {
                    email: parseInt(row.has_email) > 0,
                    screenshot: parseInt(row.has_screenshot) > 0
                }
            })),
            addressed: true
        });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// NEW ENDPOINT: Get all vendors for a specific CVE (authenticated users)
app.get('/api/cves/:cveId/vendors', requireAuth(), async (req, res) => {
    const { cveId } = req.params;

    try {
        const { rows } = await pool.query(
            `SELECT vendor, severity, status, description, published_date
             FROM cves
             WHERE cve_id = $1
             ORDER BY vendor`,
            [cveId]
        );
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Get tooltip data for a specific CVE (authenticated users)
app.get('/api/cves/:cveId/tooltip', requireAuth(), async (req, res) => {
    const { cveId } = req.params;

    if (!CVE_ID_PATTERN.test(cveId)) {
        return res.status(400).json({ error: 'Invalid CVE ID format.' });
    }

    try {
        const { rows } = await pool.query(
            'SELECT cve_id, description, severity FROM cves WHERE cve_id = $1 LIMIT 1',
            [cveId]
        );
        const row = rows[0];
        if (!row) {
            return res.json({ exists: false });
        }
        let description = row.description || '';
        if (description.length > 300) {
            description = description.substring(0, 300) + '\u2026';
        }
        res.json({ exists: true, cve_id: row.cve_id, description, severity: row.severity });
    } catch (err) {
        console.error('Error fetching CVE tooltip:', err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Compliance export — reads from cve_document_status view
app.get('/api/cves/compliance', requireAuth(), async (req, res) => {
    try {
        const { rows } = await pool.query('SELECT * FROM cve_document_status ORDER BY cve_id, vendor');
        res.json(rows);
    } catch (err) {
        console.error('Error fetching compliance data:', err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin)
app.post('/api/cves', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
    const { cve_id, vendor, severity, description, published_date } = req.body;

    // Input validation
    if (!cve_id || !isValidCveId(cve_id)) {
        return res.status(400).json({ error: 'Invalid CVE ID format. Expected CVE-YYYY-NNNNN.' });
    }
    if (!vendor || !isValidVendor(vendor)) {
        return res.status(400).json({ error: 'Vendor is required and must be under 200 characters.' });
    }
    if (!severity || !VALID_SEVERITIES.includes(severity)) {
        return res.status(400).json({ error: `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}` });
    }
    if (!description || typeof description !== 'string' || description.length > 10000) {
        return res.status(400).json({ error: 'Description is required and must be under 10000 characters.' });
    }
    if (!published_date || !/^\d{4}-\d{2}-\d{2}$/.test(published_date)) {
        return res.status(400).json({ error: 'Published date is required in YYYY-MM-DD format.' });
    }

    try {
        const { rows } = await pool.query(
            `INSERT INTO cves (cve_id, vendor, severity, description, published_date, created_by)
             VALUES ($1, $2, $3, $4, $5, $6)
             RETURNING id`,
            [cve_id, vendor, severity, description, published_date, req.user.id]
        );

        logAudit({
            userId: req.user.id,
            username: req.user.username,
            action: 'cve_create',
            entityType: 'cve',
            entityId: cve_id,
            details: { vendor, severity },
            ipAddress: req.ip
        });

        res.json({
            id: rows[0].id,
            cve_id,
            message: `CVE created successfully for vendor: ${vendor}`
        });
    } catch (err) {
        console.error('DATABASE ERROR:', err);
        if (err.code === '23505') { // Postgres unique violation
            return res.status(409).json({
                error: 'This CVE already exists for this vendor. Choose a different vendor or update the existing entry.'
            });
        }
        res.status(500).json({ error: 'Failed to create CVE entry.' });
    }
});


// Update CVE status (editor or admin)
app.patch('/api/cves/:cveId/status', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
    const { cveId } = req.params;
    const { status } = req.body;

    if (!status || !VALID_STATUSES.includes(status)) {
        return res.status(400).json({ error: `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}` });
    }

    try {
        const result = await pool.query(
            'UPDATE cves SET status = $1, updated_at = NOW() WHERE cve_id = $2',
            [status, cveId]
        );

        logAudit({
            userId: req.user.id,
            username: req.user.username,
            action: 'cve_update_status',
            entityType: 'cve',
            entityId: cveId,
            details: { status },
            ipAddress: req.ip
        });

        res.json({ message: 'Status updated successfully', changes: result.rowCount });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Bulk sync CVE data from NVD (editor or admin)
app.post('/api/cves/nvd-sync', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
    const { updates } = req.body;
    if (!Array.isArray(updates) || updates.length === 0) {
        return res.status(400).json({ error: 'No updates provided' });
    }

    let updated = 0;
    const errors = [];

    for (const entry of updates) {
        const fields = [];
        const values = [];
        let paramIndex = 1;

        if (entry.description !== null && entry.description !== undefined) {
            fields.push(`description = $${paramIndex++}`);
            values.push(entry.description);
        }
        if (entry.severity !== null && entry.severity !== undefined) {
            fields.push(`severity = $${paramIndex++}`);
            values.push(entry.severity);
        }
        if (entry.published_date !== null && entry.published_date !== undefined) {
            fields.push(`published_date = $${paramIndex++}`);
            values.push(entry.published_date);
        }
        if (fields.length === 0) continue;

        fields.push('updated_at = NOW()');
        values.push(entry.cve_id);

        try {
            const result = await pool.query(
                `UPDATE cves SET ${fields.join(', ')} WHERE cve_id = $${paramIndex}`,
                values
            );
            updated += result.rowCount;
        } catch (err) {
            console.error('NVD sync update error:', err);
            errors.push({ cve_id: entry.cve_id, error: 'Update failed' });
        }
    }

    logAudit({
        userId: req.user.id,
        username: req.user.username,
        action: 'cve_nvd_sync',
        entityType: 'cve',
        entityId: null,
        details: { count: updated, cve_ids: updates.map(u => u.cve_id) },
        ipAddress: req.ip
    });

    const result = { message: 'NVD sync completed', updated };
    if (errors.length > 0) result.errors = errors;
    res.json(result);
});

// ========== CVE EDIT & DELETE ENDPOINTS ==========

// Edit single CVE entry (editor or admin)
app.put('/api/cves/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
    const { id } = req.params;
    const { cve_id, vendor, severity, description, published_date, status } = req.body;

    // Input validation for provided fields
    if (cve_id !== undefined && !isValidCveId(cve_id)) {
        return res.status(400).json({ error: 'Invalid CVE ID format. Expected CVE-YYYY-NNNNN.' });
    }
    if (vendor !== undefined && !isValidVendor(vendor)) {
        return res.status(400).json({ error: 'Vendor must be under 200 characters.' });
    }
    if (severity !== undefined && !VALID_SEVERITIES.includes(severity)) {
        return res.status(400).json({ error: `Invalid severity. Must be one of: ${VALID_SEVERITIES.join(', ')}` });
    }
    if (description !== undefined && (typeof description !== 'string' || description.length > 10000)) {
        return res.status(400).json({ error: 'Description must be under 10000 characters.' });
    }
    if (published_date !== undefined && !/^\d{4}-\d{2}-\d{2}$/.test(published_date)) {
        return res.status(400).json({ error: 'Published date must be in YYYY-MM-DD format.' });
    }
    if (status !== undefined && !VALID_STATUSES.includes(status)) {
        return res.status(400).json({ error: `Invalid status. Must be one of: ${VALID_STATUSES.join(', ')}` });
    }

    try {
        // Fetch existing row first
        const { rows: existingRows } = await pool.query('SELECT * FROM cves WHERE id = $1', [id]);
        const existing = existingRows[0];
        if (!existing) return res.status(404).json({ error: 'CVE entry not found' });

        const before = { cve_id: existing.cve_id, vendor: existing.vendor, severity: existing.severity, description: existing.description, published_date: existing.published_date, status: existing.status };

        const newCveId = cve_id !== undefined ? cve_id : existing.cve_id;
        const newVendor = vendor !== undefined ? vendor : existing.vendor;
        const cveIdChanged = newCveId !== existing.cve_id;
        const vendorChanged = newVendor !== existing.vendor;

        if (cveIdChanged || vendorChanged) {
            // Check UNIQUE constraint
            const { rows: conflictRows } = await pool.query(
                'SELECT id FROM cves WHERE cve_id = $1 AND vendor = $2 AND id != $3',
                [newCveId, newVendor, id]
            );
            if (conflictRows.length > 0) {
                return res.status(409).json({ error: 'A CVE entry with this CVE ID and vendor already exists.' });
            }

            // Rename document directory (with path traversal prevention)
            const oldDir = path.join('uploads', sanitizePathSegment(existing.cve_id), sanitizePathSegment(existing.vendor));
            const newDir = path.join('uploads', sanitizePathSegment(newCveId), sanitizePathSegment(newVendor));

            if (!isPathWithinUploads(oldDir) || !isPathWithinUploads(newDir)) {
                return res.status(400).json({ error: 'Invalid CVE ID or vendor name for file paths.' });
            }

            if (fs.existsSync(oldDir)) {
                const newParent = path.join('uploads', newCveId);
                if (!fs.existsSync(newParent)) {
                    fs.mkdirSync(newParent, { recursive: true });
                }
                fs.renameSync(oldDir, newDir);

                // Clean up old cve_id directory if empty
                const oldParent = path.join('uploads', existing.cve_id);
                if (fs.existsSync(oldParent)) {
                    const remaining = fs.readdirSync(oldParent);
                    if (remaining.length === 0) fs.rmdirSync(oldParent);
                }
            }

            // Update documents table - file paths
            const { rows: docs } = await pool.query(
                'SELECT id, file_path FROM documents WHERE cve_id = $1 AND vendor = $2',
                [existing.cve_id, existing.vendor]
            );

            const oldPrefix = path.join('uploads', existing.cve_id, existing.vendor);
            const newPrefix = path.join('uploads', newCveId, newVendor);

            for (const doc of docs) {
                const newFilePath = doc.file_path.replace(oldPrefix, newPrefix);
                await pool.query(
                    'UPDATE documents SET cve_id = $1, vendor = $2, file_path = $3 WHERE id = $4',
                    [newCveId, newVendor, newFilePath, doc.id]
                );
            }
        }

        // Build dynamic SET clause
        const fields = [];
        const values = [];
        let paramIndex = 1;
        if (cve_id !== undefined) { fields.push(`cve_id = $${paramIndex++}`); values.push(cve_id); }
        if (vendor !== undefined) { fields.push(`vendor = $${paramIndex++}`); values.push(vendor); }
        if (severity !== undefined) { fields.push(`severity = $${paramIndex++}`); values.push(severity); }
        if (description !== undefined) { fields.push(`description = $${paramIndex++}`); values.push(description); }
        if (published_date !== undefined) { fields.push(`published_date = $${paramIndex++}`); values.push(published_date); }
        if (status !== undefined) { fields.push(`status = $${paramIndex++}`); values.push(status); }

        if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });

        fields.push('updated_at = NOW()');
        values.push(id);

        const updateResult = await pool.query(
            `UPDATE cves SET ${fields.join(', ')} WHERE id = $${paramIndex}`,
            values
        );

        const after = {
            cve_id: newCveId, vendor: newVendor,
            severity: severity !== undefined ? severity : existing.severity,
            description: description !== undefined ? description : existing.description,
            published_date: published_date !== undefined ? published_date : existing.published_date,
            status: status !== undefined ? status : existing.status
        };

        logAudit({
            userId: req.user.id,
            username: req.user.username,
            action: 'cve_edit',
            entityType: 'cve',
            entityId: newCveId,
            details: { before, after },
            ipAddress: req.ip
        });

        res.json({ message: 'CVE updated successfully', changes: updateResult.rowCount });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Delete entire CVE - all vendors (editor or admin) - MUST be before /:id route
app.delete('/api/cves/by-cve-id/:cveId', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
    const { cveId } = req.params;

    try {
        // Get all rows for this CVE ID to know what we're deleting
        const { rows } = await pool.query('SELECT * FROM cves WHERE cve_id = $1', [cveId]);
        if (!rows || rows.length === 0) return res.status(404).json({ error: 'No CVE entries found for this CVE ID' });

        // Ownership check: Standard_User can only delete CVEs they created
        if (req.user.group === 'Standard_User') {
            const notOwned = rows.some(row => row.created_by !== req.user.id);
            if (notOwned) {
                return res.status(403).json({ error: 'You can only delete resources you created' });
            }

            // Cascade impact check for Standard_User
            const { rows: archerTickets } = await pool.query(
                'SELECT id, exc_number, cve_id, vendor FROM archer_tickets WHERE cve_id = $1',
                [cveId]
            );

            let jiraTickets = [];
            try {
                const jiraResult = await pool.query(
                    'SELECT id, cve_id, vendor, ticket_key, status FROM jira_tickets WHERE cve_id = $1',
                    [cveId]
                );
                jiraTickets = jiraResult.rows;
            } catch (jiraErr) {
                // If table doesn't exist yet, treat as empty
                if (!jiraErr.message.includes('does not exist')) throw jiraErr;
            }

            const { rows: docs } = await pool.query(
                'SELECT id, name, type FROM documents WHERE cve_id = $1',
                [cveId]
            );

            const allTickets = [
                ...(archerTickets || []).map(t => ({ ...t, source: 'archer', key: t.exc_number })),
                ...(jiraTickets || []).map(t => ({ ...t, source: 'jira', key: t.ticket_key }))
            ];

            // If no tickets at all, no compliance linkage possible — return cascade info
            if (allTickets.length === 0) {
                return res.json({
                    cascade_impact: {
                        archer_tickets: [],
                        jira_tickets: [],
                        documents: (docs || []).map(d => ({ id: d.id, name: d.name, type: d.type })),
                        blocked: false,
                        blocked_reason: null
                    }
                });
            }

            // Check compliance linkage for each ticket
            const likeConditions = [];
            const likeParams = [];
            let pIdx = 1;
            for (const t of allTickets) {
                likeConditions.push(`ci.extra_json LIKE $${pIdx++}`);
                likeParams.push(`%${t.key}%`);
            }
            // Also check if the CVE ID itself appears in compliance extra_json
            likeConditions.push(`ci.extra_json LIKE $${pIdx++}`);
            likeParams.push(`%${cveId}%`);

            let compLinks = [];
            try {
                const compResult = await pool.query(
                    `SELECT ci.id, ci.extra_json, cu.report_date
                     FROM compliance_items ci
                     JOIN compliance_uploads cu ON ci.upload_id = cu.id
                     WHERE ci.status = 'active' AND (${likeConditions.join(' OR ')})`,
                    likeParams
                );
                compLinks = compResult.rows;
            } catch (compErr) {
                if (!compErr.message.includes('does not exist')) throw compErr;
            }

            // Determine which tickets are compliance-linked
            const linkedTicketKeys = new Set();
            for (const cl of compLinks) {
                const json = cl.extra_json || '';
                for (const t of allTickets) {
                    if (json.includes(t.key)) {
                        linkedTicketKeys.add(`${t.source}:${t.id}`);
                    }
                }
                if (json.includes(cveId)) {
                    for (const t of allTickets) {
                        linkedTicketKeys.add(`${t.source}:${t.id}`);
                    }
                }
            }

            const archerTicketsResult = (archerTickets || []).map(t => ({
                id: t.id,
                exc_number: t.exc_number,
                compliance_linked: linkedTicketKeys.has(`archer:${t.id}`)
            }));

            const jiraTicketsResult = (jiraTickets || []).map(t => ({
                id: t.id,
                ticket_key: t.ticket_key,
                compliance_linked: linkedTicketKeys.has(`jira:${t.id}`)
            }));

            const documentsResult = (docs || []).map(d => ({
                id: d.id,
                name: d.name,
                type: d.type
            }));

            const hasComplianceLink = archerTicketsResult.some(t => t.compliance_linked)
                || jiraTicketsResult.some(t => t.compliance_linked);

            if (hasComplianceLink) {
                const blockedArcher = archerTicketsResult.find(t => t.compliance_linked);
                const blockedJira = jiraTicketsResult.find(t => t.compliance_linked);
                const blockedLabel = blockedArcher
                    ? `Archer ticket ${blockedArcher.exc_number}`
                    : `JIRA ticket ${blockedJira.ticket_key}`;
                return res.status(403).json({
                    error: 'CVE deletion blocked: associated ticket linked to compliance report',
                    cascade_impact: {
                        archer_tickets: archerTicketsResult,
                        jira_tickets: jiraTicketsResult,
                        documents: documentsResult,
                        blocked: true,
                        blocked_reason: `${blockedLabel} is linked to a compliance report`
                    }
                });
            }

            // Not blocked — return cascade impact for frontend warning
            return res.json({
                cascade_impact: {
                    archer_tickets: archerTicketsResult,
                    jira_tickets: jiraTicketsResult,
                    documents: documentsResult,
                    blocked: false,
                    blocked_reason: null
                }
            });
        }

        // Admin flow: proceed directly with deletion
        await pool.query('DELETE FROM documents WHERE cve_id = $1', [cveId]);
        const deleteResult = await pool.query('DELETE FROM cves WHERE cve_id = $1', [cveId]);

        // Remove upload directory (with path traversal prevention)
        const safeCveId = sanitizePathSegment(cveId);
        const cveDir = path.join('uploads', safeCveId);
        if (safeCveId && isPathWithinUploads(cveDir) && fs.existsSync(cveDir)) {
            fs.rmSync(cveDir, { recursive: true, force: true });
        }

        logAudit({
            userId: req.user.id,
            username: req.user.username,
            action: 'cve_delete',
            entityType: 'cve',
            entityId: cveId,
            details: { type: 'all_vendors', vendors: rows.map(r => r.vendor), count: rows.length },
            ipAddress: req.ip
        });

        res.json({ message: `Deleted CVE ${cveId} and all ${rows.length} vendor entries`, deleted: deleteResult.rowCount });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});


// Delete single CVE vendor entry (editor or admin)
app.delete('/api/cves/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
    const { id } = req.params;

    try {
        const { rows: cveRows } = await pool.query('SELECT * FROM cves WHERE id = $1', [id]);
        const cve = cveRows[0];
        if (!cve) return res.status(404).json({ error: 'CVE entry not found' });

        // Ownership check: Standard_User can only delete CVEs they created
        if (req.user.group === 'Standard_User' && cve.created_by !== req.user.id) {
            return res.status(403).json({ error: 'You can only delete resources you created' });
        }

        // Cascade/compliance check for Standard_User
        if (req.user.group === 'Standard_User') {
            const { rows: archerTickets } = await pool.query(
                'SELECT id, exc_number FROM archer_tickets WHERE cve_id = $1 AND vendor = $2',
                [cve.cve_id, cve.vendor]
            );

            let jiraTickets = [];
            try {
                const jiraResult = await pool.query(
                    'SELECT id, ticket_key FROM jira_tickets WHERE cve_id = $1 AND vendor = $2',
                    [cve.cve_id, cve.vendor]
                );
                jiraTickets = jiraResult.rows;
            } catch (jiraErr) {
                if (!jiraErr.message.includes('does not exist')) throw jiraErr;
            }

            const allTickets = [
                ...(archerTickets || []).map(t => ({ ...t, source: 'archer', key: t.exc_number })),
                ...(jiraTickets || []).map(t => ({ ...t, source: 'jira', key: t.ticket_key }))
            ];

            if (allTickets.length > 0) {
                const likeConditions = allTickets.map((_, i) => `ci.extra_json LIKE $${i + 1}`);
                const likeParams = allTickets.map(t => `%${t.key}%`);

                let compLinks = [];
                try {
                    const compResult = await pool.query(
                        `SELECT ci.id, ci.extra_json FROM compliance_items ci
                         JOIN compliance_uploads cu ON ci.upload_id = cu.id
                         WHERE ci.status = 'active' AND (${likeConditions.join(' OR ')})`,
                        likeParams
                    );
                    compLinks = compResult.rows;
                } catch (compErr) {
                    if (!compErr.message.includes('does not exist')) throw compErr;
                }

                const hasLink = compLinks.some(cl => {
                    const json = cl.extra_json || '';
                    return allTickets.some(t => json.includes(t.key));
                });

                if (hasLink) {
                    return res.status(403).json({
                        error: 'CVE deletion blocked: associated ticket linked to compliance report',
                        cascade_impact: { blocked: true, blocked_reason: 'Associated ticket is linked to a compliance report' }
                    });
                }
            }
        }

        // Proceed with deletion
        // Delete associated documents from DB and disk
        const { rows: docs } = await pool.query(
            'SELECT id, file_path FROM documents WHERE cve_id = $1 AND vendor = $2',
            [cve.cve_id, cve.vendor]
        );

        // Delete document files from disk (with path traversal prevention)
        if (docs && docs.length > 0) {
            docs.forEach(doc => {
                if (doc.file_path && isPathWithinUploads(doc.file_path) && fs.existsSync(doc.file_path)) {
                    fs.unlinkSync(doc.file_path);
                }
            });
        }

        // Delete documents from DB
        await pool.query('DELETE FROM documents WHERE cve_id = $1 AND vendor = $2', [cve.cve_id, cve.vendor]);

        // Delete CVE row
        const deleteResult = await pool.query('DELETE FROM cves WHERE id = $1', [id]);

        // Clean up directories (with path traversal prevention)
        const safeVendorDir = path.join('uploads', sanitizePathSegment(cve.cve_id), sanitizePathSegment(cve.vendor));
        if (isPathWithinUploads(safeVendorDir) && fs.existsSync(safeVendorDir)) {
            fs.rmSync(safeVendorDir, { recursive: true, force: true });
        }
        const safeCveDir = path.join('uploads', sanitizePathSegment(cve.cve_id));
        if (isPathWithinUploads(safeCveDir) && fs.existsSync(safeCveDir)) {
            const remaining = fs.readdirSync(safeCveDir);
            if (remaining.length === 0) fs.rmdirSync(safeCveDir);
        }

        logAudit({
            userId: req.user.id,
            username: req.user.username,
            action: 'cve_delete',
            entityType: 'cve',
            entityId: cve.cve_id,
            details: { type: 'single_vendor', vendor: cve.vendor, severity: cve.severity },
            ipAddress: req.ip
        });

        res.json({ message: `Deleted ${cve.vendor} entry for ${cve.cve_id}` });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// ========== DOCUMENT ENDPOINTS ==========

// Get documents for a CVE - FILTER BY VENDOR (authenticated users)
app.get('/api/cves/:cveId/documents', requireAuth(), async (req, res) => {
    const { cveId } = req.params;
    const { vendor } = req.query;  // Optional vendor filter

    let query = 'SELECT * FROM documents WHERE cve_id = $1';
    const params = [cveId];
    let paramIndex = 2;

    if (vendor) {
        query += ` AND vendor = $${paramIndex++}`;
        params.push(vendor);
    }

    query += ' ORDER BY uploaded_at DESC';

    try {
        const { rows } = await pool.query(query, params);
        res.json(rows);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Upload document - ADD ERROR HANDLING FOR MULTER (editor or admin)
app.post('/api/cves/:cveId/documents', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res, next) => {
    upload.single('file')(req, res, async (err) => {
        if (err) {
            console.error('Upload error:', err.message);
            if (err.message && (err.message.startsWith('File type') || err.message.startsWith('MIME type'))) {
                return res.status(400).json({ error: err.message });
            }
            if (err.code === 'LIMIT_FILE_SIZE') {
                return res.status(400).json({ error: 'File exceeds the 10MB size limit.' });
            }
            return res.status(500).json({ error: 'File upload failed.' });
        }

        const { cveId } = req.params;
        const { type, notes, vendor } = req.body;
        const file = req.file;

        if (!file) {
            console.error('ERROR: No file uploaded');
            return res.status(400).json({ error: 'No file uploaded' });
        }

        if (!vendor) {
            if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
            return res.status(400).json({ error: 'Vendor is required' });
        }

        // Validate document type
        if (type && !VALID_DOC_TYPES.includes(type)) {
            if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
            return res.status(400).json({ error: `Invalid document type. Must be one of: ${VALID_DOC_TYPES.join(', ')}` });
        }

        // Sanitize path segments to prevent directory traversal
        const safeCveId = sanitizePathSegment(cveId);
        const safeVendor = sanitizePathSegment(vendor);
        const safeFilename = sanitizePathSegment(file.filename);

        if (!safeCveId || !safeVendor || !safeFilename) {
            if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
            return res.status(400).json({ error: 'Invalid CVE ID, vendor, or filename.' });
        }

        // Move file from temp to proper location
        const finalDir = path.join('uploads', safeCveId, safeVendor);
        const finalPath = path.join(finalDir, safeFilename);

        // Verify paths stay within uploads directory
        if (!isPathWithinUploads(finalDir) || !isPathWithinUploads(finalPath)) {
            if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
            return res.status(400).json({ error: 'Invalid file path.' });
        }

        if (!fs.existsSync(finalDir)) {
            fs.mkdirSync(finalDir, { recursive: true });
        }

        // Move file from temp to final location
        fs.renameSync(file.path, finalPath);

        const fileSizeKB = (file.size / 1024).toFixed(2) + ' KB';

        try {
            const { rows } = await pool.query(
                `INSERT INTO documents (cve_id, vendor, name, type, file_path, file_size, mime_type, notes)
                 VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
                 RETURNING id`,
                [cveId, vendor, file.originalname, type, finalPath, fileSizeKB, file.mimetype, notes]
            );

            logAudit({
                userId: req.user.id,
                username: req.user.username,
                action: 'document_upload',
                entityType: 'document',
                entityId: cveId,
                details: { vendor, type, filename: file.originalname },
                ipAddress: req.ip
            });

            res.json({
                id: rows[0].id,
                message: 'Document uploaded successfully',
                file: {
                    name: file.originalname,
                    size: fileSizeKB
                }
            });
        } catch (dbErr) {
            console.error('Document insert error:', dbErr);
            // If database insert fails, delete the file
            if (fs.existsSync(finalPath)) {
                fs.unlinkSync(finalPath);
            }
            res.status(500).json({ error: 'Internal server error.' });
        }
    });
});

// Delete document (admin only)
app.delete('/api/documents/:id', requireAuth(), requireGroup('Admin'), async (req, res) => {
    const { id } = req.params;

    try {
        // First get the file path to delete the actual file
        const { rows } = await pool.query('SELECT file_path FROM documents WHERE id = $1', [id]);
        const row = rows[0];

        // Only delete file if path is within uploads directory
        if (row && row.file_path && isPathWithinUploads(row.file_path) && fs.existsSync(row.file_path)) {
            fs.unlinkSync(row.file_path);
        }

        await pool.query('DELETE FROM documents WHERE id = $1', [id]);

        logAudit({
            userId: req.user.id,
            username: req.user.username,
            action: 'document_delete',
            entityType: 'document',
            entityId: id,
            details: { file_path: row ? row.file_path : null },
            ipAddress: req.ip
        });

        res.json({ message: 'Document deleted successfully' });
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// ========== UTILITY ENDPOINTS ==========

// Get all vendors (authenticated users)
app.get('/api/vendors', requireAuth(), async (req, res) => {
    try {
        const { rows } = await pool.query('SELECT DISTINCT vendor FROM cves ORDER BY vendor');
        res.json(rows.map(r => r.vendor));
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Get statistics (authenticated users)
app.get('/api/stats', requireAuth(), async (req, res) => {
    const query = `
        SELECT
            COUNT(DISTINCT c.id) as total_cves,
            COUNT(DISTINCT CASE WHEN c.severity = 'Critical' THEN c.id END) as critical_count,
            COUNT(DISTINCT CASE WHEN c.status = 'Addressed' THEN c.id END) as addressed_count,
            COUNT(DISTINCT d.id) as total_documents,
            COUNT(DISTINCT CASE WHEN cd.compliance_status = 'Complete' THEN c.id END) as compliant_count
        FROM cves c
        LEFT JOIN documents d ON c.cve_id = d.cve_id
        LEFT JOIN cve_document_status cd ON c.cve_id = cd.cve_id
    `;

    try {
        const { rows } = await pool.query(query);
        res.json(rows[0]);
    } catch (err) {
        console.error(err);
        res.status(500).json({ error: 'Internal server error.' });
    }
});

// Serve frontend build (for testing/production — serves React SPA)
const frontendBuild = path.join(__dirname, '..', 'frontend', 'build');
if (fs.existsSync(frontendBuild)) {
    app.use(express.static(frontendBuild));
    // SPA fallback — serve index.html for any non-API route
    app.use((req, res, next) => {
        if (!req.path.startsWith('/api/') && !req.path.startsWith('/uploads/')) {
            res.sendFile(path.join(frontendBuild, 'index.html'));
        } else {
            next();
        }
    });
}

// Start server
app.listen(PORT, () => {
    console.log(`CVE API server running on http://${API_HOST}:${PORT}`);
    console.log(`CORS origins: ${CORS_ORIGINS.join(', ')}`);
});
