#!/usr/bin/env node

// @ts-check

const { Command } = require('commander');
const { spawn } = require('child_process');
const path = require('path');
const pkg = require('../package.json');
const { assertSupportedNodeVersion } = require('../src/Engine.js');

run().catch(err => {
    console.error(err);

    process.exitCode = process.exitCode || 1;
    process.exit();
});

/**
 * Run the program.
 */
async function run() {
    const program = new Command();

    program.name('mix');
    program.version(pkg.version);
    program.option(
        '--mix-config <path>',
        'The path to your Mix configuration file.',
        'webpack.mix'
    );
    program.option('--no-progress', 'Disable progress reporting', false);

    program
        .command('watch')
        .description('Build and watch files for changes.')
        .option('--hot', 'Enable hot reloading.', false)
        .option('--https', 'Enable https.', false)
        .action((opts, cmd) =>
            executeScript('watch', { ...program.opts(), ...opts }, cmd.args)
        );

    program
        .command('build', { isDefault: true })
        .description('Compile Mix.')
        .option('-p, --production', 'Run Mix in production mode.', false)
        .action((opts, cmd) =>
            executeScript('build', { ...program.opts(), ...opts }, cmd.args)
        );

    await program.parseAsync(process.argv);
}

/**
 * Execute the script.
 *
 * @param {"build"|"watch"} cmd
 * @param {{[key: string]: any}} opts
 * @param {string[]} args
 */
async function executeScript(cmd, opts, args = []) {
    assertSupportedNodeVersion();

    const env = getEffectiveEnv(opts);

    // We MUST use a relative path because the files
    // created by npm dont correctly handle paths
    // containg spaces on Windows (yarn does)
    const configPath = path.relative(
        process.cwd(),
        require.resolve('../setup/webpack.config.js')
    );

    const script = [
        commandScript(cmd, opts),
        `--config="${configPath}"`,
        ...quoteArgs(args)
    ].join(' ');

    const scriptEnv = {
        NODE_ENV: env,
        MIX_FILE: opts.mixConfig
    };

    const nodeEnv = requiresLegacyOpenSSLProvider()
        ? { NODE_OPTIONS: process.env.NODE_OPTIONS || `--openssl-legacy-provider` }
        : {};

    if (isTesting()) {
        process.stdout.write(
            JSON.stringify({
                script,
                env: scriptEnv
            })
        );

        return;
    }

    function restart() {
        let child = spawn(script, {
            stdio: 'inherit',
            shell: true,
            env: {
                ...process.env,
                ...nodeEnv,
                ...scriptEnv
            }
        });

        let shouldOverwriteExitCode = true;

        child.on('exit', (code, signal) => {
            // Note adapted from cross-env:
            // https://github.com/kentcdodds/cross-env/blob/3edefc7b450fe273655664f902fd03d9712177fe/src/index.js#L30-L31

            // The process exit code can be null when killed by the OS (like an out of memory error) or sometimes by node
            // SIGINT means the _user_ pressed Ctrl-C to interrupt the process execution
            // Return the appropriate error code in that case
            if (code === null) {
                code = signal === 'SIGINT' ? 130 : 1;
            }

            if (shouldOverwriteExitCode) {
                process.exitCode = code;
            }
        });

        process.on('SIGINT', () => {
            shouldOverwriteExitCode = false;
            child.kill('SIGINT');
        });

        process.on('SIGTERM', () => {
            shouldOverwriteExitCode = false;
            child.kill('SIGTERM');
        });
    }

    restart();
}

/**
 * Get the command-specific portion of the script.
 *
 * @param {"build"|"watch"} cmd
 * @param {{[key: string]: any}} opts
 */
function commandScript(cmd, opts) {
    const showProgress = isTTY() && opts.progress;
    const script = ['webpack'];

    if (cmd === 'build' && showProgress) {
        script.push('--progress');
    } else if (cmd === 'watch' && !opts.hot) {
        script.push('--watch');

        if (showProgress) {
            script.push('--progress');
        }
    } else if (cmd === 'watch' && opts.hot) {
        script.push('serve', '--hot');

        if (opts.https) {
            script.push('--https');
        }
    }

    return script.join(' ');
}

/**
 * Get the command arguments with quoted values.
 *
 * @param {string[]} args
 */
function quoteArgs(args) {
    return args.map(arg => {
        // Split string at first = only
        const pattern = /^([^=]+)=(.*)$/;
        const keyValue = arg.includes('=') ? pattern.exec(arg).slice(1) : [];

        if (keyValue.length === 2) {
            return `${keyValue[0]}="${keyValue[1]}"`;
        }

        return arg;
    });
}

/**
 * Get the effective envirnoment to run in
 *
 ** @param {{[key: string]: any}} opts
 */
function getEffectiveEnv(opts) {
    // If we've requested a production compile we enforce use of the production env
    // If we don't a user's global NODE_ENV may override and prevent minification of assets
    if (opts.production) {
        return 'production';
    }

    // We use `development` by default or under certain specific conditions when testing
    if (!process.env.NODE_ENV || (isTesting() && process.env.NODE_ENV === 'test')) {
        return 'development';
    }

    // Otherwsise defer to the current value of NODE_ENV
    return process.env.NODE_ENV;
}

function isTesting() {
    return process.env.TESTING;
}

function isTTY() {
    if (isTesting() && process.env.IS_TTY !== undefined) {
        return process.env.IS_TTY === 'true';
    }

    if (isTesting() && process.stdout.isTTY === undefined) {
        return true;
    }

    return process.stdout.isTTY;
}

function requiresLegacyOpenSSLProvider() {
    if (!process.version.startsWith('v17.') && !process.version.startsWith('v18.')) {
        return false;
    }

    try {
        require('crypto').createHash('md4').update('test').digest('hex');

        return false;
    } catch (err) {
        return true;
    }
}
