// routes/ivantiFpWorkflow.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const pool = require('../db');
const { requireAuth, requireGroup } = require('../middleware/auth');
const { ivantiFormPost, ivantiPost } = require('../helpers/ivantiApi');
const logAudit = require('../helpers/auditLog');

// ---------------------------------------------------------------------------
// Pure helpers (exported for testing)
// ---------------------------------------------------------------------------

const ALLOWED_EXTENSIONS = new Set([
    '.pdf', '.png', '.jpg', '.jpeg', '.gif',
    '.doc', '.docx', '.xlsx', '.csv', '.txt', '.zip'
]);

function isAllowedFileExtension(filename) {
    if (!filename || typeof filename !== 'string') return false;
    const ext = path.extname(filename).toLowerCase();
    return ALLOWED_EXTENSIONS.has(ext);
}

function validateFpWorkflowForm(body) {
    const errors = {};
    if (!body.name || typeof body.name !== 'string' || body.name.trim().length === 0) {
        errors.name = 'Workflow name is required.';
    } else if (body.name.trim().length > 255) {
        errors.name = 'Workflow name must be 255 characters or fewer.';
    }
    if (!body.reason || typeof body.reason !== 'string' || body.reason.trim().length === 0) {
        errors.reason = 'Reason is required.';
    }
    if (body.description !== undefined && body.description !== null && body.description !== '') {
        if (typeof body.description !== 'string') errors.description = 'Description must be a string.';
        else if (body.description.length > 2000) errors.description = 'Description must be 2000 characters or fewer.';
    }
    if (!body.expirationDate || typeof body.expirationDate !== 'string' || body.expirationDate.trim().length === 0) {
        errors.expirationDate = 'Expiration date is required.';
    } else {
        const parsed = new Date(body.expirationDate);
        if (isNaN(parsed.getTime())) {
            errors.expirationDate = 'Expiration date must be a valid date.';
        } else {
            const today = new Date(); today.setHours(0, 0, 0, 0);
            const expDay = new Date(parsed); expDay.setHours(0, 0, 0, 0);
            if (expDay <= today) errors.expirationDate = 'Expiration date must be in the future.';
            else { const maxDate = new Date(today); maxDate.setDate(maxDate.getDate() + 120); if (expDay > maxDate) errors.expirationDate = 'Expiration date cannot be more than 120 days from today.'; }
        }
    }
    return errors;
}

function buildSubjectFilterRequest(findingIds) {
    return JSON.stringify({ subject: 'hostFinding', filterRequest: { filters: [{ field: 'id', exclusive: false, operator: 'IN', value: findingIds.map(id => String(id)).join(',') }] } });
}

function buildIvantiFormFields(formData, findingIds) {
    const scopeMap = { 'Authorized': 'AUTHORIZED', 'None': 'NONE', 'Automated': 'AUTOMATED' };
    return [
        { name: 'name', value: formData.name },
        { name: 'reason', value: formData.reason },
        { name: 'description', value: formData.description || '' },
        { name: 'expirationDate', value: formData.expirationDate },
        { name: 'overrideControl', value: scopeMap[formData.scopeOverride] || 'AUTHORIZED' },
        { name: 'subjectFilterRequest', value: buildSubjectFilterRequest(findingIds) },
        { name: 'isEmptyWorkflow', value: findingIds.length === 0 ? 'true' : 'false' }
    ];
}

const LIFECYCLE_STATUSES = new Set(['submitted', 'approved', 'rejected', 'rework', 'resubmitted']);

function validateLifecycleTransition(currentStatus, newStatus) {
    if (currentStatus === 'approved') return { valid: false, error: 'This submission is finalized and cannot be edited.' };
    if (!LIFECYCLE_STATUSES.has(newStatus)) return { valid: false, error: 'Invalid lifecycle status.' };
    return { valid: true };
}

function mergeFindings(existingJson, newIds) {
    const existing = JSON.parse(existingJson || '[]');
    const merged = [...new Set([...existing, ...newIds])];
    return JSON.stringify(merged);
}

function buildSubmissionHistoryEntry(changeType, details, userId, username) {
    return { user_id: userId, username: username, change_type: changeType, change_details_json: JSON.stringify(details), created_at: new Date().toISOString() };
}

function filterVisibleSubmissions(submissions) {
    return submissions.filter(s => s.lifecycle_status !== 'approved' && s.dismissed_at == null);
}

function shouldShowDismissButton(submission) {
    return submission.lifecycle_status === 'rejected' && submission.dismissed_at == null;
}

// ---------------------------------------------------------------------------
// Resolve workflow batch UUID
// ---------------------------------------------------------------------------
async function resolveWorkflowBatchUuid(submission, apiKey, clientId, skipTls) {
    if (submission.ivanti_workflow_batch_uuid) return submission.ivanti_workflow_batch_uuid;

    const searchUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/search`;
    const workflowName = submission.workflow_name || '';
    const searchBody = { filters: workflowName ? [{ field: 'name', exclusive: false, operator: 'EXACT', value: workflowName }] : [], projection: 'internal', sort: [{ field: 'created', direction: 'DESC' }], page: 0, size: 10 };

    let result;
    try { result = await ivantiPost(searchUrl, searchBody, apiKey, skipTls); } catch (e) { return null; }
    if (result.status !== 200) return null;

    let uuid = null;
    try {
        const data = JSON.parse(result.body);
        let batches = data._embedded?.workflowBatches || data._embedded?.workflowBatch || data.content || data.data || (Array.isArray(data) ? data : []);
        const batchId = String(submission.ivanti_workflow_batch_id);
        const batch = batches.find(b => String(b.id) === batchId) || batches[0];
        if (batch) uuid = batch.uuid || batch.workflowBatchUuid || batch.batchUuid || batch.groupUuid || batch.group_uuid || batch.workflow_batch_uuid || batch.uid || null;
    } catch (e) { return null; }

    if (uuid && submission.id) {
        pool.query(`UPDATE ivanti_fp_submissions SET ivanti_workflow_batch_uuid = $1 WHERE id = $2`, [uuid, submission.id]).catch(() => {});
    }
    return uuid;
}

// ---------------------------------------------------------------------------
// Multer configuration
// ---------------------------------------------------------------------------
const uploadStorage = multer.memoryStorage();
const fpUpload = multer({ storage: uploadStorage, limits: { fileSize: 10 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (isAllowedFileExtension(file.originalname)) cb(null, true); else cb(new Error(`File type not allowed: ${path.extname(file.originalname)}`)); } }).array('attachments', 10);

// ---------------------------------------------------------------------------
// Router factory
// ---------------------------------------------------------------------------
function createIvantiFpWorkflowRouter() {
    const router = express.Router();

    /**
     * GET /api/ivanti/fp-workflow/documents/search
     *
     * Searches the documents library by name, CVE ID, or vendor.
     * Returns up to 50 results ordered by upload date descending.
     *
     * @query {string} q — Optional search term; matches against name, cve_id, or vendor (ILIKE)
     * @returns {Array<Object>} Array of document metadata objects
     *   - id {number}
     *   - cve_id {string}
     *   - vendor {string}
     *   - name {string}
     *   - type {string}
     *   - file_size {number}
     *   - mime_type {string}
     *   - uploaded_at {string}
     * @error 500 Database error
     */
    router.get('/documents/search', requireAuth(), async (req, res) => {
        const q = (req.query.q || '').trim();
        try {
            let rows;
            if (q) {
                const like = `%${q}%`;
                const result = await pool.query(
                    `SELECT id, cve_id, vendor, name, type, file_size, mime_type, uploaded_at FROM documents WHERE name ILIKE $1 OR cve_id ILIKE $2 OR vendor ILIKE $3 ORDER BY uploaded_at DESC LIMIT 50`,
                    [like, like, like]
                );
                rows = result.rows;
            } else {
                const result = await pool.query(`SELECT id, cve_id, vendor, name, type, file_size, mime_type, uploaded_at FROM documents ORDER BY uploaded_at DESC LIMIT 50`);
                rows = result.rows;
            }
            res.json(rows || []);
        } catch (err) {
            console.error('Error searching documents:', err);
            res.status(500).json({ error: 'Database error.' });
        }
    });

    /**
     * POST /api/ivanti/fp-workflow
     *
     * Creates a new False Positive workflow in Ivanti and records the submission locally.
     * Validates queue items belong to the user, are FP type, and are pending.
     * Uploads local files and library documents as attachments.
     * Marks associated queue items as complete on success.
     * Requires Admin or Standard_User group.
     *
     * @body {Object} multipart/form-data
     *   - findingIds {string} JSON array of Ivanti finding IDs
     *   - queueItemIds {string} JSON array of todo queue item IDs
     *   - libraryDocIds {string} Optional JSON array of document library IDs to attach
     *   - name {string} Workflow name (required, max 255 chars)
     *   - reason {string} Reason for false positive (required)
     *   - description {string} Optional description (max 2000 chars)
     *   - expirationDate {string} Required, must be future date within 120 days
     *   - scopeOverride {string} Optional: Authorized, None, or Automated
     *   - attachments {File[]} Optional uploaded files (max 10, each max 10 MB)
     * @returns {Object}
     *   - success {boolean}
     *   - workflowBatchId {number} Ivanti workflow batch ID
     *   - queueItemsUpdated {number} Count of queue items marked complete
     *   - status {string} "success"
     * @error 400 Invalid input, queue item validation failure
     * @error 403 Queue items belong to another user
     * @error 429 Ivanti API rate limit
     * @error 500 Ivanti API key not configured
     * @error 502 Ivanti API connection or response failure
     */
    router.post('/', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res) => {
        fpUpload(req, res, (multerErr) => {
            if (multerErr) {
                if (multerErr.code === 'LIMIT_FILE_SIZE') return res.status(400).json({ error: 'File exceeds the 10 MB size limit.' });
                return res.status(400).json({ error: multerErr.message });
            }

            let findingIds, queueItemIds;
            try { findingIds = JSON.parse(req.body.findingIds || '[]'); queueItemIds = JSON.parse(req.body.queueItemIds || '[]'); }
            catch (e) { return res.status(400).json({ error: 'findingIds and queueItemIds must be valid JSON arrays.' }); }

            if (!Array.isArray(findingIds) || findingIds.length === 0) return res.status(400).json({ error: 'At least one finding ID is required.' });
            if (!Array.isArray(queueItemIds) || queueItemIds.length === 0) return res.status(400).json({ error: 'At least one queue item ID is required.' });

            let libraryDocIds;
            try { libraryDocIds = JSON.parse(req.body.libraryDocIds || '[]'); } catch (e) { return res.status(400).json({ error: 'libraryDocIds must be a valid JSON array.' }); }
            if (!Array.isArray(libraryDocIds)) return res.status(400).json({ error: 'libraryDocIds must be a valid JSON array.' });
            for (const id of libraryDocIds) { if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: `Invalid library document ID: ${id}.` }); }

            const validationErrors = validateFpWorkflowForm(req.body);
            if (Object.keys(validationErrors).length > 0) return res.status(400).json({ success: false, errors: validationErrors });

            const files = req.files || [];
            for (const file of files) { if (!isAllowedFileExtension(file.originalname)) return res.status(400).json({ error: `File type not allowed: ${file.originalname}` }); }

            (async () => {
                // Verify queue items
                const { rows: queueRows } = await pool.query(
                    `SELECT id, workflow_type, status, user_id FROM ivanti_todo_queue WHERE id = ANY($1)`, [queueItemIds]
                );
                if (!queueRows || queueRows.length !== queueItemIds.length) return res.status(400).json({ error: 'One or more queue items not found.' });
                for (const row of queueRows) {
                    if (row.user_id !== req.user.id) return res.status(403).json({ error: 'You can only submit your own queue items.' });
                    if (row.workflow_type !== 'FP') return res.status(400).json({ error: `Queue item ${row.id} is not an FP workflow type.` });
                    if (row.status !== 'pending') return res.status(400).json({ error: `Queue item ${row.id} is not in pending status.` });
                }

                const apiKey = process.env.IVANTI_API_KEY;
                const clientId = process.env.IVANTI_CLIENT_ID || '1550';
                const skipTls = process.env.IVANTI_SKIP_TLS === 'true';
                if (!apiKey) return res.status(500).json({ success: false, error: 'Ivanti API key is not configured.', step: 'create_workflow' });

                const formFields = buildIvantiFormFields(req.body, findingIds);

                // Look up library documents
                let libraryDocs = [];
                const libraryAttachmentResults = [];
                if (libraryDocIds.length > 0) {
                    const { rows: docRows } = await pool.query(`SELECT id, name, file_path, file_size, mime_type FROM documents WHERE id = ANY($1)`, [libraryDocIds]);
                    libraryDocs = docRows;
                    const foundIds = new Set(libraryDocs.map(d => d.id));
                    const missingIds = libraryDocIds.filter(id => !foundIds.has(id));
                    if (missingIds.length > 0) return res.status(400).json({ error: `Library document IDs not found: ${missingIds.join(', ')}` });
                    // Validate file types for library docs (same rules as local uploads)
                    const unsupportedDocs = libraryDocs.filter(d => !isAllowedFileExtension(d.name));
                    if (unsupportedDocs.length > 0) {
                        const names = unsupportedDocs.map(d => d.name).join(', ');
                        return res.status(400).json({ error: `Library document file type not supported by Ivanti: ${names}. Allowed: ${[...ALLOWED_EXTENSIONS].join(', ')}` });
                    }
                }

                const libraryFormFiles = [];
                for (const doc of libraryDocs) {
                    try { const buffer = fs.readFileSync(doc.file_path); libraryFormFiles.push({ name: 'files', buffer, filename: doc.name }); libraryAttachmentResults.push({ filename: doc.name, success: true, source: 'library', documentId: doc.id }); }
                    catch (readErr) { libraryAttachmentResults.push({ success: false, error: 'File not found on disk', source: 'library', documentId: doc.id, filename: doc.name }); }
                }

                const localFormFiles = files.map(f => ({ name: 'files', buffer: f.buffer, filename: f.originalname }));
                const formFiles = [...localFormFiles, ...libraryFormFiles];
                const createUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/falsePositive/request`;

                let createResult;
                try { createResult = await ivantiFormPost(createUrl, formFields, formFiles, apiKey, skipTls); }
                catch (networkErr) {
                    logAudit({ userId: req.user.id, username: req.user.username, action: 'ivanti_fp_workflow_failed', entityType: 'ivanti_workflow', details: { error: networkErr.message, findingIds }, ipAddress: req.ip });
                    return res.status(502).json({ success: false, error: 'Failed to connect to Ivanti API.', step: 'create_workflow', details: networkErr.message });
                }

                if (createResult.status !== 200 && createResult.status !== 201 && createResult.status !== 202) {
                    const errorMap = { 401: 'Ivanti API key is invalid or missing.', 419: 'API key lacks workflow creation permissions.', 429: 'Ivanti API rate limit reached.' };
                    let errorMsg = errorMap[createResult.status];
                    if (!errorMsg) {
                        // Try to extract detail from the Ivanti response body
                        let bodyDetail = '';
                        try {
                            const parsed = JSON.parse(createResult.body);
                            bodyDetail = parsed.message || parsed.error || parsed.detail || JSON.stringify(parsed);
                        } catch (_) {
                            bodyDetail = (createResult.body || '').slice(0, 500);
                        }
                        errorMsg = `Workflow creation failed (${createResult.status}): ${bodyDetail || 'No details returned by Ivanti API.'}`;
                    }
                    logAudit({ userId: req.user.id, username: req.user.username, action: 'ivanti_fp_workflow_failed', entityType: 'ivanti_workflow', details: { error: errorMsg, status: createResult.status, responseBody: (createResult.body || '').slice(0, 1000), findingIds }, ipAddress: req.ip });
                    return res.status(createResult.status === 429 ? 429 : 502).json({ success: false, error: errorMsg, step: 'create_workflow' });
                }

                let workflowBatchId;
                try { const createData = JSON.parse(createResult.body); workflowBatchId = createData.id; }
                catch (parseErr) { return res.status(502).json({ success: false, error: 'Failed to parse Ivanti API response.', step: 'create_workflow' }); }

                let workflowBatchUuid = null;
                try { workflowBatchUuid = await resolveWorkflowBatchUuid({ id: null, ivanti_workflow_batch_id: workflowBatchId, ivanti_workflow_batch_uuid: null, workflow_name: req.body.name }, apiKey, clientId, skipTls); } catch (e) {}

                const localAttachmentResults = files.map(f => ({ filename: f.originalname, success: true, source: 'local' }));
                const allAttachmentResults = [...localAttachmentResults, ...libraryAttachmentResults];
                const totalAttachmentCount = files.length + libraryDocIds.length;

                try {
                    await pool.query(
                        `INSERT INTO ivanti_fp_submissions (user_id, username, ivanti_workflow_batch_id, ivanti_workflow_batch_uuid, ivanti_generated_id, workflow_name, reason, description, expiration_date, scope_override, finding_ids_json, queue_item_ids_json, attachment_count, attachment_results_json, status, error_message)
                         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,
                        [req.user.id, req.user.username, workflowBatchId, workflowBatchUuid, null, req.body.name, req.body.reason, req.body.description || null, req.body.expirationDate, req.body.scopeOverride || 'Authorized', JSON.stringify(findingIds), JSON.stringify(queueItemIds), totalAttachmentCount, JSON.stringify(allAttachmentResults), 'success', null]
                    );
                } catch (dbErr) { console.error('Failed to insert submission record:', dbErr); }

                logAudit({ userId: req.user.id, username: req.user.username, action: 'ivanti_fp_workflow_created', entityType: 'ivanti_workflow', entityId: String(workflowBatchId), details: { workflowName: req.body.name, findingIds, attachmentCount: totalAttachmentCount, status: 'success' }, ipAddress: req.ip });

                let queueItemsUpdated = 0;
                try {
                    const qResult = await pool.query(`UPDATE ivanti_todo_queue SET status='complete', updated_at=NOW() WHERE id = ANY($1) AND user_id=$2`, [queueItemIds, req.user.id]);
                    queueItemsUpdated = qResult.rowCount;
                } catch (queueErr) { console.error('Failed to update queue items:', queueErr); }

                res.json({ success: true, workflowBatchId, queueItemsUpdated, status: 'success' });
            })().catch((unexpectedErr) => { console.error('Unexpected error in FP workflow submission:', unexpectedErr); res.status(500).json({ success: false, error: 'Internal server error.' }); });
        });
    });

    /**
     * GET /api/ivanti/fp-workflow/submissions
     *
     * Returns all FP workflow submissions belonging to the authenticated user,
     * enriched with Ivanti workflow state (rework notes, approval notes, current state).
     * Automatically syncs lifecycle_status from Ivanti currentState when it differs.
     * Includes submission history entries for each submission.
     *
     * @query None
     * @returns {Array<Object>} Array of submission objects
     *   - id {number}
     *   - user_id {number}
     *   - username {string}
     *   - ivanti_workflow_batch_id {number}
     *   - ivanti_workflow_batch_uuid {string|null}
     *   - workflow_name {string}
     *   - reason {string}
     *   - description {string|null}
     *   - expiration_date {string}
     *   - scope_override {string}
     *   - finding_ids_json {string}
     *   - queue_item_ids_json {string}
     *   - attachment_count {number}
     *   - lifecycle_status {string} submitted|approved|rejected|rework|resubmitted
     *   - dismissed_at {string|null}
     *   - requeued_at {string|null}
     *   - history {Array<Object>} Submission history entries
     *   - ivanti_rework_note {string|null} Enriched from Ivanti API
     *   - ivanti_approval_note {string|null} Enriched from Ivanti API
     *   - ivanti_current_state {string|null} Enriched from Ivanti API
     * @error 500 Internal server error
     */
    router.get('/submissions', requireAuth(), async (req, res) => {
        try {
            const { rows: submissions } = await pool.query(`SELECT * FROM ivanti_fp_submissions WHERE user_id = $1 ORDER BY created_at DESC`, [req.user.id]);

            if (submissions.length > 0) {
                const submissionIds = submissions.map(s => s.id);
                const { rows: historyRows } = await pool.query(`SELECT * FROM ivanti_fp_submission_history WHERE submission_id = ANY($1) ORDER BY created_at ASC`, [submissionIds]);
                const historyMap = {};
                for (const row of historyRows) { if (!historyMap[row.submission_id]) historyMap[row.submission_id] = []; historyMap[row.submission_id].push(row); }
                for (const sub of submissions) { sub.history = historyMap[sub.id] || []; }

                const apiKey = process.env.IVANTI_API_KEY;
                const clientId = process.env.IVANTI_CLIENT_ID || '1550';
                const skipTls = process.env.IVANTI_SKIP_TLS === 'true';
                if (apiKey) {
                    try {
                        for (const sub of submissions) {
                            if (!sub.workflow_name) continue;
                            const searchUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/search`;
                            const searchBody = { filters: [{ field: 'name', exclusive: false, operator: 'EXACT', value: sub.workflow_name }], projection: 'internal', sort: [{ field: 'created', direction: 'DESC' }], page: 0, size: 1 };
                            const result = await ivantiPost(searchUrl, searchBody, apiKey, skipTls);
                            if (result.status === 200) {
                                const data = JSON.parse(result.body);
                                const batches = data._embedded?.workflowBatches || data._embedded?.workflowBatch || [];
                                const batch = batches[0];
                                if (batch) { sub.ivanti_rework_note = batch.reworkNote || null; sub.ivanti_approval_note = batch.approvalNote || null; sub.ivanti_current_state_notes = batch.currentStateUserNotes || null; sub.ivanti_previous_state_notes = batch.previousStateUserNotes || null; sub.ivanti_current_state = batch.currentState || null; }
                            }
                        }
                    } catch (e) { console.error('Error enriching submissions with Ivanti notes:', e.message); }

                    // Sync lifecycle_status from Ivanti currentState when it differs
                    const STATE_MAP = { 'APPROVED': 'approved', 'REJECTED': 'rejected', 'REWORK': 'rework' };
                    for (const sub of submissions) {
                        if (!sub.ivanti_current_state) continue;
                        const mappedStatus = STATE_MAP[sub.ivanti_current_state.toUpperCase()];
                        if (mappedStatus && mappedStatus !== sub.lifecycle_status) {
                            try {
                                await pool.query(
                                    `UPDATE ivanti_fp_submissions SET lifecycle_status = $1, updated_at = NOW() WHERE id = $2`,
                                    [mappedStatus, sub.id]
                                );
                                sub.lifecycle_status = mappedStatus;
                            } catch (syncErr) { console.error(`Failed to sync lifecycle_status for submission ${sub.id}:`, syncErr.message); }
                        }
                    }
                }
            }
            res.json(submissions);
        } catch (err) { console.error('Error fetching FP submissions:', err); res.status(500).json({ error: 'Internal server error.' }); }
    });

    /**
     * PUT /api/ivanti/fp-workflow/submissions/:id
     *
     * Edits the workflow fields of an existing FP submission. Proxies the update
     * to the Ivanti API, then updates the local record. Transitions lifecycle_status
     * to "resubmitted" if currently rejected or rework.
     * Requires Admin or Standard_User group.
     *
     * @param {string} id — Submission ID (URL parameter)
     * @body {Object}
     *   - name {string} Workflow name (required, max 255 chars)
     *   - reason {string} Reason (required)
     *   - description {string} Optional (max 2000 chars)
     *   - expirationDate {string} Required, future date within 120 days
     *   - scopeOverride {string} Optional: Authorized, None, or Automated
     * @returns {Object}
     *   - success {boolean}
     *   - submission {Object} The updated submission record
     * @error 400 Validation errors or submission is finalized
     * @error 403 Submission belongs to another user
     * @error 404 Submission not found
     * @error 429 Ivanti API rate limit
     * @error 500 Ivanti API key not configured or local DB failure
     * @error 502 Ivanti API connection or response failure
     */
    router.put('/submissions/:id', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res) => {
        (async () => {
            const submissionId = req.params.id;

            // 1. Fetch submission and verify ownership
            const { rows: subRows } = await pool.query(
                `SELECT * FROM ivanti_fp_submissions WHERE id = $1`, [submissionId]
            );
            const submission = subRows[0];

            if (!submission) {
                return res.status(404).json({ error: 'Submission not found.' });
            }
            if (submission.user_id !== req.user.id) {
                return res.status(403).json({ error: 'You can only edit your own submissions.' });
            }

            // 2. Lifecycle guard
            if (submission.lifecycle_status === 'approved') {
                return res.status(400).json({ error: 'This submission is finalized and cannot be edited.' });
            }

            // 3. Validate body
            const validationErrors = validateFpWorkflowForm(req.body);
            if (Object.keys(validationErrors).length > 0) {
                return res.status(400).json({ success: false, errors: validationErrors });
            }

            // 4. Proxy to Ivanti
            const apiKey = process.env.IVANTI_API_KEY;
            const clientId = process.env.IVANTI_CLIENT_ID || '1550';
            const skipTls = process.env.IVANTI_SKIP_TLS === 'true';

            if (!apiKey) {
                return res.status(500).json({ success: false, error: 'Ivanti API key is not configured.' });
            }

            const scopeMap = { 'Authorized': 'AUTHORIZED', 'None': 'NONE', 'Automated': 'AUTOMATED' };
            const updateUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/falsePositive/update`;
            const updateBody = {
                id: submission.ivanti_workflow_batch_id,
                name: req.body.name,
                reason: req.body.reason,
                description: req.body.description || '',
                expirationDate: req.body.expirationDate,
                overrideControl: scopeMap[req.body.scopeOverride] || 'AUTHORIZED'
            };

            let ivantiResult;
            try {
                ivantiResult = await ivantiPost(updateUrl, updateBody, apiKey, skipTls);
            } catch (networkErr) {
                logAudit({
                    userId: req.user.id, username: req.user.username,
                    action: 'ivanti_fp_submission_edit_failed', entityType: 'ivanti_workflow',
                    details: { error: networkErr.message, submissionId },
                    ipAddress: req.ip
                });
                return res.status(502).json({ success: false, error: 'Failed to connect to Ivanti API.', details: networkErr.message });
            }

            if (ivantiResult.status !== 200 && ivantiResult.status !== 201 && ivantiResult.status !== 202) {
                const errorMap = {
                    401: 'Ivanti API key is invalid or missing. Contact your administrator.',
                    419: 'API key lacks permissions for this operation.',
                    429: 'Ivanti API rate limit reached. Please try again in a few minutes.'
                };
                const errorMsg = ivantiResult.status >= 500
                    ? 'Ivanti API is temporarily unavailable. Please try again later.'
                    : (errorMap[ivantiResult.status] || `Operation failed: ${ivantiResult.status}`);
                return res.status(ivantiResult.status === 429 ? 429 : 502).json({ success: false, error: errorMsg });
            }

            // 5. Determine new lifecycle_status
            let newLifecycleStatus = submission.lifecycle_status;
            if (submission.lifecycle_status === 'rejected' || submission.lifecycle_status === 'rework') {
                newLifecycleStatus = 'resubmitted';
            }

            // 6. Update local record
            try {
                await pool.query(
                    `UPDATE ivanti_fp_submissions
                     SET workflow_name = $1, reason = $2, description = $3, expiration_date = $4, scope_override = $5, lifecycle_status = $6, updated_at = NOW()
                     WHERE id = $7`,
                    [req.body.name, req.body.reason, req.body.description || null, req.body.expirationDate, req.body.scopeOverride || 'Authorized', newLifecycleStatus, submissionId]
                );
            } catch (dbErr) {
                console.error('Failed to update submission record:', dbErr);
                return res.status(500).json({ success: false, error: 'Failed to update local record.' });
            }

            // 7. Insert history row
            const historyEntry = buildSubmissionHistoryEntry('fields_updated', {
                changed: {
                    name: { from: submission.workflow_name, to: req.body.name },
                    reason: { from: submission.reason, to: req.body.reason },
                    description: { from: submission.description, to: req.body.description || '' },
                    expirationDate: { from: submission.expiration_date, to: req.body.expirationDate },
                    scopeOverride: { from: submission.scope_override, to: req.body.scopeOverride || 'Authorized' }
                }
            }, req.user.id, req.user.username);

            try {
                await pool.query(
                    `INSERT INTO ivanti_fp_submission_history (submission_id, user_id, username, change_type, change_details_json, created_at)
                     VALUES ($1, $2, $3, $4, $5, NOW())`,
                    [submissionId, historyEntry.user_id, historyEntry.username, historyEntry.change_type, historyEntry.change_details_json]
                );
            } catch (histErr) {
                console.error('Failed to insert history row:', histErr);
            }

            // 8. Audit log
            logAudit({
                userId: req.user.id, username: req.user.username,
                action: 'ivanti_fp_submission_edited', entityType: 'ivanti_workflow',
                entityId: String(submission.ivanti_workflow_batch_id),
                details: { submissionId, workflowName: req.body.name },
                ipAddress: req.ip
            });

            // 9. Return updated record
            const { rows: updatedRows } = await pool.query(
                `SELECT * FROM ivanti_fp_submissions WHERE id = $1`, [submissionId]
            );

            res.json({ success: true, submission: updatedRows[0] });
        })().catch((unexpectedErr) => {
            console.error('Unexpected error in PUT /submissions/:id:', unexpectedErr);
            res.status(500).json({ success: false, error: 'Internal server error.' });
        });
    });

    /**
     * POST /api/ivanti/fp-workflow/submissions/:id/findings
     *
     * Maps additional findings to an existing FP workflow in Ivanti. Resolves the
     * workflow batch UUID, then maps each finding individually. Successfully mapped
     * findings are merged into the submission's finding_ids_json and their queue items
     * are marked complete.
     * Requires Admin or Standard_User group.
     *
     * @param {string} id — Submission ID (URL parameter)
     * @body {Object}
     *   - findingIds {Array<number|string>} Finding IDs to map (at least one required)
     *   - queueItemIds {Array<number>} Corresponding queue item IDs (at least one required)
     * @returns {Object}
     *   - success {boolean}
     *   - addedFindings {Array} Successfully mapped finding IDs
     *   - failedFindings {Array} Finding IDs that failed to map
     *   - queueItemsUpdated {number} Count of queue items marked complete
     * @error 400 Invalid input, queue item validation, or UUID resolution failure
     * @error 403 Submission or queue items belong to another user
     * @error 404 Submission not found
     * @error 500 Ivanti API key not configured
     * @error 502 All findings failed to map
     */
    router.post('/submissions/:id/findings', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res) => {
        (async () => {
            const submissionId = req.params.id;
            const { findingIds, queueItemIds } = req.body;

            if (!Array.isArray(findingIds) || findingIds.length === 0) {
                return res.status(400).json({ error: 'At least one finding ID is required.' });
            }
            if (!Array.isArray(queueItemIds) || queueItemIds.length === 0) {
                return res.status(400).json({ error: 'At least one queue item ID is required.' });
            }

            // 1. Fetch submission and verify ownership
            const { rows: subRows } = await pool.query(
                `SELECT * FROM ivanti_fp_submissions WHERE id = $1`, [submissionId]
            );
            const submission = subRows[0];

            if (!submission) {
                return res.status(404).json({ error: 'Submission not found.' });
            }
            if (submission.user_id !== req.user.id) {
                return res.status(403).json({ error: 'You can only edit your own submissions.' });
            }

            // 2. Lifecycle guard
            if (submission.lifecycle_status === 'approved') {
                return res.status(400).json({ error: 'This submission is finalized and cannot be edited.' });
            }

            // 3. Verify queue items belong to user, are FP type, and pending
            const { rows: queueRows } = await pool.query(
                `SELECT id, workflow_type, status, user_id FROM ivanti_todo_queue WHERE id = ANY($1)`,
                [queueItemIds]
            );

            if (queueRows.length !== queueItemIds.length) {
                return res.status(400).json({ error: 'One or more queue items not found.' });
            }
            for (const row of queueRows) {
                if (row.user_id !== req.user.id) {
                    return res.status(403).json({ error: 'You can only submit your own queue items.' });
                }
                if (row.workflow_type !== 'FP') {
                    return res.status(400).json({ error: `Queue item ${row.id} is not an FP workflow type.` });
                }
                if (row.status !== 'pending') {
                    return res.status(400).json({ error: `Queue item ${row.id} is not in pending status.` });
                }
            }

            // 4. Proxy to Ivanti map endpoint
            const apiKey = process.env.IVANTI_API_KEY;
            const clientId = process.env.IVANTI_CLIENT_ID || '1550';
            const skipTls = process.env.IVANTI_SKIP_TLS === 'true';

            if (!apiKey) {
                return res.status(500).json({ success: false, error: 'Ivanti API key is not configured.' });
            }

            const mapUuid = await resolveWorkflowBatchUuid(submission, apiKey, clientId, skipTls);
            if (!mapUuid) {
                return res.status(400).json({ success: false, error: 'Could not resolve workflow batch UUID. The workflow may not exist in Ivanti.' });
            }

            const mapUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/falsePositive/${encodeURIComponent(mapUuid)}/map`;

            const mappedIds = [];
            const failedIds = [];
            for (const fid of findingIds) {
                const mapBody = {
                    subject: 'hostFinding',
                    filterRequest: {
                        filters: [{ field: 'id', exclusive: false, operator: 'EXACT', value: String(fid) }]
                    }
                };
                try {
                    const result = await ivantiPost(mapUrl, mapBody, apiKey, skipTls);
                    if (result.status === 200 || result.status === 201 || result.status === 202) {
                        mappedIds.push(fid);
                    } else {
                        failedIds.push({ id: fid, status: result.status });
                    }
                } catch (err) {
                    failedIds.push({ id: fid, error: err.message });
                }
            }

            if (mappedIds.length === 0) {
                return res.status(502).json({ success: false, error: 'Failed to map any findings to the workflow.' });
            }

            // 5. Merge only successfully mapped finding IDs
            const mergedJson = mergeFindings(submission.finding_ids_json, mappedIds);

            try {
                await pool.query(
                    `UPDATE ivanti_fp_submissions SET finding_ids_json = $1, updated_at = NOW() WHERE id = $2`,
                    [mergedJson, submissionId]
                );
            } catch (dbErr) {
                console.error('Failed to update finding_ids_json:', dbErr);
            }

            // 6. Mark only successfully mapped queue items as complete
            let queueItemsUpdated = 0;
            const mappedSet = new Set(mappedIds.map(String));
            const successQueueIds = queueItemIds.filter((qid, idx) => {
                const queueItem = queueRows.find(r => r.id === qid);
                return queueItem && mappedSet.has(String(findingIds[idx]));
            });
            if (successQueueIds.length > 0) {
                try {
                    const qResult = await pool.query(
                        `UPDATE ivanti_todo_queue SET status='complete', updated_at=NOW() WHERE id = ANY($1) AND user_id=$2`,
                        [successQueueIds, req.user.id]
                    );
                    queueItemsUpdated = qResult.rowCount;
                } catch (queueErr) {
                    console.error('Failed to update queue items:', queueErr);
                }
            }

            // 7. Insert history row
            const historyEntry = buildSubmissionHistoryEntry('findings_added', {
                addedFindingIds: mappedIds,
                failedFindingIds: failedIds.map(f => f.id || f),
                queueItemIds: successQueueIds
            }, req.user.id, req.user.username);

            try {
                await pool.query(
                    `INSERT INTO ivanti_fp_submission_history (submission_id, user_id, username, change_type, change_details_json, created_at)
                     VALUES ($1, $2, $3, $4, $5, NOW())`,
                    [submissionId, historyEntry.user_id, historyEntry.username, historyEntry.change_type, historyEntry.change_details_json]
                );
            } catch (histErr) {
                console.error('Failed to insert history row:', histErr);
            }

            // 8. Audit log
            logAudit({
                userId: req.user.id, username: req.user.username,
                action: 'ivanti_fp_findings_added', entityType: 'ivanti_workflow',
                entityId: String(submission.ivanti_workflow_batch_id),
                details: { submissionId, addedFindingIds: findingIds, queueItemsUpdated },
                ipAddress: req.ip
            });

            res.json({ success: true, addedFindings: mappedIds, failedFindings: failedIds, queueItemsUpdated });
        })().catch((unexpectedErr) => {
            console.error('Unexpected error in POST /submissions/:id/findings:', unexpectedErr);
            res.status(500).json({ success: false, error: 'Internal server error.' });
        });
    });

    /**
     * POST /api/ivanti/fp-workflow/submissions/:id/attachments
     *
     * Uploads additional attachments (local files and/or library documents) to an
     * existing FP workflow in Ivanti. Updates the local submission's attachment count
     * and results.
     * Requires Admin or Standard_User group.
     *
     * @param {string} id — Submission ID (URL parameter)
     * @body {Object} multipart/form-data
     *   - attachments {File[]} Local files to upload (max 10, each max 10 MB)
     *   - libraryDocIds {string} Optional JSON array of document library IDs
     * @returns {Object}
     *   - success {boolean}
     *   - attachmentResults {Array<Object>} Per-file upload results with filename, success, source
     *   - status {string} "success" if all uploaded, "partial" if some failed
     * @error 400 No files provided, invalid file type, or submission is finalized
     * @error 403 Submission belongs to another user
     * @error 404 Submission not found
     * @error 500 Ivanti API key not configured
     */
    router.post('/submissions/:id/attachments', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res) => {
        fpUpload(req, res, (multerErr) => {
            if (multerErr) {
                if (multerErr.code === 'LIMIT_FILE_SIZE') {
                    return res.status(400).json({ error: 'File exceeds the 10 MB size limit.' });
                }
                return res.status(400).json({ error: multerErr.message });
            }

            const files = req.files || [];

            let libraryDocIds;
            try { libraryDocIds = JSON.parse(req.body.libraryDocIds || '[]'); }
            catch (e) { return res.status(400).json({ error: 'libraryDocIds must be a valid JSON array.' }); }
            if (!Array.isArray(libraryDocIds)) return res.status(400).json({ error: 'libraryDocIds must be a valid JSON array.' });
            for (const id of libraryDocIds) { if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: `Invalid library document ID: ${id}.` }); }

            if (files.length === 0 && libraryDocIds.length === 0) {
                return res.status(400).json({ error: 'At least one file or library document is required.' });
            }

            for (const file of files) {
                if (!isAllowedFileExtension(file.originalname)) {
                    return res.status(400).json({ error: `File type not allowed: ${file.originalname}` });
                }
            }

            (async () => {
                const submissionId = req.params.id;

                const { rows: subRows } = await pool.query(
                    `SELECT * FROM ivanti_fp_submissions WHERE id = $1`, [submissionId]
                );
                const submission = subRows[0];

                if (!submission) return res.status(404).json({ error: 'Submission not found.' });
                if (submission.user_id !== req.user.id) return res.status(403).json({ error: 'You can only edit your own submissions.' });
                if (submission.lifecycle_status === 'approved') return res.status(400).json({ error: 'This submission is finalized and cannot be edited.' });

                // Look up library documents
                let libraryDocs = [];
                if (libraryDocIds.length > 0) {
                    const { rows: docRows } = await pool.query(
                        `SELECT id, name, file_path, file_size, mime_type FROM documents WHERE id = ANY($1)`, [libraryDocIds]
                    );
                    libraryDocs = docRows;
                    const foundIds = new Set(libraryDocs.map(d => d.id));
                    const missingIds = libraryDocIds.filter(id => !foundIds.has(id));
                    if (missingIds.length > 0) return res.status(400).json({ error: `Library document IDs not found: ${missingIds.join(', ')}` });
                    // Validate file types for library docs
                    const unsupportedDocs = libraryDocs.filter(d => !isAllowedFileExtension(d.name));
                    if (unsupportedDocs.length > 0) {
                        const names = unsupportedDocs.map(d => d.name).join(', ');
                        return res.status(400).json({ error: `Library document file type not supported by Ivanti: ${names}. Allowed: ${[...ALLOWED_EXTENSIONS].join(', ')}` });
                    }
                }

                const apiKey = process.env.IVANTI_API_KEY;
                const clientId = process.env.IVANTI_CLIENT_ID || '1550';
                const skipTls = process.env.IVANTI_SKIP_TLS === 'true';

                if (!apiKey) return res.status(500).json({ success: false, error: 'Ivanti API key is not configured.' });

                const attachUuid = await resolveWorkflowBatchUuid(submission, apiKey, clientId, skipTls);
                if (!attachUuid) return res.status(400).json({ success: false, error: 'Could not resolve workflow batch UUID.' });

                const attachUrl = `/client/${encodeURIComponent(clientId)}/workflowBatch/falsePositive/${encodeURIComponent(attachUuid)}/attach`;
                const attachmentResults = [];

                // Upload local files
                for (const f of files) {
                    try {
                        const formFiles = [{ name: 'file', buffer: f.buffer, filename: f.originalname, contentType: f.mimetype || 'application/octet-stream' }];
                        const result = await ivantiFormPost(attachUrl, [], formFiles, apiKey, skipTls);
                        const success = result.status === 200 || result.status === 201 || result.status === 202;
                        attachmentResults.push({ filename: f.originalname, success, source: 'local', ...(success ? {} : { error: `Upload failed: ${result.status}` }) });
                    } catch (uploadErr) {
                        attachmentResults.push({ filename: f.originalname, success: false, source: 'local', error: uploadErr.message });
                    }
                }

                // Upload library files
                for (const doc of libraryDocs) {
                    let buffer;
                    try { buffer = fs.readFileSync(doc.file_path); }
                    catch (readErr) { attachmentResults.push({ success: false, error: 'File not found on disk', source: 'library', documentId: doc.id, filename: doc.name }); continue; }
                    try {
                        const formFiles = [{ name: 'file', buffer, filename: doc.name, contentType: doc.mime_type || 'application/octet-stream' }];
                        const result = await ivantiFormPost(attachUrl, [], formFiles, apiKey, skipTls);
                        const success = result.status === 200 || result.status === 201 || result.status === 202;
                        attachmentResults.push({ filename: doc.name, success, source: 'library', documentId: doc.id, ...(success ? {} : { error: `Upload failed: ${result.status}` }) });
                    } catch (uploadErr) {
                        attachmentResults.push({ filename: doc.name, success: false, source: 'library', documentId: doc.id, error: uploadErr.message });
                    }
                }

                // Update attachment_count and attachment_results_json
                const existingResults = JSON.parse(submission.attachment_results_json || '[]');
                const allResults = [...existingResults, ...attachmentResults];
                const successCount = attachmentResults.filter(r => r.success).length;
                const newAttachmentCount = (submission.attachment_count || 0) + successCount;

                try {
                    await pool.query(
                        `UPDATE ivanti_fp_submissions SET attachment_count = $1, attachment_results_json = $2, updated_at = NOW() WHERE id = $3`,
                        [newAttachmentCount, JSON.stringify(allResults), submissionId]
                    );
                } catch (dbErr) { console.error('Failed to update attachment records:', dbErr); }

                // Insert history row
                const historyEntry = buildSubmissionHistoryEntry('attachments_added', { files: attachmentResults }, req.user.id, req.user.username);
                try {
                    await pool.query(
                        `INSERT INTO ivanti_fp_submission_history (submission_id, user_id, username, change_type, change_details_json, created_at)
                         VALUES ($1, $2, $3, $4, $5, NOW())`,
                        [submissionId, historyEntry.user_id, historyEntry.username, historyEntry.change_type, historyEntry.change_details_json]
                    );
                } catch (histErr) { console.error('Failed to insert history row:', histErr); }

                logAudit({
                    userId: req.user.id, username: req.user.username,
                    action: 'ivanti_fp_attachments_added', entityType: 'ivanti_workflow',
                    entityId: String(submission.ivanti_workflow_batch_id),
                    details: { submissionId, attachmentResults, libraryDocCount: libraryDocIds.length },
                    ipAddress: req.ip
                });

                const allSucceeded = attachmentResults.every(r => r.success);
                res.json({ success: true, attachmentResults, status: allSucceeded ? 'success' : 'partial' });
            })().catch((unexpectedErr) => {
                console.error('Unexpected error in POST /submissions/:id/attachments:', unexpectedErr);
                res.status(500).json({ success: false, error: 'Internal server error.' });
            });
        });
    });

    /**
     * PATCH /api/ivanti/fp-workflow/submissions/:id/dismiss
     *
     * Dismisses a rejected FP submission by setting dismissed_at timestamp.
     * Only rejected submissions can be dismissed.
     * Requires Admin or Standard_User group.
     *
     * @param {string} id — Submission ID (URL parameter)
     * @body None
     * @returns {Object} { success: true }
     * @error 400 Submission is not in rejected status
     * @error 403 Submission belongs to another user
     * @error 404 Submission not found
     * @error 500 Internal server error
     */
    router.patch('/submissions/:id/dismiss', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res) => {
        (async () => {
            const submissionId = req.params.id;

            const { rows: subRows } = await pool.query(
                `SELECT * FROM ivanti_fp_submissions WHERE id = $1`, [submissionId]
            );
            const submission = subRows[0];

            if (!submission) return res.status(404).json({ error: 'Submission not found.' });
            if (submission.user_id !== req.user.id) return res.status(403).json({ error: 'You can only dismiss your own submissions.' });
            if (submission.lifecycle_status !== 'rejected') return res.status(400).json({ error: 'Only rejected submissions can be dismissed.' });

            try {
                await pool.query(
                    `UPDATE ivanti_fp_submissions SET dismissed_at = NOW() WHERE id = $1`,
                    [submissionId]
                );
            } catch (dbErr) {
                console.error('Failed to set dismissed_at:', dbErr);
                return res.status(500).json({ error: 'Internal server error.' });
            }

            logAudit({
                userId: req.user.id, username: req.user.username,
                action: 'ivanti_fp_submission_dismissed', entityType: 'ivanti_workflow',
                entityId: String(submission.ivanti_workflow_batch_id),
                details: { submissionId },
                ipAddress: req.ip
            });

            res.json({ success: true });
        })().catch((unexpectedErr) => {
            console.error('Unexpected error in PATCH /submissions/:id/dismiss:', unexpectedErr);
            res.status(500).json({ error: 'Internal server error.' });
        });
    });

    /**
     * POST /api/ivanti/fp-workflow/submissions/:id/requeue
     *
     * Re-queues findings from a rejected FP submission into the todo queue
     * under a specified target workflow type. Creates new pending queue items
     * for each finding referenced in the submission's queue_item_ids_json.
     * Requires Admin or Standard_User group.
     *
     * @param {string} id — Submission ID (URL parameter)
     * @body {Object}
     *   - workflow_type {string} Required. One of: FP, Archer, CARD, GRANITE, DECOM
     *   - vendor {string} Required for FP, Archer, and DECOM workflows; max 200 chars
     * @returns {Object}
     *   - success {boolean}
     *   - items {Array<Object>} Newly created queue items with parsed cves
     *   - count {number} Number of items created
     * @error 400 Invalid input, submission not rejected, or already re-queued
     * @error 403 Submission belongs to another user
     * @error 404 Submission not found
     * @error 500 Internal server error
     */
    router.post('/submissions/:id/requeue', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res) => {
        (async () => {
            const submissionId = req.params.id;
            const { workflow_type, vendor } = req.body;

            // Validate workflow_type
            const VALID_REQUEUE_TYPES = ['FP', 'Archer', 'CARD', 'GRANITE', 'DECOM'];
            const INVENTORY_TYPES = ['CARD', 'GRANITE'];
            if (!VALID_REQUEUE_TYPES.includes(workflow_type)) {
                return res.status(400).json({ error: 'workflow_type must be FP, Archer, CARD, GRANITE, or DECOM.' });
            }

            // Validate vendor for FP/Archer/DECOM
            if (!INVENTORY_TYPES.includes(workflow_type)) {
                if (!vendor || typeof vendor !== 'string' || vendor.trim().length === 0) {
                    return res.status(400).json({ error: 'vendor is required for FP, Archer, and DECOM workflows.' });
                }
                if (vendor.trim().length > 200) {
                    return res.status(400).json({ error: 'vendor must be 200 characters or fewer.' });
                }
            }

            const vendorVal = INVENTORY_TYPES.includes(workflow_type) ? '' : vendor.trim();

            // Fetch submission
            const { rows: subRows } = await pool.query(
                `SELECT * FROM ivanti_fp_submissions WHERE id = $1`, [submissionId]
            );
            const submission = subRows[0];

            if (!submission) return res.status(404).json({ error: 'Submission not found.' });
            if (submission.user_id !== req.user.id) return res.status(403).json({ error: 'You can only re-queue your own submissions.' });
            if (submission.lifecycle_status !== 'rejected') return res.status(400).json({ error: 'Only rejected submissions can be re-queued.' });
            if (submission.requeued_at) return res.status(400).json({ error: 'Findings from this submission have already been re-queued.' });

            // Parse original queue item IDs
            let queueItemIds = [];
            try {
                queueItemIds = JSON.parse(submission.queue_item_ids_json || '[]');
            } catch (e) { /* ignore parse errors */ }

            // Parse finding IDs (always available, even for submissions created outside dashboard)
            let findingIds = [];
            try {
                findingIds = JSON.parse(submission.finding_ids_json || '[]');
            } catch (e) { /* ignore */ }

            if ((!Array.isArray(queueItemIds) || queueItemIds.length === 0) &&
                (!Array.isArray(findingIds) || findingIds.length === 0)) {
                return res.status(400).json({ error: 'No findings associated with this submission.' });
            }

            // Fetch original queue items to get finding data (if they still exist)
            let findingsToQueue = [];
            if (queueItemIds.length > 0) {
                const { rows: originalItems } = await pool.query(
                    `SELECT finding_id, finding_title, cves_json, ip_address, hostname FROM ivanti_todo_queue WHERE id = ANY($1)`,
                    [queueItemIds]
                );
                findingsToQueue = originalItems;
            }

            // Fallback: if original queue items were deleted or never existed,
            // use finding_ids_json to look up finding data from ivanti_findings
            if (findingsToQueue.length === 0 && findingIds.length > 0) {
                const { rows: findings } = await pool.query(
                    `SELECT id AS finding_id, title AS finding_title, cves, ip_address, host_name AS hostname FROM ivanti_findings WHERE id = ANY($1)`,
                    [findingIds.map(String)]
                );
                findingsToQueue = findings.map(f => ({
                    ...f,
                    cves_json: Array.isArray(f.cves) ? JSON.stringify(f.cves) : null,
                }));

                // Last resort: create items with just the finding IDs (minimal data)
                if (findingsToQueue.length === 0) {
                    findingsToQueue = findingIds.map(id => ({
                        finding_id: String(id),
                        finding_title: null,
                        cves_json: null,
                        ip_address: null,
                        hostname: null,
                    }));
                }
            }

            // INSERT new pending queue items for each finding
            const newItems = [];
            for (const item of findingsToQueue) {
                const { rows: inserted } = await pool.query(
                    `INSERT INTO ivanti_todo_queue
                         (user_id, finding_id, finding_title, cves_json, ip_address, hostname, vendor, workflow_type)
                     VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
                     RETURNING *`,
                    [req.user.id, item.finding_id, item.finding_title, item.cves_json, item.ip_address, item.hostname, vendorVal, workflow_type]
                );
                newItems.push(inserted[0]);
            }

            // UPDATE submission to mark as requeued
            await pool.query(
                `UPDATE ivanti_fp_submissions SET requeued_at = NOW() WHERE id = $1`,
                [submissionId]
            );

            // Audit log (fire-and-forget)
            const auditFindingIds = findingsToQueue.map(i => i.finding_id).filter(Boolean);
            logAudit({
                userId: req.user.id, username: req.user.username,
                action: 'fp_submission_requeued', entityType: 'ivanti_fp_submissions',
                entityId: String(submission.id),
                details: { target_workflow_type: workflow_type, items_created: newItems.length, finding_ids: auditFindingIds },
                ipAddress: req.ip
            });

            // Return items with parsed cves
            const itemsWithCves = newItems.map(i => ({
                ...i,
                cves: i.cves_json ? JSON.parse(i.cves_json) : []
            }));

            res.status(201).json({ success: true, items: itemsWithCves, count: newItems.length });
        })().catch((unexpectedErr) => {
            console.error('Unexpected error in POST /submissions/:id/requeue:', unexpectedErr);
            res.status(500).json({ success: false, error: 'Internal server error.' });
        });
    });

    /**
     * PATCH /api/ivanti/fp-workflow/submissions/:id/status
     *
     * Manually updates the lifecycle status of an FP submission.
     * Validates the transition is allowed (e.g., approved submissions cannot be changed).
     * Requires Admin or Standard_User group.
     *
     * @param {string} id — Submission ID (URL parameter)
     * @body {Object}
     *   - lifecycle_status {string} New status. One of: submitted, approved, rejected, rework, resubmitted
     * @returns {Object}
     *   - success {boolean}
     *   - previousStatus {string}
     *   - newStatus {string}
     * @error 400 Invalid transition or invalid status value
     * @error 403 Submission belongs to another user
     * @error 404 Submission not found
     * @error 500 Internal server error
     */
    router.patch('/submissions/:id/status', requireAuth(), requireGroup('Admin', 'Standard_User'), (req, res) => {
        (async () => {
            const submissionId = req.params.id;
            const newStatus = req.body.lifecycle_status;

            const { rows: subRows } = await pool.query(
                `SELECT * FROM ivanti_fp_submissions WHERE id = $1`, [submissionId]
            );
            const submission = subRows[0];

            if (!submission) return res.status(404).json({ error: 'Submission not found.' });
            if (submission.user_id !== req.user.id) return res.status(403).json({ error: 'You can only edit your own submissions.' });

            const transition = validateLifecycleTransition(submission.lifecycle_status, newStatus);
            if (!transition.valid) return res.status(400).json({ error: transition.error });

            const previousStatus = submission.lifecycle_status;

            try {
                await pool.query(
                    `UPDATE ivanti_fp_submissions SET lifecycle_status = $1, updated_at = NOW() WHERE id = $2`,
                    [newStatus, submissionId]
                );
            } catch (dbErr) {
                console.error('Failed to update lifecycle status:', dbErr);
                return res.status(500).json({ success: false, error: 'Failed to update status.' });
            }

            const historyEntry = buildSubmissionHistoryEntry('status_changed', { from: previousStatus, to: newStatus }, req.user.id, req.user.username);
            try {
                await pool.query(
                    `INSERT INTO ivanti_fp_submission_history (submission_id, user_id, username, change_type, change_details_json, created_at)
                     VALUES ($1, $2, $3, $4, $5, NOW())`,
                    [submissionId, historyEntry.user_id, historyEntry.username, historyEntry.change_type, historyEntry.change_details_json]
                );
            } catch (histErr) { console.error('Failed to insert history row:', histErr); }

            logAudit({
                userId: req.user.id, username: req.user.username,
                action: 'ivanti_fp_status_changed', entityType: 'ivanti_workflow',
                entityId: String(submission.ivanti_workflow_batch_id),
                details: { submissionId, from: previousStatus, to: newStatus },
                ipAddress: req.ip
            });

            res.json({ success: true, previousStatus, newStatus });
        })().catch((unexpectedErr) => {
            console.error('Unexpected error in PATCH /submissions/:id/status:', unexpectedErr);
            res.status(500).json({ success: false, error: 'Internal server error.' });
        });
    });

    return router;
}

module.exports = createIvantiFpWorkflowRouter;
module.exports.validateFpWorkflowForm = validateFpWorkflowForm;
module.exports.buildIvantiFormFields = buildIvantiFormFields;
module.exports.buildSubjectFilterRequest = buildSubjectFilterRequest;
module.exports.isAllowedFileExtension = isAllowedFileExtension;
module.exports.validateLifecycleTransition = validateLifecycleTransition;
module.exports.mergeFindings = mergeFindings;
module.exports.buildSubmissionHistoryEntry = buildSubmissionHistoryEntry;
module.exports.filterVisibleSubmissions = filterVisibleSubmissions;
module.exports.shouldShowDismissButton = shouldShowDismissButton;
