#!/usr/bin/env node
// bu-reassignment-check.js — Check if disappeared findings were reassigned to a different BU
//
// Queries Ivanti for the specific finding IDs that are completely gone from our
// BU-filtered results, using NO filters at all (just the finding IDs).
// If they come back with a different BU, that confirms BU reassignment.
//
// Usage: node backend/scripts/bu-reassignment-check.js

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');

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

async function queryByFindingIds(findingIds, apiKey, clientId, skipTls) {
    const urlPath = `/client/${encodeURIComponent(clientId)}/hostFinding/search`;
    const allResults = [];

    // Ivanti's IN filter can handle batches — but let's chunk to be safe
    const chunkSize = 50;
    for (let i = 0; i < findingIds.length; i += chunkSize) {
        const chunk = findingIds.slice(i, i + chunkSize);
        const idList = chunk.join(',');

        // Query with ONLY the finding ID filter — no BU, no severity, no state
        const filters = [
            {
                field: 'id',
                exclusive: false,
                operator: 'IN',
                orWithPrevious: false,
                implicitFilters: [],
                value: idList,
                caseSensitive: false
            }
        ];

        let page = 0;
        let totalPages = 1;

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

            try {
                const result = await ivantiPost(urlPath, body, apiKey, skipTls);
                if (result.status !== 200) {
                    console.error(`  API returned status ${result.status} for chunk starting at ${i}`);
                    break;
                }

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

                for (const f of findings) {
                    const bu = f.assetCustomAttributes?.['1550_host_1']?.[0] || 'UNKNOWN';
                    allResults.push({
                        id: String(f.id),
                        severity: typeof f.severity === 'number' ? f.severity : parseFloat(f.severity) || 0,
                        title: f.title || '',
                        hostName: f.host?.hostName || '',
                        ipAddress: f.host?.ipAddress || '',
                        state: f.status || f.generic_state || '',
                        bu,
                        // Check for FP workflow
                        fpWorkflow: extractFP(f)
                    });
                }

                console.error(`  Chunk ${Math.floor(i/chunkSize)+1}: page ${page+1}/${totalPages}, ${findings.length} results`);
                page++;
            } catch (err) {
                console.error(`  Error querying chunk at ${i}:`, err.message);
                break;
            }
        } while (page < totalPages);
    }

    return allResults;
}

function extractFP(f) {
    const wfDist = f.workflowDistribution || {};
    const fpBuckets = [
        ...(wfDist.approvedWorkflows   || []),
        ...(wfDist.actionableWorkflows || []),
        ...(wfDist.requestedWorkflows  || []),
        ...(wfDist.reworkedWorkflows   || []),
        ...(wfDist.rejectedWorkflows   || []),
        ...(wfDist.expiredWorkflows    || []),
    ].filter(w => (w.generatedId || '').startsWith('FP#'));
    const entry = fpBuckets[0];
    if (!entry) return null;
    return { id: entry.generatedId, state: entry.state };
}

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');
        process.exit(1);
    }

    const db = new sqlite3.Database(DB_PATH);

    // Get the 124 finding IDs that were completely gone from BU-filtered results
    const goneFindings = await dbAll(db,
        `SELECT finding_id, last_severity, finding_title, current_state
         FROM ivanti_finding_archives
         WHERE current_state IN ('ARCHIVED', 'CLOSED')`
    );

    const goneIds = goneFindings.map(f => f.finding_id);
    console.error(`\n=== BU Reassignment Check ===`);
    console.error(`Querying Ivanti for ${goneIds.length} disappeared finding IDs (no BU/severity/state filter)...\n`);

    const results = await queryByFindingIds(goneIds, apiKey, clientId, skipTls);

    const foundMap = new Map(results.map(r => [r.id, r]));

    // Categorize
    const reassigned = [];    // Found with different BU
    const sameBU = [];        // Found with same BU (STEAM or ACCESS-ENG)
    const notFound = [];      // Still not found even without filters
    const withFP = [];        // Has an FP workflow (any state)

    for (const arch of goneFindings) {
        const found = foundMap.get(arch.finding_id);
        if (!found) {
            notFound.push(arch);
        } else if (found.bu !== 'NTS-AEO-ACCESS-ENG' && found.bu !== 'NTS-AEO-STEAM') {
            reassigned.push({ ...arch, currentBU: found.bu, currentSeverity: found.severity, currentState: found.state, fp: found.fpWorkflow });
            if (found.fpWorkflow) withFP.push({ ...arch, ...found });
        } else {
            sameBU.push({ ...arch, currentBU: found.bu, currentSeverity: found.severity, currentState: found.state, fp: found.fpWorkflow });
            if (found.fpWorkflow) withFP.push({ ...arch, ...found });
        }
    }

    console.log('');
    console.log('='.repeat(130));
    console.log('BU REASSIGNMENT CHECK RESULTS');
    console.log('='.repeat(130));

    console.log(`\nREASSIGNED TO DIFFERENT BU: ${reassigned.length} findings`);
    console.log('-'.repeat(130));
    if (reassigned.length > 0) {
        console.log(
            'Finding ID'.padEnd(14) +
            'Archive State'.padEnd(15) +
            'Last Sev'.padEnd(10) +
            'Current Sev'.padEnd(13) +
            'Current BU'.padEnd(30) +
            'FP Workflow'.padEnd(25) +
            'Title'
        );
        console.log('-'.repeat(130));
        for (const f of reassigned) {
            const fpStr = f.fp ? `${f.fp.id} (${f.fp.state})` : '-';
            console.log(
                f.finding_id.padEnd(14) +
                f.current_state.padEnd(15) +
                f.last_severity.toFixed(2).padEnd(10) +
                f.currentSeverity.toFixed(2).padEnd(13) +
                f.currentBU.padEnd(30) +
                fpStr.padEnd(25) +
                f.finding_title.substring(0, 40)
            );
        }
    }

    console.log(`\nSTILL SAME BU (but missing from filtered results): ${sameBU.length} findings`);
    console.log('-'.repeat(130));
    if (sameBU.length > 0) {
        console.log(
            'Finding ID'.padEnd(14) +
            'Archive State'.padEnd(15) +
            'Last Sev'.padEnd(10) +
            'Current Sev'.padEnd(13) +
            'Current BU'.padEnd(30) +
            'Current State'.padEnd(15) +
            'Title'
        );
        console.log('-'.repeat(130));
        for (const f of sameBU) {
            console.log(
                f.finding_id.padEnd(14) +
                f.current_state.padEnd(15) +
                f.last_severity.toFixed(2).padEnd(10) +
                f.currentSeverity.toFixed(2).padEnd(13) +
                f.currentBU.padEnd(30) +
                f.currentState.padEnd(15) +
                f.finding_title.substring(0, 40)
            );
        }
    }

    console.log(`\nCOMPLETELY GONE (not found even without any filters): ${notFound.length} findings`);
    if (notFound.length > 0 && notFound.length <= 20) {
        console.log('-'.repeat(130));
        for (const f of notFound) {
            console.log(`  ${f.finding_id}  ${f.last_severity.toFixed(2)}  ${f.finding_title.substring(0, 60)}`);
        }
    }

    if (withFP.length > 0) {
        console.log(`\nFINDINGS WITH FP WORKFLOWS: ${withFP.length}`);
        console.log('-'.repeat(130));
        for (const f of withFP) {
            const fpStr = f.fpWorkflow ? `${f.fpWorkflow.id} (${f.fpWorkflow.state})` : f.fp ? `${f.fp.id} (${f.fp.state})` : '-';
            console.log(`  ${f.finding_id || f.id}  ${fpStr}  ${f.bu || f.currentBU}  ${(f.finding_title || f.title || '').substring(0, 50)}`);
        }
    }

    // Summary
    console.log('');
    console.log('='.repeat(130));
    console.log('SUMMARY');
    console.log('='.repeat(130));
    console.log(`  Total disappeared findings checked:  ${goneFindings.length}`);
    console.log(`  Reassigned to different BU:          ${reassigned.length}`);
    console.log(`  Still same BU (unexpected):          ${sameBU.length}`);
    console.log(`  Completely gone from platform:        ${notFound.length}`);
    console.log(`  Have FP workflows:                   ${withFP.length}`);

    if (reassigned.length > 0) {
        const buCounts = {};
        reassigned.forEach(f => { buCounts[f.currentBU] = (buCounts[f.currentBU] || 0) + 1; });
        console.log('\n  BU reassignment breakdown:');
        for (const [bu, cnt] of Object.entries(buCounts).sort((a, b) => b[1] - a[1])) {
            console.log(`    ${bu}: ${cnt} findings`);
        }
    }

    if (reassigned.length > goneFindings.length * 0.5) {
        console.log('\n  VERDICT: BU REASSIGNMENT CONFIRMED.');
    } else if (notFound.length > goneFindings.length * 0.5) {
        console.log('\n  VERDICT: Findings removed from platform entirely (decommission or data purge).');
    } else {
        console.log('\n  VERDICT: Mixed causes — review individual categories above.');
    }

    db.close();
}

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