/**
 * lighthouse-scan.js
 *
 * Node.js scanner for the WMD SED Lighthouse Monitor.
 * Called once per domain per device mode by lighthouse-gather.php via proc_open().
 * Runs Lighthouse programmatically against a domain using Puppeteer's Chromium.
 *
 * Usage:
 *   node lighthouse-scan.js --domain=example.com --device=mobile --timeout=60000
 *   node lighthouse-scan.js --domain=example.com --device=desktop --timeout=60000
 *
 * Always outputs a single valid JSON object to stdout, even on error.
 * All diagnostic/debug output goes to stderr only - never stdout.
 *
 * Exit codes:
 *   0  - completed (pass, fail, or partial - result in stdout JSON)
 *   1  - hard error (result in stdout JSON with status=error)
 */

'use strict';

const puppeteer = require('puppeteer');

// Lighthouse v10+ is ESM-only. When loaded via CJS require() the callable
// sits at .default rather than being the module itself (v9 behaviour).
// Normalise to a single callable that works across both versions.
const _lhModule  = require('lighthouse');
const lighthouse = (typeof _lhModule === 'function')
    ? _lhModule
    : (typeof _lhModule.default === 'function' ? _lhModule.default : null);

// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------

const args = {};
for (const arg of process.argv.slice(2)) {
    const match = arg.match(/^--([a-zA-Z0-9_-]+)(?:=(.*))?$/);
    if (match) {
        args[match[1]] = match[2] !== undefined ? match[2] : true;
    }
}

const domain  = (args.domain  || '').trim().toLowerCase().replace(/^https?:\/\//, '').replace(/\/$/, '');
const device  = (args.device  || 'mobile').toLowerCase();
const timeout = parseInt(args.timeout || '60000', 10);
const debug   = args.debug === true || args.debug === 'true' || args.debug === '1';

// ---------------------------------------------------------------------------
// Output helpers - stdout is JSON only; debug goes to stderr
// ---------------------------------------------------------------------------

function dbg(msg) {
    if (debug) process.stderr.write('[DEBUG] ' + msg + '\n');
}

function outputResult(result) {
    process.stdout.write(JSON.stringify(result) + '\n');
}

function outputError(message, url) {
    outputResult({
        status : 'error',
        device : device,
        url    : url || null,
        scores : {
            performance    : null,
            seo            : null,
            accessibility  : null,
            'best-practices': null,
        },
        audits : {},
        error  : message,
    });
}

// ---------------------------------------------------------------------------
// Validation
// ---------------------------------------------------------------------------

if (!domain) {
    outputError('No domain supplied. Use --domain=example.com');
    process.exit(1);
}

if (!['mobile', 'desktop'].includes(device)) {
    outputError('Invalid --device value "' + device + '". Use mobile or desktop.');
    process.exit(1);
}

// ---------------------------------------------------------------------------
// Lighthouse configuration
// ---------------------------------------------------------------------------

// Scored categories supported by Lighthouse v10+. PWA was removed in v10.
const CATEGORIES = ['performance', 'seo', 'accessibility', 'best-practices'];

// Alias used in onlyCategories config setting — same list.
const LIGHTHOUSE_REQUEST_CATEGORIES = CATEGORIES;

/**
 * Build a Lighthouse config object for the given device mode.
 * Mobile uses Lighthouse's default throttling (simulated 4G, mid-tier mobile).
 * Desktop disables throttling and uses a desktop viewport.
 */
function buildLighthouseConfig(deviceMode) {
    const formFactor = deviceMode === 'desktop' ? 'desktop' : 'mobile';

    const screenEmulation = deviceMode === 'desktop'
        ? {
            mobile           : false,
            width            : 1350,
            height           : 940,
            deviceScaleFactor: 1,
            disabled         : false,
          }
        : {
            mobile           : true,
            width            : 412,
            height           : 823,
            deviceScaleFactor: 1.75,
            disabled         : false,
          };

    const throttling = deviceMode === 'desktop'
        ? {
            rttMs                 : 0,
            throughputKbps        : 0,
            cpuSlowdownMultiplier : 1,
            requestLatencyMs      : 0,
            downloadThroughputKbps: 0,
            uploadThroughputKbps  : 0,
          }
        : undefined; // undefined = use Lighthouse defaults (simulated slow 4G)

    const config = {
        extends     : 'lighthouse:default',
        settings    : {
            onlyCategories : LIGHTHOUSE_REQUEST_CATEGORIES,
            formFactor,
            screenEmulation,
            channel        : 'cli',
        },
    };

    if (throttling) {
        config.settings.throttling = throttling;
        config.settings.throttlingMethod = 'provided';
    }

    return config;
}

// ---------------------------------------------------------------------------
// Audit extraction
// ---------------------------------------------------------------------------

/**
 * Extract only the failing or warning audits for a category from the full
 * Lighthouse result. Passing audits (score === 1 and no numeric score needed)
 * are omitted to keep JSON file sizes manageable.
 *
 * Returns an array of simplified audit objects.
 */
function extractAudits(lhResult, categoryId) {
    const category = lhResult.categories[categoryId];
    if (!category) return [];

    const audits = [];

    for (const auditRef of category.auditRefs) {
        const audit = lhResult.audits[auditRef.id];
        if (!audit) continue;

        // Skip informational audits and those that fully pass
        if (audit.scoreDisplayMode === 'informative') continue;
        if (audit.scoreDisplayMode === 'not-applicable') continue;
        if (audit.score === 1) continue;

        const simplified = {
            id          : audit.id,
            title       : audit.title,
            description : audit.description
                ? audit.description.replace(/\[Learn more\]\(.*?\)/gi, '').trim()
                : '',
            score        : audit.score,
            displayValue : audit.displayValue || null,
            impact       : auditRef.weight > 0 ? 'weighted' : 'informational',
        };

        // Include details for table/opportunity audits (e.g. render-blocking resources)
        if (audit.details && audit.details.type === 'opportunity') {
            simplified.overallSavingsMs = audit.details.overallSavingsMs || null;
        }

        audits.push(simplified);
    }

    // Sort by score ascending (worst first), then by weight descending
    audits.sort((a, b) => {
        const scoreA = a.score === null ? -1 : a.score;
        const scoreB = b.score === null ? -1 : b.score;
        return scoreA - scoreB;
    });

    return audits;
}

/**
 * Extract incomplete/manual-check items for a category.
 */
function extractIncomplete(lhResult, categoryId) {
    const category = lhResult.categories[categoryId];
    if (!category) return [];

    const items = [];

    for (const auditRef of category.auditRefs) {
        const audit = lhResult.audits[auditRef.id];
        if (!audit) continue;
        if (audit.scoreDisplayMode !== 'manual' && audit.scoreDisplayMode !== 'notApplicable') continue;

        items.push({
            id          : audit.id,
            title       : audit.title,
            description : audit.description || '',
        });
    }

    return items;
}

// ---------------------------------------------------------------------------
// URL resolution: try https first, fall back to http
// ---------------------------------------------------------------------------

async function resolveUrl(domain, page) {
    const candidates = ['https://' + domain, 'http://' + domain];

    for (const url of candidates) {
        try {
            dbg('Trying URL: ' + url);
            const response = await page.goto(url, {
                waitUntil: 'domcontentloaded',
                timeout  : timeout,
            });
            if (response && response.ok()) {
                dbg('Resolved to: ' + url);
                return url;
            }
            // Non-2xx but reachable - still use this URL
            dbg('Non-OK response (' + (response ? response.status() : 'null') + ') for: ' + url + ', using anyway');
            return url;
        } catch (err) {
            dbg('Failed to reach ' + url + ': ' + err.message);
        }
    }

    return null;
}

// ---------------------------------------------------------------------------
// Main scan
// ---------------------------------------------------------------------------

async function runScan() {
    dbg('Starting scan: domain=' + domain + ' device=' + device + ' timeout=' + timeout);

    let browser = null;

    try {
        browser = await puppeteer.launch({
            headless          : true,
            args              : [
                '--no-sandbox',
                '--disable-setuid-sandbox',
                '--disable-dev-shm-usage',
                '--disable-gpu',
            ],
            defaultViewport   : null,
        });

        dbg('Browser launched');

        const page = await browser.newPage();

        // Resolve URL (https first, http fallback)
        const resolvedUrl = await resolveUrl(domain, page);

        if (!resolvedUrl) {
            await browser.close();
            outputError('Could not reach ' + domain + ' via https or http', null);
            process.exit(0);
        }

        // Close the probe page - Lighthouse will open its own
        await page.close();

        const config     = buildLighthouseConfig(device);
        const wsEndpoint = browser.wsEndpoint();
        const port       = parseInt(new URL(wsEndpoint).port, 10);

        dbg('Running Lighthouse on port ' + port + ' for ' + resolvedUrl);

        if (typeof lighthouse !== 'function') {
            throw new Error(
                'lighthouse is not callable — check your lighthouse npm package version. ' +
                'Run: node -e "const l=require(\'lighthouse\'); console.log(typeof l, typeof l.default)"'
            );
        }

        const runnerResult = await lighthouse(resolvedUrl, {
            port,
            output          : 'json',
            logLevel        : debug ? 'info' : 'silent',
            disableStorageReset: false,
        }, config);

        await browser.close();
        browser = null;

        if (!runnerResult || !runnerResult.lhr) {
            outputError('Lighthouse returned no result for ' + resolvedUrl, resolvedUrl);
            process.exit(0);
        }

        const lhr = runnerResult.lhr;
        dbg('Lighthouse complete. runWarnings: ' + JSON.stringify(lhr.runWarnings || []));

        // Extract scores (0–100 integers; null if not available)
        const scores = {};
        for (const cat of CATEGORIES) {
            const category = lhr.categories[cat];
            scores[cat] = category && category.score !== null
                ? Math.round(category.score * 100)
                : null;
        }

        dbg('Scores: ' + JSON.stringify(scores));

        // Extract audits per category
        const audits    = {};
        const incomplete = {};
        for (const cat of CATEGORIES) {
            audits[cat]     = extractAudits(lhr, cat);
            incomplete[cat] = extractIncomplete(lhr, cat);
        }

        // Determine overall status
        // 'error' handled above; here we just note pass/fail/partial
        // The PHP gatherer applies the threshold and LIGHTHOUSE_FAIL_CATEGORIES logic
        // and writes lh_status. We report a raw status here for reference.
        const allNull = Object.values(scores).every(s => s === null);
        const rawStatus = allNull ? 'error' : 'scanned';

        const result = {
            status      : rawStatus,
            device,
            url         : lhr.finalDisplayedUrl || resolvedUrl,
            scores,
            audits,
            incomplete,
            fetchTime   : lhr.fetchTime || null,
            error       : null,
        };

        outputResult(result);
        process.exit(0);

    } catch (err) {
        if (browser) {
            try { await browser.close(); } catch (_) {}
        }
        dbg('Fatal error: ' + err.message);
        outputError(err.message, null);
        process.exit(1);
    }
}

runScan();