// Parse rollup-plugin-visualizer HTML output → Top-15 chunks + critical-path total.
// Reads storage/bundle-analyze.html, extracts the inline `const data = {...};` JSON,
// computes per-chunk raw + gzip + brotli totals by walking each top-level chunk's tree
// and summing the corresponding nodeParts entries.

import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const htmlPath = resolve(__dirname, '..', 'storage', 'bundle-analyze.html');
const html = readFileSync(htmlPath, 'utf8');

// Locate the data assignment.
const marker = 'const data = ';
const start = html.indexOf(marker);
if (start === -1) {
    console.error('FATAL: marker "const data = " not found');
    process.exit(2);
}
// Find the terminating `};\n` that closes the object literal.
// The line ends with `}};` (closing data) — find first `};` after start.
const afterMarker = start + marker.length;
// Trailing semicolon: search for `};` then back up. Simpler: capture until matching brace.
let depth = 0;
let i = afterMarker;
let inString = false;
let escape = false;
let firstBraceFound = false;
for (; i < html.length; i++) {
    const ch = html[i];
    if (escape) { escape = false; continue; }
    if (ch === '\\') { escape = true; continue; }
    if (ch === '"') { inString = !inString; continue; }
    if (inString) continue;
    if (ch === '{') { depth++; firstBraceFound = true; }
    else if (ch === '}') {
        depth--;
        if (firstBraceFound && depth === 0) { i++; break; }
    }
}
const jsonStr = html.slice(afterMarker, i);
const data = JSON.parse(jsonStr);

// Build uid -> nodePart map (raw=renderedLength, gzip=gzipLength, brotli=brotliLength).
// In visualizer schema v2, each leaf uid lives in `nodeParts`.
const nodeParts = data.nodeParts || {};

// Walk a tree node, collect all uids in subtree.
function collectUids(node, out) {
    if (!node) return;
    if (node.uid) {
        out.push(node.uid);
        return;
    }
    if (Array.isArray(node.children)) {
        for (const c of node.children) collectUids(c, out);
    }
}

// Top-level: data.tree.children = array of chunks (name = "assets/<chunk>.js" or "assets/<chunk>.css").
const chunks = [];
for (const chunkNode of data.tree.children) {
    const uids = [];
    collectUids(chunkNode, uids);
    let raw = 0;
    let gzip = 0;
    let brotli = 0;
    for (const uid of uids) {
        const part = nodeParts[uid];
        if (!part) continue;
        raw += part.renderedLength || 0;
        gzip += part.gzipLength || 0;
        brotli += part.brotliLength || 0;
    }
    chunks.push({
        name: chunkNode.name,
        raw,
        gzip,
        brotli,
        moduleCount: uids.length,
    });
}

// Sort by raw size descending.
chunks.sort((a, b) => b.raw - a.raw);

// Format helper.
const fmt = (n) => `${(n / 1024).toFixed(2)} kB`;

console.log('=== Top-15 chunks (sorted by raw size) ===');
console.log('| # | name | raw | gzip | brotli | modules |');
console.log('|---|---|---|---|---|---|');
chunks.slice(0, 15).forEach((c, idx) => {
    console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} | ${c.moduleCount} |`);
});

console.log('\n=== All chunks (full list, for critical-path identification) ===');
console.log('| # | name | raw | gzip | brotli |');
console.log('|---|---|---|---|---|');
chunks.forEach((c, idx) => {
    console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} |`);
});

// Critical path: eager chunks loaded on initial page render.
// For an SPA with vite-plugin + dynamic-import lazy routes, the eager set = entry chunks
// (app-*.js + app-*.css) plus any chunks they statically import.
// The entry is `resources/js/app.ts` → produces `app-<hash>.js`.
// Static imports of entry from manifest = critical path; dynamic imports = lazy.
//
// Read the manifest.json (Laravel Vite plugin) to find the entry's static imports.
const manifestPath = resolve(__dirname, '..', 'public', 'build', 'manifest.json');
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));

// Find entry (isEntry: true).
const entries = Object.entries(manifest).filter(([, v]) => v.isEntry);
console.log('\n=== Manifest entries (isEntry=true) ===');
for (const [key, val] of entries) {
    console.log(`  ${key} -> ${val.file}`);
    if (val.imports) console.log(`    imports: ${JSON.stringify(val.imports)}`);
    if (val.css) console.log(`    css: ${JSON.stringify(val.css)}`);
    if (val.dynamicImports) console.log(`    dynamicImports.count: ${val.dynamicImports.length}`);
}

// Resolve critical-path file set: entry.file + recursive entry.imports[].file + entry.css[].
const eagerFiles = new Set();
function addEager(manifestKey) {
    const entry = manifest[manifestKey];
    if (!entry) return;
    if (entry.file && !eagerFiles.has(entry.file)) {
        eagerFiles.add(entry.file);
        // Recurse into static imports only (NOT dynamicImports).
        if (Array.isArray(entry.imports)) {
            for (const imp of entry.imports) addEager(imp);
        }
        if (Array.isArray(entry.css)) {
            for (const c of entry.css) eagerFiles.add(c);
        }
    }
}
for (const [key, val] of entries) {
    if (val.isEntry) addEager(key);
}

console.log('\n=== Critical-path eager file set (entry + static imports + css) ===');
for (const f of eagerFiles) console.log(`  - ${f}`);

// Sum the corresponding chunks' gzip + raw.
let eagerRaw = 0;
let eagerGzip = 0;
let eagerBrotli = 0;
for (const f of eagerFiles) {
    // Chunk name in visualizer is "assets/<file>" stripping "assets/" prefix in manifest path.
    // Manifest file path is e.g. "assets/app-XXXX.js" already.
    const target = chunks.find((c) => c.name === f);
    if (target) {
        eagerRaw += target.raw;
        eagerGzip += target.gzip;
        eagerBrotli += target.brotli;
    } else {
        // CSS files are not in tree (visualizer only tracks JS bundle internals).
        // Read CSS size directly from disk.
        try {
            const cssPath = resolve(__dirname, '..', 'public', 'build', f);
            const css = readFileSync(cssPath);
            const zlib = await import('node:zlib');
            const gzSize = zlib.gzipSync(css).length;
            const brSize = zlib.brotliCompressSync(css).length;
            eagerRaw += css.length;
            eagerGzip += gzSize;
            eagerBrotli += brSize;
            console.log(`  (CSS direct measure) ${f}: raw=${fmt(css.length)} gzip=${fmt(gzSize)} brotli=${fmt(brSize)}`);
        } catch (e) {
            console.log(`  WARN: could not measure ${f}: ${e.message}`);
        }
    }
}

console.log('\n=== Critical-path eager total (visualizer-attribution; pre-minify source bytes) ===');
console.log(`raw:    ${fmt(eagerRaw)}`);
console.log(`gzip:   ${fmt(eagerGzip)}`);
console.log(`brotli: ${fmt(eagerBrotli)}`);
console.log(`(${eagerFiles.size} files)`);

// ==========================================================================
// DISK-TRUTH VIEW: measure emitted files directly. Visualizer's renderedLength
// is pre-minify source-byte attribution; on-disk JS is post-minify. Vite's
// build stdout reports on-disk truth. Re-compute Top-15 + critical-path from
// disk for an apples-to-apples view against the Vite log.
// ==========================================================================

import { readdirSync, statSync } from 'node:fs';
import { gzipSync, brotliCompressSync } from 'node:zlib';

const buildDir = resolve(__dirname, '..', 'public', 'build', 'assets');
const allFiles = readdirSync(buildDir);
const diskJs = [];
const diskCss = [];
for (const f of allFiles) {
    const full = resolve(buildDir, f);
    const st = statSync(full);
    if (!st.isFile()) continue;
    const content = readFileSync(full);
    const entry = {
        name: `assets/${f}`,
        raw: content.length,
        gzip: gzipSync(content).length,
        brotli: brotliCompressSync(content).length,
    };
    if (f.endsWith('.js')) diskJs.push(entry);
    else if (f.endsWith('.css')) diskCss.push(entry);
}
diskJs.sort((a, b) => b.raw - a.raw);
diskCss.sort((a, b) => b.raw - a.raw);

console.log('\n=== Top-15 chunks (DISK-TRUTH, .js only, sorted by raw on-disk size) ===');
console.log('| # | name | raw | gzip | brotli |');
console.log('|---|---|---|---|---|');
diskJs.slice(0, 15).forEach((c, idx) => {
    console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} |`);
});

console.log('\n=== Top-10 CSS files (DISK-TRUTH) ===');
console.log('| # | name | raw | gzip | brotli |');
console.log('|---|---|---|---|---|');
diskCss.slice(0, 10).forEach((c, idx) => {
    console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} |`);
});

// Critical-path eager total — DISK TRUTH using same manifest set.
let diskEagerRaw = 0;
let diskEagerGzip = 0;
let diskEagerBrotli = 0;
const diskEagerDetails = [];
for (const f of eagerFiles) {
    const target = [...diskJs, ...diskCss].find((c) => c.name === f);
    if (target) {
        diskEagerRaw += target.raw;
        diskEagerGzip += target.gzip;
        diskEagerBrotli += target.brotli;
        diskEagerDetails.push(target);
    } else {
        console.log(`  WARN: ${f} not found on disk`);
    }
}

console.log('\n=== Critical-path eager files (DISK TRUTH details) ===');
console.log('| # | name | raw | gzip | brotli |');
console.log('|---|---|---|---|---|');
diskEagerDetails.forEach((c, idx) => {
    console.log(`| ${idx + 1} | ${c.name} | ${fmt(c.raw)} | ${fmt(c.gzip)} | ${fmt(c.brotli)} |`);
});

console.log('\n=== Critical-path eager total (DISK TRUTH = sum of emitted files) ===');
console.log(`raw:    ${fmt(diskEagerRaw)}`);
console.log(`gzip:   ${fmt(diskEagerGzip)}`);
console.log(`brotli: ${fmt(diskEagerBrotli)}`);
console.log(`(${eagerFiles.size} files)`);
