// Node crypto is used to make a stable hashed filename for the bundled theme script.
import { createHash } from 'crypto';
// Node fs is used to read, copy, create, and remove files during the build and publish steps.
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'fs';
// Node path helpers are used to build safe file paths in a cross-platform way.
import { basename, dirname, extname, resolve } from 'path';
// fileURLToPath turns the current module URL into a normal file path for __dirname.
import { fileURLToPath } from 'url';
// sharp is used to shrink PNG and JPG files before they are published.
import sharp from 'sharp';
// Tailwind's Vite plugin lets Vite process the theme's Tailwind CSS entry file.
import tailwindcss from '@tailwindcss/vite';
// defineConfig gives Vite typed config structure and keeps the config wrapper standard.
import { defineConfig } from 'vite';

const __dirname = dirname(fileURLToPath(import.meta.url));
const DIST_DIR = resolve(__dirname, 'dist');
const STYLE_ENTRY_PATH = resolve(__dirname, 'src/css/style.css');
const BUNDLED_SCRIPT_SOURCES = [
    // These files are joined into one browser-safe script bundle.
    // The final output must stay plain classic JavaScript, not ESM.
    resolve(__dirname, 'node_modules/jquery/dist/jquery.min.js'),
    resolve(__dirname, 'src/js/script.js')
];
const STANDALONE_SCRIPT_SOURCES = [
    // These scripts stay as separate files because templates load them with Theme::script().
    resolve(__dirname, 'src/js/conditionals.js')
];
const FONT_EXTENSIONS = new Set(['.woff', '.woff2', '.ttf', '.otf', '.eot']);
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.ico']);
const THEME_CONFIG_PATH = resolve(__dirname, 'theme.json');

const DEFAULT_PUBLISH_DIR = resolve(__dirname, `../../public/themes/${getThemeName()}`);
const SOURCE_IMAGES_DIR = resolve(__dirname, 'src/images');
const SOURCE_FONTS_DIR = resolve(__dirname, 'src/fonts');

/**
 * Reads the theme name from theme.json so the default publish path is correct.
 */
function getThemeName() {
    const themeConfig = JSON.parse(readFileSync(THEME_CONFIG_PATH, 'utf8'));

    if (typeof themeConfig.name !== 'string' || themeConfig.name.trim() === '') {
        throw new Error('theme.json must include a non-empty "name" value.');
    }

    return themeConfig.name.trim();
}

/**
 * Picks the publish directory for built files.
 * THEME_PUBLISH_DIR can override the default path.
 */
function resolvePublishDir() {
    if (process.env.THEME_PUBLISH_DIR) {
        return resolve(__dirname, process.env.THEME_PUBLISH_DIR);
    }

    return DEFAULT_PUBLISH_DIR;
}

/**
 * Removes a file or folder only when it already exists.
 */
function removeIfExists(targetPath, options = {}) {
    if (!existsSync(targetPath)) {
        return;
    }

    rmSync(targetPath, options);
}

/**
 * Copies a directory tree only when the source folder exists.
 */
function copyDirectoryIfExists(sourceDir, destinationDir) {
    if (!existsSync(sourceDir)) {
        return;
    }

    mkdirSync(destinationDir, { recursive: true });
    cpSync(sourceDir, destinationDir, { recursive: true });
}

/**
 * Turns the theme script into plain browser JavaScript.
 *
 * Child themes may keep a small src/js/script.js file that imports the parent
 * theme script. This inlines that import so the published output still works
 * with a normal <script> tag.
 */
function readClassicScriptSource(sourcePath) {
    let sourceContent = readFileSync(sourcePath, 'utf8').trimEnd();

    if (!sourcePath.endsWith('/src/js/script.js')) {
        return sourceContent;
    }

    const importMatch = sourceContent.match(
        /^import\s+['"](.+\/vendor\/ignite\/themes\/.+\/src\/js\/script\.js)['"];?\s*$/m
    );

    if (!importMatch) {
        return sourceContent;
    }

    const importedScriptPath = resolve(dirname(sourcePath), importMatch[1]);
    const importedScriptContent = readFileSync(importedScriptPath, 'utf8').trimEnd();

    sourceContent = sourceContent.replace(importMatch[0], importedScriptContent);

    return sourceContent;
}

/**
 * Combines the bundled script sources into one generated file.
 */
function buildClassicScriptsBundle() {
    const bundleContent = BUNDLED_SCRIPT_SOURCES
        .map((sourcePath) => readClassicScriptSource(sourcePath))
        .join('\n\n');
    const bundleHash = createHash('sha256').update(bundleContent).digest('hex').slice(0, 8);
    const bundleFileName = `scripts-${bundleHash}.js`;
    const bundleDirectory = resolve(DIST_DIR, 'javascripts');
    const bundlePath = resolve(bundleDirectory, bundleFileName);

    mkdirSync(bundleDirectory, { recursive: true });
    writeFileSync(
        bundlePath,
        `/* Generated by the theme build. Do not edit directly. */\n${bundleContent}\n`
    );

    return bundleFileName;
}

/**
 * Copies scripts that are loaded directly through Theme::script().
 */
function publishStandaloneScripts() {
    const bundleDirectory = resolve(DIST_DIR, 'javascripts');

    mkdirSync(bundleDirectory, { recursive: true });

    for (const sourcePath of STANDALONE_SCRIPT_SOURCES) {
        const destinationPath = resolve(bundleDirectory, basename(sourcePath));

        cpSync(sourcePath, destinationPath);
    }
}

/**
 * Rewrites the Vite manifest so Theme::vite() points to the generated bundle.
 * It also removes standalone scripts and image records that should not stay in
 * the manifest.
 */
function updateManifestForClassicScripts(bundleFileName) {
    const manifestPath = resolve(DIST_DIR, 'manifest.json');

    if (! existsSync(manifestPath)) {
        return;
    }

    const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));

    manifest['src/js/script.js'] = {
        file: `javascripts/${bundleFileName}`,
        name: 'scripts',
        src: 'src/js/script.js',
        isEntry: true
    };

    delete manifest['src/js/conditionals.js'];

    for (const [manifestKey, entry] of Object.entries(manifest)) {
        if (
            manifestKey.startsWith('src/images/')
            || (typeof entry?.file === 'string' && entry.file.startsWith('images/'))
        ) {
            delete manifest[manifestKey];
        }
    }

    writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
}

/**
 * Gets every file path inside a directory tree.
 */
function collectFilesRecursively(directory) {
    if (!existsSync(directory)) {
        return [];
    }

    const files = [];

    for (const entry of readdirSync(directory)) {
        const entryPath = resolve(directory, entry);
        const entryStats = statSync(entryPath);

        if (entryStats.isDirectory()) {
            files.push(...collectFilesRecursively(entryPath));
            continue;
        }

        files.push(entryPath);
    }

    return files;
}

/**
 * Optimizes PNG and JPEG files with sharp.
 * The file is only replaced when the optimized version is smaller.
 */
async function optimizeDistImages(imagesDir) {
    const imageFiles = collectFilesRecursively(imagesDir);

    for (const imagePath of imageFiles) {
        if (imagePath.match(/\.png$/i)) {
            try {
                const originalBuffer = readFileSync(imagePath);
                const optimizedBuffer = await sharp(originalBuffer)
                    .png({
                        compressionLevel: 9,
                        palette: true,
                        quality: 90,
                        effort: 10
                    })
                    .toBuffer();

                // Keep the original when optimization would not shrink the file.
                if (optimizedBuffer.length <= originalBuffer.length) {
                    writeFileSync(imagePath, optimizedBuffer);
                }
            } catch {
                console.warn(`[ignite-theme-images] PNG optimization skipped for: ${imagePath}`);
            }

            continue;
        }

        if (!imagePath.match(/\.(jpe?g)$/i)) {
            continue;
        }

        try {
            const originalBuffer = readFileSync(imagePath);
            const optimizedBuffer = await sharp(originalBuffer)
                .jpeg({
                    quality: 90,
                    mozjpeg: true
                })
                .toBuffer();

            // Avoid replacing files when the optimized variant is unexpectedly larger.
            if (optimizedBuffer.length <= originalBuffer.length) {
                writeFileSync(imagePath, optimizedBuffer);
            }
        } catch {
            console.warn(`[ignite-theme-images] Optimization skipped for: ${imagePath}`);
        }
    }
}

/**
 * Copies built assets from dist to the public theme folder.
 * This keeps the runtime folders in sync and also copies source assets that
 * need to ship with the theme.
 */
async function publishThemeAssets() {
    const publishDir = resolvePublishDir();
    const publishSubdirs = ['fonts', 'images', 'javascripts', 'styles'];
    const manifestSource = resolve(DIST_DIR, 'manifest.json');
    const manifestDestination = resolve(publishDir, 'manifest.json');
    const distFontsDir = resolve(DIST_DIR, 'fonts');
    const distImagesDir = resolve(DIST_DIR, 'images');

    copyDirectoryIfExists(SOURCE_FONTS_DIR, distFontsDir);
    copyDirectoryIfExists(SOURCE_IMAGES_DIR, distImagesDir);
    const classicBundleFileName = buildClassicScriptsBundle();
    publishStandaloneScripts();
    updateManifestForClassicScripts(classicBundleFileName);

    await optimizeDistImages(distImagesDir);

    mkdirSync(publishDir, { recursive: true });

    const legacyVendorDir = resolve(publishDir, 'vendor');

    removeIfExists(legacyVendorDir, { recursive: true, force: true });

    for (const subdir of publishSubdirs) {
        const sourceDir = resolve(DIST_DIR, subdir);
        const destinationDir = resolve(publishDir, subdir);

        // Remove old files first so renamed or rehashed assets do not leave junk behind.
        removeIfExists(destinationDir, { recursive: true, force: true });

        if (existsSync(sourceDir)) {
            cpSync(sourceDir, destinationDir, { recursive: true });
        }
    }

    if (existsSync(manifestSource)) {
        removeIfExists(manifestDestination, { force: true });
        cpSync(manifestSource, manifestDestination);
    }

    return publishDir;
}

export default defineConfig({
    base: `/themes/${getThemeName()}/`,
    resolve: {
        alias: {
            '@theme-images': resolve(__dirname, 'src/images')
        }
    },
    plugins: [
        tailwindcss(),
        {
            name: 'ignite-theme-publish',
            async closeBundle() {
                const publishDir = await publishThemeAssets();
                console.info(`[ignite-theme-publish] Published assets to: ${publishDir}`);
            }
        }
    ],
    build: {
        outDir: DIST_DIR,
        emptyOutDir: true,
        manifest: 'manifest.json',
        rollupOptions: {
            output: {
                assetFileNames: ({ name }) => {
                    const extension = extname(name ?? '').toLowerCase();

                    if (extension === '.css') {
                        return 'styles/[name]-[hash][extname]';
                    }

                    if (FONT_EXTENSIONS.has(extension)) {
                        return 'fonts/[name][extname]';
                    }

                    if (IMAGE_EXTENSIONS.has(extension)) {
            // Keep image names simple so code can reference them directly.
            return 'images/[name][extname]';
                    }

                    return '[name]-[hash][extname]';
                },
                chunkFileNames: 'javascripts/[name]-[hash].js',
                entryFileNames: 'javascripts/[name]-[hash].js'
            },
            // Keep the manifest keyed by the source path that Theme::vite() uses.
            input: {
                styles: STYLE_ENTRY_PATH
            }
        }
    }
});
