/**
 * Pull full Hoffman API inventory, diff against local DB, write three files:
 *   - _hoffman_only_sns.txt   (SNs on Hoffman not in local DB)
 *   - _local_only_sns.txt     (SNs in local DB not on Hoffman)
 *   - _pull_inventory.log     (progress and summary)
 *
 * Writes directly via fs.appendFileSync so progress survives SSH disconnects.
 * Run detached; tail the log file for progress.
 */
const fs = require('fs');
const https = require('https');
const { URL } = require('url');
const db = require('./db');

const LOG = 'C:/Shares/testdatadb/database/_pull_inventory.log';
const OUT_HOFFMAN = 'C:/Shares/testdatadb/database/_hoffman_only_sns.txt';
const OUT_LOCAL = 'C:/Shares/testdatadb/database/_local_only_sns.txt';
const CREDS_PATH = 'C:/ProgramData/dataforth-uploader/credentials.json';
const PAGE_SIZE = 1000;

function log(msg) {
    const line = `[${new Date().toISOString()}] ${msg}\n`;
    fs.appendFileSync(LOG, line);
}

function req(method, uri, headers) {
    return new Promise((resolve, reject) => {
        const u = new URL(uri);
        const r = https.request({
            hostname: u.hostname, port: u.port || 443, path: u.pathname + u.search,
            method, headers, timeout: 45000,
        }, res => {
            let data = '';
            res.on('data', c => data += c);
            res.on('end', () => { clearTimeout(hardTimer); resolve({ status: res.statusCode, body: data }); });
            res.on('error', e => { clearTimeout(hardTimer); reject(e); });
        });
        // Hard deadline — some proxies keep TCP alive but never send data. If we
        // don't hear back in 45s, destroy the request and reject.
        const hardTimer = setTimeout(() => {
            r.destroy(new Error('hard deadline 45s'));
            reject(new Error('hard deadline 45s'));
        }, 45000);
        r.on('error', e => { clearTimeout(hardTimer); reject(e); });
        r.on('timeout', () => r.destroy(new Error('socket timeout')));
        r.end();
    });
}

async function reqRetry(method, uri, headers, tries = 3) {
    let lastErr;
    for (let i = 0; i < tries; i++) {
        try { return await req(method, uri, headers); }
        catch (e) {
            lastErr = e;
            log(`  retry ${i+1}/${tries} after ${e.message}`);
            await new Promise(r => setTimeout(r, 2000 * (i + 1)));
        }
    }
    throw lastErr;
}

async function getToken(creds) {
    const form = 'grant_type=client_credentials' +
        '&client_id=' + encodeURIComponent(creds.CF_CLIENT_ID) +
        '&client_secret=' + encodeURIComponent(creds.CF_CLIENT_SECRET) +
        '&scope=' + encodeURIComponent(creds.CF_SCOPE);
    const r = await new Promise((res, rej) => {
        const u = new URL(creds.CF_TOKEN_URL);
        const rq = https.request({
            hostname: u.hostname, port: u.port || 443, path: u.pathname,
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Content-Length': Buffer.byteLength(form),
            },
            timeout: 30000,
        }, resp => {
            let d = '';
            resp.on('data', c => d += c);
            resp.on('end', () => res({ status: resp.statusCode, body: d }));
        });
        rq.on('error', rej);
        rq.write(form);
        rq.end();
    });
    const parsed = JSON.parse(r.body);
    if (!parsed.access_token) throw new Error('token fetch failed: ' + r.status + ' ' + r.body.slice(0, 200));
    return parsed.access_token;
}

(async () => {
    try {
        fs.writeFileSync(LOG, '');
        log('START inventory pull');

        const creds = JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8'));
        const token = await getToken(creds);
        log('token len=' + token.length);

        const allSns = new Set();
        let page = 1;
        let total = null;
        const t0 = Date.now();
        const skippedPages = [];
        let consecutiveFailures = 0;
        while (true) {
            const url = creds.CF_API_BASE + '/api/v1/TestReportDataFiles?page=' + page + '&pageSize=' + PAGE_SIZE;
            let r;
            try {
                r = await reqRetry('GET', url, { 'Authorization': 'Bearer ' + token });
                consecutiveFailures = 0;
            } catch (e) {
                // Skip this page on sustained failure, don't abort.
                log(`page ${page} SKIPPED after retries: ${e.message}`);
                skippedPages.push(page);
                consecutiveFailures++;
                if (consecutiveFailures >= 10) {
                    log(`FATAL: ${consecutiveFailures} consecutive page failures — aborting`);
                    break;
                }
                page++;
                continue;
            }
            if (r.status !== 200) {
                log(`page ${page} HTTP ${r.status}: ${r.body.slice(0, 300)}`);
                break;
            }
            const obj = JSON.parse(r.body);
            total = obj.TotalCount;
            for (const it of obj.Items) allSns.add(it.SerialNumber);
            if (page === 1 || page % 50 === 0 || allSns.size >= total) {
                const rate = allSns.size / Math.max(1, (Date.now() - t0) / 1000);
                const eta = Math.round((total - allSns.size) / Math.max(rate, 1));
                log(`page ${page} collected ${allSns.size}/${total} rate ${rate.toFixed(0)}/s eta ${eta}s skipped=${skippedPages.length}`);
            }
            if (obj.Items.length < PAGE_SIZE || allSns.size >= total) break;
            page++;
        }
        if (skippedPages.length > 0) {
            log(`retrying ${skippedPages.length} skipped pages with longer delay`);
            for (const p of skippedPages) {
                const url = creds.CF_API_BASE + '/api/v1/TestReportDataFiles?page=' + p + '&pageSize=' + PAGE_SIZE;
                try {
                    await new Promise(r => setTimeout(r, 3000));
                    const r2 = await reqRetry('GET', url, { 'Authorization': 'Bearer ' + token });
                    if (r2.status === 200) {
                        const obj = JSON.parse(r2.body);
                        for (const it of obj.Items) allSns.add(it.SerialNumber);
                        log(`  recovered page ${p} (+${obj.Items.length} SNs)`);
                    }
                } catch (e) {
                    log(`  page ${p} still failed: ${e.message}`);
                }
            }
        }
        log(`Hoffman inventory collected: ${allSns.size}`);

        log('querying local DB...');
        const localRows = await db.query('SELECT serial_number FROM test_records');
        const localSns = new Set(localRows.map(r => r.serial_number));
        log(`Local DB unique SNs: ${localSns.size}`);

        const hoffmanOnly = [];
        for (const s of allSns) if (!localSns.has(s)) hoffmanOnly.push(s);
        const localOnly = [];
        for (const s of localSns) if (!allSns.has(s)) localOnly.push(s);

        fs.writeFileSync(OUT_HOFFMAN, hoffmanOnly.join('\n'));
        fs.writeFileSync(OUT_LOCAL, localOnly.join('\n'));
        log(`Hoffman-only (need pull): ${hoffmanOnly.length}  -> ${OUT_HOFFMAN}`);
        log(`Local-only (not on Hoffman): ${localOnly.length}  -> ${OUT_LOCAL}`);
        log('DONE');

        await db.close();
    } catch (e) {
        log('FATAL: ' + e.message + '\n' + (e.stack || ''));
        process.exit(1);
    }
})();
