//builderman
const fs = require('fs-extra');
const path = require('path');
const CleanCSS = require('clean-css');
const minify = require('minify');
const { XMLParser } = require('fast-xml-parser');
const cheerio = require('cheerio');

const cssMinifier = new CleanCSS({
    level: 2,
    compatibility: '*'
});

// Error handler function
// ts = error message, err = error type ('nofile' or 'generic')
// In order to implement error logging, we need to wait untill we load the config.
// Otherwise I can guarantee that shenanigans will occur
function fixts(ts, err) {
    if (err === 'nofile') {
        console.error(`There be nothing: ${ts}`);
    } else {
        //NOTE: Sometimes "generic" will be called. we don't actually check it. if we get an error that isnt nofile, it's generic.
        console.error(`This code sucks: ${ts}`);
    }
}

async function loadConfig(configPath) {
    if (!await fs.pathExists(configPath)) {
        fixts(configPath, 'nofile');
        throw new Error('Config file not found');
    }
    const configContent = await fs.readFile(configPath, 'utf8');
    const parser = new XMLParser({
        ignoreAttributes: false,
        attributeNamePrefix: '',
        allowBooleanAttributes: true,
        parseTagValue: true,
        isArray: (name, jpath, isLeafNode, isAttribute) => {
            return name === 'build' || name === 'tag';
        }
    });
    try {
        const config = parser.parse(configContent);
        return config.builds.build;
    } catch (err) {
        fixts('Failed to parse config.xml', 'generic');
        throw err;
    }
}

async function processHTML(htmlContent, buildConfig) {
    const $ = cheerio.load(htmlContent, { xmlMode: false });
    
    // Handle includes - if specified, keep only those tags
    if (buildConfig.include && buildConfig.include.tag) {
        const includes = Array.isArray(buildConfig.include.tag) 
            ? buildConfig.include.tag 
            : [buildConfig.include.tag];
            
        // Remove non-included tags at root level only when includes are specified
        $('body > *').each((_, elem) => {
            if (!includes.includes(elem.tagName)) {
                $(elem).remove();
            }
        });
    }
    // No includes specified means keep everything (except excludes)

    // Handle excludes - always remove specified tags
    if (buildConfig.exclude && buildConfig.exclude.tag) {
        const excludes = Array.isArray(buildConfig.exclude.tag) 
            ? buildConfig.exclude.tag 
            : [buildConfig.exclude.tag];
            
        excludes.forEach(tag => {
            $(tag).remove();
        });
    }

    return $.html();
}

async function processBuild(inputDir, outputDir, buildConfig) {
    if (!await fs.pathExists(inputDir)) {
        fixts(inputDir, 'nofile');
        throw new Error('Input directory not found');
    }

    const buildOutputDir = path.join(outputDir, buildConfig.name);
    await fs.ensureDir(buildOutputDir);

    // Process HTML template
    const htmlFiles = await fs.readdir(inputDir);
    for (const file of htmlFiles) {
        if (file.endsWith('.html')) {
            const inputFile = path.join(inputDir, file);
            if (!await fs.pathExists(inputFile)) {
                fixts(inputFile, 'nofile');
                continue;
            }
            try {
                const htmlContent = await fs.readFile(inputFile, 'utf8');
                const processedHTML = await processHTML(htmlContent, buildConfig);
                
                // Save the processed HTML to a temporary file for minification
                const tempFile = path.join(buildOutputDir, 'temp_' + file);
                await fs.writeFile(tempFile, processedHTML, 'utf8');

                // Minify using the file path
                const minified = await minify(tempFile);
                await fs.writeFile(
                    path.join(buildOutputDir, file),
                    minified,
                    'utf8'
                );

                // Clean up temp file
                await fs.remove(tempFile);
            } catch (err) {
                fixts(`Error processing ${file}: ${err}`, 'generic');
                // On error, at least save the unminified version
                const debugFile = path.join(buildOutputDir, 'debug_' + file);
                await fs.writeFile(debugFile, processedHTML, 'utf8');
            }
        }
    }

    // Process associated stylesheet
    if (buildConfig.style && buildConfig.style.src) {
        const cssFile = path.join(inputDir, buildConfig.style.src);
        if (!await fs.pathExists(cssFile)) {
            fixts(cssFile, 'nofile');
            return;
        }
        try {
            const cssContent = await fs.readFile(cssFile, 'utf8');
            const minified = cssMinifier.minify(cssContent);
            await fs.writeFile(
                path.join(buildOutputDir, buildConfig.style.src),
                minified.styles,
                'utf8'
            );
        } catch (err) {
            fixts(`Error processing CSS file ${buildConfig.style.src}: ${err}`, 'generic');
        }
    }
}

async function processFiles(inputDir, outputDir) {
    if (!await fs.pathExists(inputDir)) {
        fixts(inputDir, 'nofile');
        throw new Error('Input directory not found');
    }

    // Remove old dist directory if it exists
    await fs.remove(outputDir);
    await fs.ensureDir(outputDir);
    
    try {
        // Load and parse config
        const configPath = path.join(inputDir, 'config.xml');
        const builds = await loadConfig(configPath);
        
        // Process each build configuration
        const buildConfigs = Array.isArray(builds) ? builds : [builds];
        for (const buildConfig of buildConfigs) {
            await processBuild(inputDir, outputDir, buildConfig);
        }
    } catch (err) {
        fixts(`Build process failed: ${err}`, 'generic');
        throw err;
    }
}

// Run if called directly
if (require.main === module) {
    const inputDir = process.argv[2] || 'src';
    const outputDir = process.argv[3] || 'dist';
    
    processFiles(inputDir, outputDir).catch(err => fixts(err, 'generic'));
}
