#!/usr/bin/env node
import { spawn } from 'node:child_process';
import { copyFile, readFile, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { platform } from 'node:os';
import * as readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

const ROOT = dirname(fileURLToPath(import.meta.url));
const RENDERER_REPO_URL = 'https://github.com/duckietm/Nitro_Render_V3.git';
const RENDERER_DIR = resolve(ROOT, '..', 'Nitro_Render_V3');
const CONFIG_DIR = join(ROOT, 'public', 'configuration');
const NITRO_BUILD_FILE = join(ROOT, '.nitro-build.json');
const IS_WINDOWS = platform() === 'win32';
const MIN_NODE_MAJOR = 18;
const VALID_JSON_MODES = ['json5', 'legacy', 'auto'];
const DEFAULT_JSON_MODE = 'json5';

const KEY_SPECS = {
    'socket.url':           { type: 'url',       schemes: ['ws:', 'wss:'],     flag: 'socket-url' },
    'api.url':              { type: 'url',       schemes: ['http:', 'https:'], flag: 'api-url' },
    'asset.url':            { type: 'url',       schemes: ['http:', 'https:'], flag: 'asset-url' },
    'image.library.url':    { type: 'url',       schemes: ['http:', 'https:'], flag: 'image-library-url' },
    'hof.furni.url':        { type: 'url',       schemes: ['http:', 'https:'], flag: 'hof-furni-url' },
    'camera.url':           { type: 'url',       schemes: ['http:', 'https:'], flag: 'camera-url' },
    'thumbnails.url':       { type: 'url',       schemes: ['http:', 'https:'], flag: 'thumbnails-url' },
    'url.prefix':           { type: 'pathOrUrl', schemes: ['http:', 'https:'], flag: 'url-prefix' },
    'habbopages.url':       { type: 'pathOrUrl', schemes: ['http:', 'https:'], flag: 'habbopages-url' },
    'apiBaseUrl':           { type: 'url',       schemes: ['http:', 'https:'], flag: 'api-base-url' },
    'plainConfigBaseUrl':   { type: 'url',       schemes: ['http:', 'https:'], flag: 'plain-config-base-url' },
    'plainGamedataBaseUrl': { type: 'url',       schemes: ['http:', 'https:'], flag: 'plain-gamedata-base-url' }
};

const FLAG_TO_KEY = Object.fromEntries(
    Object.entries(KEY_SPECS).map(([key, spec]) => [spec.flag, key])
);

const CONFIG_FILES = [
    {
        example: 'renderer-config.example',
        target: 'renderer-config.json',
        keys: ['socket.url', 'api.url', 'asset.url', 'image.library.url', 'hof.furni.url']
    },
    {
        example: 'ui-config.example',
        target: 'ui-config.json',
        keys: ['camera.url', 'thumbnails.url', 'url.prefix', 'habbopages.url']
    },
    {
        example: 'client-mode.example',
        target: 'client-mode.json',
        keys: ['apiBaseUrl', 'plainConfigBaseUrl', 'plainGamedataBaseUrl']
    }
];

const STEPS = [
    'Check prerequisites',
    'Clone Nitro_Render_V3',
    'Setup renderer (yarn install + yarn link)',
    'Setup client (yarn install + yarn link)',
    'Copy config files',
    'Choose JSON parsing mode',
    'Configure URLs',
    'Build (yarn build)',
    'Summary'
];

let currentStep = 0;
let activeReadline = null;
const summary = {
    rendererCloned: false,
    rendererSkipped: false,
    configsCreated: [],
    configsKept: [],
    configsPatched: [],
    jsonMode: null,
    jsonModeSource: null,
    buildRan: false,
    buildSkipped: false
};

const useColor = !process.env.NO_COLOR && process.stdout.isTTY;
const c = {
    reset:  useColor ? '\x1b[0m'  : '',
    bold:   useColor ? '\x1b[1m'  : '',
    dim:    useColor ? '\x1b[2m'  : '',
    red:    useColor ? '\x1b[31m' : '',
    green:  useColor ? '\x1b[32m' : '',
    yellow: useColor ? '\x1b[33m' : '',
    cyan:   useColor ? '\x1b[36m' : ''
};

function info(msg) { console.log(c.cyan   + '[i]' + c.reset + ' ' + msg); }
function ok(msg)   { console.log(c.green  + '[+]' + c.reset + ' ' + msg); }
function warn(msg) { console.log(c.yellow + '[!]' + c.reset + ' ' + msg); }
function err(msg)  { console.error(c.red  + '[x]' + c.reset + ' ' + msg); }

function step(label) {
    currentStep += 1;
    const sep = '----------------------------------------------------------------';
    console.log('');
    console.log(c.dim + sep + c.reset);
    console.log(c.bold + '[' + currentStep + '/' + STEPS.length + '] ' + label + c.reset);
    console.log(c.dim + sep + c.reset);
}

function runShell(cmdString, cwd) {
    return new Promise((resolveFn, rejectFn) => {
        const child = spawn(cmdString, { shell: true, cwd, stdio: 'inherit' });
        child.on('error', e => rejectFn(new Error("'" + cmdString + "' (cwd: " + cwd + ") failed to start: " + e.message)));
        child.on('exit', code => {
            if (code === 0) resolveFn();
            else rejectFn(new Error("'" + cmdString + "' (cwd: " + cwd + ") exited with code " + code));
        });
    });
}

function runCapture(cmdString) {
    return new Promise((resolveFn, rejectFn) => {
        const child = spawn(cmdString, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
        let stdout = '';
        let stderr = '';
        child.stdout.on('data', d => { stdout += d.toString(); });
        child.stderr.on('data', d => { stderr += d.toString(); });
        child.on('error', e => rejectFn(new Error("'" + cmdString + "' failed to start: " + e.message)));
        child.on('exit', code => {
            if (code === 0) resolveFn(stdout.trim());
            else rejectFn(new Error("'" + cmdString + "' exited with code " + code + (stderr ? ': ' + stderr.trim() : '')));
        });
    });
}

function validateValue(value, spec) {
    if (value === '' || value === undefined || value === null) {
        if (spec.type === 'pathOrUrl') return { valid: true };
        return { valid: false, error: 'value cannot be empty' };
    }
    if (spec.type === 'pathOrUrl' && value.startsWith('/')) {
        return { valid: true };
    }
    let parsed;
    try {
        parsed = new URL(value);
    } catch {
        return { valid: false, error: 'not a valid URL' };
    }
    if (spec.schemes.length > 0 && !spec.schemes.includes(parsed.protocol)) {
        const allowed = spec.schemes.map(s => s.replace(':', '')).join(', ');
        return { valid: false, error: 'scheme must be one of: ' + allowed };
    }
    return { valid: true };
}

function parseArgs() {
    const opts = {
        interactive: true,
        skipBuild: false,
        skipClone: false,
        skipLink: false,
        help: false,
        jsonMode: null,
        urlOverrides: {}
    };
    const builtinFlags = new Set([
        '--non-interactive', '--skip-prompts',
        '--skip-build', '--skip-clone', '--skip-link',
        '--help', '-h'
    ]);
    for (const arg of process.argv.slice(2)) {
        if (builtinFlags.has(arg)) {
            switch (arg) {
                case '--non-interactive':
                case '--skip-prompts': opts.interactive = false; break;
                case '--skip-build':   opts.skipBuild = true; break;
                case '--skip-clone':   opts.skipClone = true; break;
                case '--skip-link':    opts.skipLink = true; break;
                case '--help':
                case '-h':             opts.help = true; break;
            }
            continue;
        }
        const eq = arg.indexOf('=');
        if (arg.startsWith('--') && eq > 2) {
            const flagName = arg.slice(2, eq);
            const value = arg.slice(eq + 1);
            if (flagName === 'json-mode') {
                if (!VALID_JSON_MODES.includes(value)) {
                    warn('Invalid --json-mode=' + value + ' (expected: ' + VALID_JSON_MODES.join(', ') + '), ignored');
                    continue;
                }
                opts.jsonMode = value;
                continue;
            }
            const key = FLAG_TO_KEY[flagName];
            if (key) {
                opts.urlOverrides[key] = value;
                continue;
            }
        }
        warn('Unknown flag: ' + arg + ' (ignored)');
    }
    return opts;
}

function printUsage() {
    const flagList = Object.entries(KEY_SPECS)
        .map(([key, spec]) => '  --' + spec.flag + '=<value>' + ' '.repeat(Math.max(1, 32 - spec.flag.length)) + 'Set ' + key)
        .join('\n');
    console.log([
        'Nitro-V3 cross-platform installer',
        '',
        'Usage: node install.mjs [flags]',
        '',
        'Workflow flags:',
        '  --non-interactive, --skip-prompts   Keep default URLs unless overridden by --<key>=<value>',
        '  --json-mode=<json5|legacy|auto>     Choose the JSON parsing mode without prompting',
        '  --skip-build                        Skip the final yarn build',
        '  --skip-clone                        Skip cloning Nitro_Render_V3',
        '  --skip-link                         Skip yarn link calls (useful when re-running)',
        '  --help, -h                          Show this help and exit',
        '',
        'URL override flags (override interactive prompts; combine with --non-interactive for fully automated runs):',
        flagList,
        '',
        'Steps performed:',
        '  1. Check Node >= ' + MIN_NODE_MAJOR + ', yarn, git',
        '  2. Clone Nitro_Render_V3 to ../Nitro_Render_V3',
        '  3. yarn install + yarn link in the renderer',
        '  4. yarn install + yarn link "@nitrots/nitro-renderer" in this project',
        '  5. Copy public/configuration/*.example -> *.json (keeps existing files)',
        '  6. Choose JSON parsing mode (json5 recommended) -> writes .nitro-build.json',
        '  7. Prompt for URLs and patch the JSON config files',
        '  8. yarn build (honours the JSON mode chosen at step 6)',
        ''
    ].join('\n'));
}

async function checkPrereqs() {
    const nodeVer = process.versions.node;
    const major = parseInt(nodeVer.split('.')[0], 10);
    if (Number.isNaN(major) || major < MIN_NODE_MAJOR) {
        throw new Error('Node >= ' + MIN_NODE_MAJOR + ' required (you have v' + nodeVer + '). Install from https://nodejs.org/');
    }
    ok('Node v' + nodeVer);

    try {
        const v = await runCapture('yarn --version');
        ok('yarn ' + v);
    } catch {
        const hint = IS_WINDOWS ? 'npm i -g yarn' : 'sudo npm i -g yarn';
        throw new Error('yarn not found on PATH. Install with: ' + hint);
    }

    try {
        const v = await runCapture('git --version');
        ok(v);
    } catch {
        const hint = IS_WINDOWS ? 'winget install Git.Git' : 'sudo apt-get install git  (or your distro equivalent)';
        throw new Error('git not found on PATH. Install with: ' + hint);
    }
}

async function cloneRenderer(opts) {
    if (opts.skipClone) { info('--skip-clone: not cloning Nitro_Render_V3'); summary.rendererSkipped = true; return; }
    if (existsSync(RENDERER_DIR)) {
        warn('Nitro_Render_V3 already exists at ' + RENDERER_DIR + ' - skipping clone (yarn install/link will still run).');
        summary.rendererSkipped = true;
        return;
    }
    await runShell('git clone ' + RENDERER_REPO_URL + ' "' + RENDERER_DIR + '"', dirname(RENDERER_DIR));
    summary.rendererCloned = true;
    ok('Cloned Nitro_Render_V3 to ' + RENDERER_DIR);
}

async function setupRenderer(opts) {
    if (!existsSync(RENDERER_DIR)) {
        throw new Error('Renderer directory not found: ' + RENDERER_DIR + '. Re-run without --skip-clone or clone it manually.');
    }
    await runShell('yarn install', RENDERER_DIR);
    if (opts.skipLink) { info('--skip-link: skipping yarn link in renderer'); return; }
    try {
        await runShell('yarn link', RENDERER_DIR);
    } catch (e) {
        warn('yarn link in renderer failed (likely already linked): ' + e.message);
    }
}

async function setupClient(opts) {
    await runShell('yarn install', ROOT);
    if (opts.skipLink) { info('--skip-link: skipping yarn link in client'); return; }
    try {
        await runShell('yarn link "@nitrots/nitro-renderer"', ROOT);
    } catch (e) {
        warn('yarn link "@nitrots/nitro-renderer" failed (likely already linked): ' + e.message);
    }
}

async function writeJsonMode(mode) {
    const payload = { jsonMode: mode, configuredAt: new Date().toISOString() };
    await writeFile(NITRO_BUILD_FILE, JSON.stringify(payload, null, 2) + '\n', 'utf8');
}

async function chooseJsonMode(opts) {
    if (opts.jsonMode) {
        await writeJsonMode(opts.jsonMode);
        summary.jsonMode = opts.jsonMode;
        summary.jsonModeSource = 'CLI (--json-mode)';
        ok('JSON mode set to ' + opts.jsonMode + ' (from --json-mode)');
        return;
    }

    let existing = null;
    if (existsSync(NITRO_BUILD_FILE)) {
        try {
            const raw = await readFile(NITRO_BUILD_FILE, 'utf8');
            const parsed = JSON.parse(raw);
            if (VALID_JSON_MODES.includes(parsed?.jsonMode)) existing = parsed.jsonMode;
        } catch {}
    }

    if (!opts.interactive) {
        const mode = existing || DEFAULT_JSON_MODE;
        await writeJsonMode(mode);
        summary.jsonMode = mode;
        summary.jsonModeSource = existing ? 'existing .nitro-build.json' : 'default (non-interactive)';
        info('--non-interactive: JSON mode = ' + mode + (existing ? ' (preserved)' : ' (default)'));
        return;
    }

    info('Pick how configuration files (renderer-config, ui-config, gamedata) are parsed.');
    info('  1) JSON5  (recommended - accepts comments, trailing commas, single quotes)');
    info('  2) JSON   (legacy strict - only standard JSON valid)');
    if (existing) info('  Current value in .nitro-build.json: ' + existing);

    const rl = readline.createInterface({ input, output });
    activeReadline = rl;
    let chosen = null;
    try {
        while (chosen === null) {
            const defaultLabel = existing || '1=JSON5';
            const answer = (await rl.question('  Choice [' + defaultLabel + ']: ')).trim().toLowerCase();
            if (answer.length === 0) {
                chosen = existing || DEFAULT_JSON_MODE;
            } else if (answer === '1' || answer === 'json5' || answer === 'y' || answer === 'yes') {
                chosen = 'json5';
            } else if (answer === '2' || answer === 'json' || answer === 'legacy' || answer === 'n' || answer === 'no') {
                chosen = 'legacy';
            } else if (answer === 'auto') {
                chosen = 'auto';
            } else {
                warn('Invalid choice. Enter 1, 2, json5, json, legacy, or auto.');
            }
        }
    } finally {
        activeReadline = null;
        rl.close();
    }

    await writeJsonMode(chosen);
    summary.jsonMode = chosen;
    summary.jsonModeSource = 'interactive prompt';
    ok('JSON mode set to ' + chosen + ' -> wrote .nitro-build.json');
    if (chosen === 'legacy') {
        warn('Legacy mode is strict: config files must be valid standard JSON (no comments, no trailing commas).');
    }
}

async function copyConfigs() {
    for (const entry of CONFIG_FILES) {
        const src = join(CONFIG_DIR, entry.example);
        const dst = join(CONFIG_DIR, entry.target);
        if (!existsSync(src)) {
            throw new Error('Missing example file: ' + src);
        }
        if (existsSync(dst)) {
            warn(entry.target + ' already exists - keeping existing file (URL overrides will still patch it).');
            summary.configsKept.push(entry.target);
        } else {
            await copyFile(src, dst);
            ok('Created ' + entry.target);
            summary.configsCreated.push(entry.target);
        }
    }
}

async function applyOverridesNonInteractive(opts) {
    for (const entry of CONFIG_FILES) {
        const dst = join(CONFIG_DIR, entry.target);
        const raw = await readFile(dst, 'utf8');
        let obj;
        try {
            obj = JSON.parse(raw);
        } catch (e) {
            throw new Error('Could not parse ' + entry.target + ' as JSON: ' + e.message);
        }
        let changed = false;
        for (const key of entry.keys) {
            if (Object.prototype.hasOwnProperty.call(opts.urlOverrides, key)) {
                const value = opts.urlOverrides[key];
                const result = validateValue(value, KEY_SPECS[key]);
                if (!result.valid) {
                    throw new Error('Invalid value for --' + KEY_SPECS[key].flag + '=' + JSON.stringify(value) + ': ' + result.error);
                }
                if (obj[key] !== value) {
                    obj[key] = value;
                    changed = true;
                }
            }
        }
        if (changed) {
            await writeFile(dst, JSON.stringify(obj, null, 4) + '\n');
            ok('Updated ' + entry.target + ' (from CLI flags)');
            summary.configsPatched.push(entry.target);
        }
    }
}

async function promptConfigs(opts) {
    const overrideKeys = Object.keys(opts.urlOverrides);
    if (!opts.interactive) {
        if (overrideKeys.length > 0) {
            info('--non-interactive with ' + overrideKeys.length + ' URL override(s); applying without prompts');
            await applyOverridesNonInteractive(opts);
        } else {
            info('--non-interactive: keeping URL values from .example defaults');
        }
        return;
    }
    info('Press Enter to keep the current value shown in [brackets]. URLs are validated.');
    if (overrideKeys.length > 0) {
        info('CLI overrides take precedence and skip prompts: ' + overrideKeys.map(k => '--' + KEY_SPECS[k].flag).join(', '));
    }
    const rl = readline.createInterface({ input, output });
    activeReadline = rl;
    try {
        for (const entry of CONFIG_FILES) {
            const dst = join(CONFIG_DIR, entry.target);
            const raw = await readFile(dst, 'utf8');
            let obj;
            try {
                obj = JSON.parse(raw);
            } catch (e) {
                throw new Error('Could not parse ' + entry.target + ' as JSON: ' + e.message);
            }
            console.log('\n  ' + c.bold + entry.target + c.reset);
            let changed = false;
            for (const key of entry.keys) {
                const spec = KEY_SPECS[key];
                if (Object.prototype.hasOwnProperty.call(opts.urlOverrides, key)) {
                    const value = opts.urlOverrides[key];
                    const result = validateValue(value, spec);
                    if (!result.valid) {
                        throw new Error('Invalid value for --' + spec.flag + '=' + JSON.stringify(value) + ': ' + result.error);
                    }
                    if (obj[key] !== value) { obj[key] = value; changed = true; }
                    console.log('    ' + c.dim + key + ' = ' + value + '  (from --' + spec.flag + ')' + c.reset);
                    continue;
                }
                const current = obj[key] === undefined ? '' : String(obj[key]);
                while (true) {
                    const answer = await rl.question('    ' + key + ' [' + current + ']: ');
                    const trimmed = answer.trim();
                    if (trimmed.length === 0) break;
                    if (trimmed === current) break;
                    const result = validateValue(trimmed, spec);
                    if (!result.valid) {
                        warn('Invalid: ' + result.error + '. Try again or press Enter to keep current.');
                        continue;
                    }
                    obj[key] = trimmed;
                    changed = true;
                    break;
                }
            }
            if (changed) {
                await writeFile(dst, JSON.stringify(obj, null, 4) + '\n');
                ok('Updated ' + entry.target);
                summary.configsPatched.push(entry.target);
            } else {
                info('No changes to ' + entry.target);
            }
        }
    } finally {
        activeReadline = null;
        rl.close();
    }
}

async function runBuild(opts) {
    if (opts.skipBuild) { info('--skip-build: skipping yarn build'); summary.buildSkipped = true; return; }
    await runShell('yarn build', ROOT);
    summary.buildRan = true;
}

function printSummary() {
    const distPath = join(ROOT, 'dist');
    console.log('');
    console.log(c.bold + '================================================================' + c.reset);
    console.log(c.bold + ' Installation summary' + c.reset);
    console.log(c.bold + '================================================================' + c.reset);
    console.log(' Renderer:   ' + RENDERER_DIR + (summary.rendererCloned ? '  (cloned)' : '  (already present)'));
    if (summary.configsCreated.length) console.log(' Created:    ' + summary.configsCreated.join(', '));
    if (summary.configsKept.length)    console.log(' Kept:       ' + summary.configsKept.join(', '));
    if (summary.configsPatched.length) console.log(' Patched:    ' + summary.configsPatched.join(', '));
    if (summary.jsonMode)              console.log(' JSON mode:  ' + summary.jsonMode + (summary.jsonModeSource ? '  (' + summary.jsonModeSource + ')' : ''));
    if (summary.buildRan)              console.log(' Build:      ' + c.green + 'OK' + c.reset + ' -> ' + distPath);
    else if (summary.buildSkipped)     console.log(' Build:      skipped');
    console.log('');
    console.log(' Next steps:');
    console.log('   - Development:  yarn start');
    if (summary.buildRan) {
        console.log('   - Production:   deploy the contents of ' + distPath + ' to your webserver');
    } else {
        console.log('   - Production:   yarn build, then deploy ' + distPath);
    }
    console.log(c.bold + '================================================================' + c.reset);
}

async function main() {
    const opts = parseArgs();
    if (opts.help) { printUsage(); process.exit(0); }

    console.log(c.bold + 'Nitro-V3 installer' + c.reset + ' (' + (IS_WINDOWS ? 'Windows' : platform()) + ')');
    console.log('Project root: ' + ROOT);

    step(STEPS[0]); await checkPrereqs();
    step(STEPS[1]); await cloneRenderer(opts);
    step(STEPS[2]); await setupRenderer(opts);
    step(STEPS[3]); await setupClient(opts);
    step(STEPS[4]); await copyConfigs();
    step(STEPS[5]); await chooseJsonMode(opts);
    step(STEPS[6]); await promptConfigs(opts);
    step(STEPS[7]); await runBuild(opts);
    step(STEPS[8]); printSummary();
}

process.on('SIGINT', () => {
    if (activeReadline) {
        try { activeReadline.close(); } catch {}
        activeReadline = null;
    }
    const label = STEPS[currentStep - 1] || 'startup';
    console.error('');
    warn('Aborted at step ' + currentStep + ' (' + label + ')');
    process.exit(130);
});

main().catch(e => {
    const label = STEPS[currentStep - 1] || 'startup';
    err('');
    err('Step ' + currentStep + ' (' + label + ') failed:');
    err('    ' + e.message);
    process.exit(1);
});
