#!/usr/bin/env node
// drift-check.js — One-time diagnostic to confirm host-level VRR score drift
//
// Queries Ivanti WITHOUT the severity filter for the same BUs, then cross-
// references the results against our archived finding IDs to see if they
// still exist at lower severity scores.
//
// Usage: node backend/scripts/drift-check.js
//
// Output: prints a comparison table and summary. Does NOT modify cve_database.db
// permanently — uses a temporary in-memory table for the comparison.

require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') });

const path    = require('path');
const sqlite3 = require('sqlite3').verbose();
const { ivantiPost } = require('../helpers/ivantiApi');

const DB_PATH = path.join(__dirname, '..', 'cve_database.db');

// Same BU filter, NO severity filter, NO state filter — get everything
const ALL_FINDINGS_FILTERS = [
    {
        field: 'assetCustomAttributes.1550_host_1.value',
        exclusive: false,
        operator: 'IN',
        orWithPrevious: false,
        implicitFilters: [],
        value: 'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM',
        caseSensitive: false
    }
];

function dbAll(db, sql, params = []) {
    return new Promise((resolve, reject) => {
        db.all(sql, params, (err, rows) => {
            if (err) reject(err);
            else resolve(rows || []);
        });
    });
}

function dbRun(db, sql, params = []) {
    return new Promise((resolve, reject) => {
        db.run(sql, params, function (err) {
            if (err) reject(err);
            else resolve(this);
        });
    });
}

async function fetchAllFindings(apiKey, clientId, skipTls, state) {
    const urlPath = `/client/${encodeURIComponent(clientId)}/hostFinding/search`;
    const filters = [
        ...ALL_FINDINGS_FILTERS,
        {
            field: 'generic_state',
            exclusive: false,
            operator: 'EXACT',
            orWithPrevious: false,
            implicitFilters: [],
            value: state,
            caseSensitive: false
        }
    ];

    let allFindings = [];
    let page = 0;
    let totalPages = 1;

    do {
        const body = {
            filters,
            projection: 'internal',
            sort: [{ field: 'severity', direction: 'ASC' }],
            page,
            size: 100
        };

        const result = await ivantiPost(urlPath, body, apiKey, skipTls);
        if (result.status !== 200) {
            console.error(`  API returned status ${result.status} on page ${page}`);
            break;
        }

        const data = JSON.parse(result.body);
        totalPages = data.page?.totalPages || 1;
        const findings = data._embedded?.hostFindings || [];

        for (const f of findings) {
            allFindings.push({
                id: String(f.id),
                severity: typeof f.severity === 'number' ? f.severity : parseFloat(f.severity) || 0,
                title: f.title || '',
                hostName: f.host?.hostName || '',
                state
            });
        }

        console.error(`  ${state} page ${page + 1}/${totalPages} — ${allFindings.length} findings so far`);
        page++;
    } while (page < totalPages);

    return allFindings;
}

async function main() {
    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) {
        console.error('IVANTI_API_KEY not set in backend/.env');
        process.exit(1);
    }

    console.error('=== Drift Check: Querying Ivanti WITHOUT severity filter ===\n');

    // Fetch all Open findings (no severity filter)
    console.error('Fetching ALL Open findings (no severity filter)...');
    const openFindings = await fetchAllFindings(apiKey, clientId, skipTls, 'Open');
    console.error(`  Total Open (all severities): ${openFindings.length}\n`);

    // Fetch all Closed findings (no severity filter)
    console.error('Fetching ALL Closed findings (no severity filter)...');
    const closedFindings = await fetchAllFindings(apiKey, clientId, skipTls, 'Closed');
    console.error(`  Total Closed (all severities): ${closedFindings.length}\n`);

    const allFindings = [...openFindings, ...closedFindings];
    const findingMap = new Map(allFindings.map(f => [f.id, f]));

    console.error(`Total findings across both states: ${allFindings.length}\n`);

    // Open the database and get archived finding IDs
    const db = new sqlite3.Database(DB_PATH);

    const archived = await dbAll(db,
        `SELECT finding_id, last_severity, finding_title, host_name, current_state
         FROM ivanti_finding_archives
         WHERE current_state IN ('ARCHIVED', 'CLOSED')
         ORDER BY current_state, last_severity DESC`
    );

    console.log('');
    console.log('='.repeat(120));
    console.log('DRIFT CHECK RESULTS');
    console.log('='.repeat(120));
    console.log('');

    // Categorize results
    const drifted = [];       // Found in API at lower severity (below 8.5)
    const stillHigh = [];     // Found in API, severity still >= 8.5
    const gone = [];          // Not found in API at all (any severity)
    const stateChanged = [];  // Found but in different state

    for (const arch of archived) {
        const current = findingMap.get(arch.finding_id);
        if (!current) {
            gone.push(arch);
        } else if (current.severity < 8.5) {
            drifted.push({ ...arch, currentSeverity: current.severity, currentState: current.state });
        } else {
            stillHigh.push({ ...arch, currentSeverity: current.severity, currentState: current.state });
        }
    }

    // Print drifted findings
    console.log(`CONFIRMED SCORE DRIFT (now below 8.5): ${drifted.length} findings`);
    console.log('-'.repeat(120));
    if (drifted.length > 0) {
        console.log(
            'Finding ID'.padEnd(14) +
            'Archive State'.padEnd(15) +
            'Last Severity'.padEnd(15) +
            'Current Severity'.padEnd(18) +
            'Delta'.padEnd(10) +
            'Current State'.padEnd(15) +
            'Title'
        );
        console.log('-'.repeat(120));
        for (const f of drifted) {
            const delta = (f.currentSeverity - f.last_severity).toFixed(2);
            console.log(
                f.finding_id.padEnd(14) +
                f.current_state.padEnd(15) +
                f.last_severity.toFixed(2).padEnd(15) +
                f.currentSeverity.toFixed(2).padEnd(18) +
                delta.padEnd(10) +
                f.currentState.padEnd(15) +
                f.finding_title.substring(0, 50)
            );
        }
    }

    console.log('');
    console.log(`STILL HIGH SEVERITY (>= 8.5, should be in filtered results): ${stillHigh.length} findings`);
    console.log('-'.repeat(120));
    if (stillHigh.length > 0) {
        console.log(
            'Finding ID'.padEnd(14) +
            'Archive State'.padEnd(15) +
            'Last Severity'.padEnd(15) +
            'Current Severity'.padEnd(18) +
            'Current State'.padEnd(15) +
            'Title'
        );
        console.log('-'.repeat(120));
        for (const f of stillHigh) {
            console.log(
                f.finding_id.padEnd(14) +
                f.current_state.padEnd(15) +
                f.last_severity.toFixed(2).padEnd(15) +
                f.currentSeverity.toFixed(2).padEnd(18) +
                f.currentState.padEnd(15) +
                f.finding_title.substring(0, 50)
            );
        }
    }

    console.log('');
    console.log(`COMPLETELY GONE (not in API at any severity): ${gone.length} findings`);
    console.log('-'.repeat(120));
    if (gone.length > 0) {
        console.log(
            'Finding ID'.padEnd(14) +
            'Archive State'.padEnd(15) +
            'Last Severity'.padEnd(15) +
            'Title'
        );
        console.log('-'.repeat(120));
        for (const f of gone) {
            console.log(
                f.finding_id.padEnd(14) +
                f.current_state.padEnd(15) +
                f.last_severity.toFixed(2).padEnd(15) +
                f.finding_title.substring(0, 50)
            );
        }
    }

    // Summary
    console.log('');
    console.log('='.repeat(120));
    console.log('SUMMARY');
    console.log('='.repeat(120));
    console.log(`  Archived/Closed findings checked:  ${archived.length}`);
    console.log(`  Confirmed score drift (< 8.5):     ${drifted.length}`);
    console.log(`  Still high severity (>= 8.5):      ${stillHigh.length}`);
    console.log(`  Completely gone from API:           ${gone.length}`);
    console.log('');

    if (drifted.length > 0) {
        const avgDelta = drifted.reduce((sum, f) => sum + (f.currentSeverity - f.last_severity), 0) / drifted.length;
        const minNew = Math.min(...drifted.map(f => f.currentSeverity));
        const maxNew = Math.max(...drifted.map(f => f.currentSeverity));
        console.log(`  Score drift range: ${minNew.toFixed(2)} – ${maxNew.toFixed(2)} (avg delta: ${avgDelta.toFixed(2)})`);
    }

    if (drifted.length > archived.length * 0.5) {
        console.log('\n  VERDICT: Host-level VRR score drift CONFIRMED.');
        console.log('  The majority of disappeared findings still exist in Ivanti but at lower severity scores.');
    } else if (drifted.length > 0) {
        console.log('\n  VERDICT: Partial score drift detected. Some findings drifted, others may have been removed.');
    } else if (gone.length > archived.length * 0.5) {
        console.log('\n  VERDICT: Score drift NOT confirmed. Most findings are completely gone from the API.');
        console.log('  This suggests BU reassignment, host decommission, or a platform-side data issue.');
    }

    db.close();
}

main().catch(err => {
    console.error('Fatal error:', err);
    process.exit(1);
});
