import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const AUDIO_EXTENSIONS = new Set(['.mp3', '.wav', '.ogg', '.aac', '.m4a']);
const SPECTRAL_SUFFIX = '.spectral.json';
/**
 * Read SpectralAudioEngine runtime source from packages/build/src/runtime/
 */
async function readSpectralRuntime() {
    const runtimePath = path.join(__dirname, 'runtime', 'spectral-audio-engine.js');
    try {
        return await fs.readFile(runtimePath, 'utf-8');
    }
    catch {
        // Fallback minimal runtime if file not found
        return '/* SpectralAudioEngine runtime not found */';
    }
}
function formatScanError(err) {
    return err instanceof Error ? err.message : String(err);
}
/**
 * Recursively scan a directory for audio and spectral JSON files.
 */
async function scanDirectory(dir, baseDir, report) {
    const audioFiles = [];
    const spectralFiles = [];
    const resolvedRoot = path.resolve(dir);
    async function walk(current) {
        let entries;
        try {
            entries = (await fs.readdir(current, { withFileTypes: true }));
        }
        catch (err) {
            const rel = path.relative(baseDir, current) || current;
            if (path.resolve(current) === resolvedRoot) {
                throw new Error(`[AudioOptimizer] Cannot read build directory "${current}": ${formatScanError(err)}`);
            }
            report.warn(`[AudioOptimizer] Skipping unreadable directory "${rel}": ${formatScanError(err)}`, err);
            return;
        }
        for (const entry of entries) {
            const fullPath = path.join(current, entry.name);
            if (entry.isDirectory()) {
                await walk(fullPath);
            }
            else if (entry.isFile()) {
                const ext = path.extname(entry.name).toLowerCase();
                if (AUDIO_EXTENSIONS.has(ext)) {
                    try {
                        const st = await fs.stat(fullPath);
                        audioFiles.push({
                            filePath: fullPath,
                            relativePath: path.relative(baseDir, fullPath),
                            size: st.size,
                            hasSpectral: false,
                        });
                    }
                    catch (err) {
                        const rel = path.relative(baseDir, fullPath);
                        report.warn(`[AudioOptimizer] Could not stat audio file "${rel}": ${formatScanError(err)}`, err);
                    }
                }
                else if (entry.name.endsWith(SPECTRAL_SUFFIX)) {
                    spectralFiles.push(fullPath);
                }
            }
        }
    }
    await walk(dir);
    // Cross-reference: for each audio file, check if a spectral counterpart exists
    const spectralSet = new Set(spectralFiles);
    for (const audio of audioFiles) {
        const spectralPath = audio.filePath.replace(/\.[^.]+$/, SPECTRAL_SUFFIX);
        if (spectralSet.has(spectralPath)) {
            audio.hasSpectral = true;
            try {
                const st = await fs.stat(spectralPath);
                audio.spectralSize = st.size;
                audio.potentialSavings = audio.size - st.size;
            }
            catch (err) {
                report.warn(`[AudioOptimizer] Could not stat spectral JSON for "${audio.relativePath}" ` +
                    `("${path.relative(baseDir, spectralPath)}"): ${formatScanError(err)}`, err);
                audio.hasSpectral = false;
                audio.spectralSize = undefined;
                audio.potentialSavings = undefined;
            }
        }
    }
    return { audioFiles, spectralFiles };
}
/**
 * Inject SpectralAudioEngine runtime into an HTML string.
 * Inserts a <script> tag before </head> (or at top of <body> if no </head>).
 */
function injectRuntimeIntoHtml(html, runtimeCode) {
    const scriptTag = `<script>/* SpectralAudioEngine v1 */\n${runtimeCode}\n</script>`;
    const headCloseIdx = html.lastIndexOf('</head>');
    if (headCloseIdx !== -1) {
        return html.slice(0, headCloseIdx) + scriptTag + '\n' + html.slice(headCloseIdx);
    }
    const bodyOpenIdx = html.indexOf('<body');
    if (bodyOpenIdx !== -1) {
        const bodyTagEnd = html.indexOf('>', bodyOpenIdx) + 1;
        return html.slice(0, bodyTagEnd) + '\n' + scriptTag + html.slice(bodyTagEnd);
    }
    return scriptTag + '\n' + html;
}
/**
 * AudioOptimizer — Build-time audio analysis and optimization.
 *
 * Scans a build directory for audio files and .spectral.json counterparts.
 * When spectral files are present, optionally injects the SpectralAudioEngine
 * runtime (< 3KB) into the output HTML so the browser can synthesize them.
 *
 * Usage:
 *   const optimizer = new AudioOptimizer({ buildDir: './dist', htmlOutputPath: './dist/index.html' });
 *   const report = await optimizer.optimize();
 *   console.log(report);
 */
export class AudioOptimizer {
    constructor(options) {
        this.options = {
            injectRuntime: true,
            htmlOutputPath: '',
            verbose: false,
            ...options,
        };
    }
    /**
     * Run the optimizer. Returns a report with audio file details and optimization results.
     */
    async optimize() {
        const { buildDir, injectRuntime, htmlOutputPath, verbose } = this.options;
        const log = (msg) => {
            if (verbose)
                console.log(`[AudioOptimizer] ${msg}`);
        };
        log(`Scanning ${buildDir} for audio files...`);
        const warn = (message, cause) => {
            console.warn(message);
            if (verbose && cause !== undefined) {
                console.warn(cause);
            }
        };
        const { audioFiles, spectralFiles } = await scanDirectory(buildDir, buildDir, { warn });
        const totalAudioSize = audioFiles.reduce((sum, f) => sum + f.size, 0);
        const totalSpectralSize = audioFiles.reduce((sum, f) => {
            return sum + (f.hasSpectral && f.spectralSize !== undefined ? f.spectralSize : f.size);
        }, 0);
        log(`Found ${audioFiles.length} audio file(s), ${spectralFiles.length} spectral JSON(s)`);
        log(`Total audio: ${(totalAudioSize / 1024).toFixed(1)}KB → with spectral: ${(totalSpectralSize / 1024).toFixed(1)}KB`);
        let runtimeInjected = false;
        let runtimeSize = 0;
        if (injectRuntime && spectralFiles.length > 0 && htmlOutputPath) {
            log(`Injecting SpectralAudioEngine runtime into ${htmlOutputPath}...`);
            try {
                const [html, runtimeCode] = await Promise.all([
                    fs.readFile(htmlOutputPath, 'utf-8'),
                    readSpectralRuntime(),
                ]);
                runtimeSize = Buffer.byteLength(runtimeCode, 'utf-8');
                const injectedHtml = injectRuntimeIntoHtml(html, runtimeCode);
                await fs.writeFile(htmlOutputPath, injectedHtml, 'utf-8');
                runtimeInjected = true;
                log(`Runtime injected (${runtimeSize}B)`);
            }
            catch (err) {
                log(`Warning: failed to inject runtime: ${err.message}`);
            }
        }
        else if (spectralFiles.length === 0) {
            log('No spectral files found — runtime injection skipped');
        }
        const netSavings = totalAudioSize - totalSpectralSize - runtimeSize;
        if (verbose) {
            for (const f of audioFiles) {
                if (f.hasSpectral) {
                    log(`  ✓ ${f.relativePath} ${(f.size / 1024).toFixed(1)}KB → spectral ${(f.spectralSize ?? 0)}B ` +
                        `(saved ${((f.potentialSavings ?? 0) / 1024).toFixed(1)}KB)`);
                }
                else {
                    log(`  - ${f.relativePath} ${(f.size / 1024).toFixed(1)}KB (no spectral counterpart)`);
                }
            }
        }
        return {
            audioFiles,
            spectralFiles,
            totalAudioSize,
            totalSpectralSize,
            runtimeInjected,
            runtimeSize,
            netSavings,
        };
    }
    /**
     * Print a human-readable summary of the optimization report.
     */
    static printReport(report) {
        console.log('\n── Audio Optimization Report ──────────────────────');
        console.log(`  Audio files:    ${report.audioFiles.length}`);
        console.log(`  Spectral JSONs: ${report.spectralFiles.length}`);
        console.log(`  Total audio:    ${(report.totalAudioSize / 1024).toFixed(1)}KB`);
        if (report.spectralFiles.length > 0) {
            console.log(`  With spectral:  ${(report.totalSpectralSize / 1024).toFixed(1)}KB`);
            if (report.runtimeInjected) {
                console.log(`  + Runtime:     +${report.runtimeSize}B`);
            }
            const savings = report.netSavings;
            const pct = report.totalAudioSize > 0
                ? ((savings / report.totalAudioSize) * 100).toFixed(1)
                : '0';
            if (savings > 0) {
                console.log(`  Net savings:    ${(savings / 1024).toFixed(1)}KB (${pct}%)`);
            }
        }
        if (report.runtimeInjected) {
            console.log('  ✓ SpectralAudioEngine runtime injected');
        }
        console.log('────────────────────────────────────────────────────\n');
    }
}
