import type { BunFile } from 'bun';
import path from 'path';
import { minify } from 'html-minifier';
import UglifyJS from 'uglify-js';
import { Jimp, ResizeStrategy } from 'jimp';
import { Type, type Static } from '@common/typebox';

import wasmPlugin from './plugins/wasmPlugin';
import imagePlugin from './plugins/imagePlugin';
import fontPlugin from './plugins/fontPlugin';
import audioPlugin from './plugins/audioPlugin';
import filePlugin from './plugins/filePlugin';

import { getGames } from './isGame';

export const b64 = async (file: string | BunFile) => (
    typeof file === 'string'
        ? Buffer.from(file)
        : Buffer.from(await file.arrayBuffer())
).toString('base64');

const AppConfigScheme = Type.Object({
    isApp: Type.Optional(Type.Boolean()),
    backgroundColor: Type.Optional(Type.String()),
    themeColor: Type.Optional(Type.String()),
});

type AppConfig = Static<typeof AppConfigScheme>;

const DEFAULT_CONFIG: AppConfig = {};

interface Args {
    production?: boolean;
    mobile?: boolean;
    local?: boolean;
}

const SW_SCRIPT = `<script>
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then(registration => console.log('SW registered'))
            .catch(err => console.error('SW registration failed:', err));
    });
}
</script>`;

const CSS_RELOAD_SCRIPT = `<script>
function connect() {
    const params = new URLSearchParams(location.search);
    const game = params.get('game') || 'index';
    const ws = new WebSocket('css-ws?game=' + encodeURIComponent(game));
    const onMessage = (e) => {
        try {
            const { type, css } = JSON.parse(e.data);
            if (type === 'css') {
                const styleTag = document.querySelector('style');
                if (styleTag) styleTag.textContent = css;
            }
        } catch { }
    };
    const onClose = (e) => {
        ws.removeEventListener('message', onMessage);
        ws.removeEventListener('close', onClose);
        setTimeout(connect, 500);
    };
    ws.addEventListener('message', onMessage);
    ws.addEventListener('close', onClose);
}
connect();
</script>`;

async function buildBundle(game: string, production: boolean) {
    const assetsDir = path.resolve(import.meta.dir, 'assets');
    const srcDir = path.resolve(import.meta.dir, '..', 'src');
    const gameDir = path.resolve(srcDir, 'games', game);
    const gameAssetsDir = path.resolve(gameDir, 'assets');

    let appConfig = Bun.file(path.resolve(gameAssetsDir, 'config.yml'));
    let config = DEFAULT_CONFIG;
    if (await appConfig.exists()) {
        try {
            const maybeConfig = Bun.YAML.parse(await appConfig.text());
            if (Type.Is(AppConfigScheme, maybeConfig)) {
                config = maybeConfig;
            }
        } catch { }
    }
    const entrypoints = [
        path.resolve(assetsDir, 'global.css'),
    ];
    if (config.isApp) {
        entrypoints.push(path.resolve(assetsDir, 'app.css'));
    }
    entrypoints.push(path.resolve(srcDir, 'index.ts'));
    const bundle = await Bun.build({
        outdir: '/tmp',
        entrypoints,
        sourcemap: production ? 'none' : 'inline',
        define: {
            global: 'window',
            GAME: `"${game}"`,
            GAMES: JSON.stringify(await getGames()),
        },
        plugins: [
            imagePlugin,
            audioPlugin,
            fontPlugin,
            wasmPlugin({ production }),
            filePlugin,
        ]
    });

    return { bundle, config, gameDir, gameAssetsDir, srcDir, assetsDir };
}

export async function buildHTML(game: string, {
    production = false,
    mobile = false,
    local = false,
}: Args = {}) {
    const {
        bundle,
        config,
        gameAssetsDir,
        assetsDir,
    } = await buildBundle(game, production);

    const html = await Bun.file(path.resolve(assetsDir, 'index.html')).text();

    if (bundle.success) {
        const scriptFile = bundle.outputs.find(a => a.kind === 'entry-point' && a.path.endsWith('.js'));
        if (!scriptFile) {
            console.error('No entry point found:', bundle.outputs);
            return;
        }
        const iconFile = Bun.file(path.resolve(gameAssetsDir, 'favicon.ico'));
        let icon = '';
        if (await iconFile.exists()) {
            icon = `<link rel="icon" href="data:;base64,${await b64(iconFile)}" />`;
        }
        let style = '';
        let styleFiles = bundle.outputs.filter(a => a.kind === 'asset' && a.path.endsWith('.css'));
        for (let styleFile of styleFiles) {
            style += await styleFile.text();
        }
        const title = game[0].toUpperCase() + game.slice(1).toLowerCase();
        let pwaIconFile = path.resolve(gameAssetsDir, 'pwa_icon.png');
        if (!await Bun.file(pwaIconFile).exists()) {
            pwaIconFile = path.resolve(assetsDir, 'pwa_icon.png');
        }

        const pwaIcon = await Jimp.read(pwaIconFile);
        if (pwaIcon.width !== 192 || pwaIcon.height !== 192) {
            pwaIcon.resize({ w: 192, h: 192, mode: ResizeStrategy.BICUBIC });
        }
        const pwaIconData = await pwaIcon.getBase64("image/png");

        let manifest = '';
        if (production && !local) {
            const publishURL = process.env.PUBLISH_URL ? `${process.env.PUBLISH_URL}${game}` : '.';
            const manifestJSON = JSON.stringify({
                name: title,
                short_name: title,
                start_url: publishURL,
                id: `/${game}`,
                display_override: config.isApp ? [] : ["window-controls-overlay"],
                display: config.isApp ? "standalone" : "fullscreen",
                background_color: config.backgroundColor ?? "#ffffff",
                theme_color: config.themeColor ?? "#000000",
                icons: [{
                    src: pwaIconData,
                    sizes: '192x192',
                    type: 'image/png'
                }]
            });
            manifest = `<link rel="manifest" href="data:application/json;base64,${await b64(manifestJSON)}" />`;
        }
        let script = await scriptFile.text();
        const inits = new Set<string>();
        script = script.replace(/var (init_[^ ]+) = __esm\(\(\)/g, (_, $1) => {
            inits.add($1);
            return `var ${$1} = __esm(async ()`;
        });

        for (const init of inits) {
            script = script.replaceAll(`${init}()`, `await ${init}()`);
        }

        script = script.replaceAll('await Promise.resolve().then(() =>', '(');
        let scriptPrefix = '';
        if (production) {
            const minifyResult = UglifyJS.minify(script, {
                module: true,
                toplevel: true,
            });
            if (minifyResult.error) {
                console.warn(`Minify error: ${minifyResult.error}`);
            } else {
                script = minifyResult.code;
            }
        } else if (mobile) {
            const eruda = await Bun.file(path.resolve(import.meta.dir, '..', 'node_modules', 'eruda', 'eruda.js')).text();
            scriptPrefix += `<script>${eruda};\neruda.init();</script>`;
        }
        if (!local) {
            scriptPrefix += SW_SCRIPT;
        }
        if (local && !production) {
            scriptPrefix += CSS_RELOAD_SCRIPT;
        }

        // function to avoid $& being replaced
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement
        const resultHTML = html
            .replace('<!--$SCRIPT$-->', () => `${scriptPrefix}<script type="module">${script}</script>`)
            .replace('<!--$TITLE$-->', () => title)
            .replace('<!--$ICON$-->', () => icon)
            .replace('<!--$MANIFEST$-->', () => manifest)
            .replace('<!--$STYLE$-->', () => `<style>${style}</style>`);

        return minify(resultHTML, {
            collapseWhitespace: production,
            decodeEntities: true,
            minifyCSS: production,
            minifyJS: production,
        });
    } else {
        console.error('Failed: ', !bundle.success, bundle);
    }
}

/**
 * Rebuild only the CSS bundle (resolves composes: directives via Bun.build).
 * Used by the dev server for CSS hot reload.
 */
export async function buildCSS(game: string): Promise<string | null> {
    const { bundle } = await buildBundle(game, false);
    if (bundle.success) {
        let style = '';
        for (const file of bundle.outputs.filter(a => a.kind === 'asset' && a.path.endsWith('.css'))) {
            style += await file.text();
        }
        return style;
    } else {
        console.error('CSS build failed:', bundle);
        return null;
    }
}