/**
 * Shell service — mode interactif REPL + scripting pour le CLI Rainbow
 *
 * Lancé quand rbw est exécuté sans commande positionnelle.
 *
 * Fonctionnalités :
 *   Variables   : $var = commande  /  $var.data[0].id dans les commandes
 *   Foreach     : foreach $var.data as x: commande
 *   Scripts     : run <fichier.rbw>
 *   Built-ins   : profile  echo  vars  unset  clear  help  exit
 */

import readline from 'node:readline';
import vm       from 'node:vm';
import { spawn, execSync } from 'node:child_process';
import { join, dirname, basename } from 'node:path';
import { homedir } from 'node:os';
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
import chalk from 'chalk';
import { PrettyPrint } from '../output/pretty_print.js';

const HISTORY_FILE = join(homedir(), '.rainbow-cli', 'shell-history.json');
const HISTORY_SIZE = 500;

/**
 * Complète un chemin partiel : liste les entrées du répertoire correspondant.
 * Ajoute un '/' aux répertoires pour permettre de continuer la complétion.
 */
function _pathCompletions(partial) {
    try {
        const expanded = partial.replace(/^~(?=\/|$)/, homedir());
        const trailingSlash = expanded.endsWith('/');
        const dir  = trailingSlash ? expanded : (dirname(expanded) || '.');
        const base = trailingSlash ? '' : basename(expanded);

        const entries = readdirSync(dir || '.', { withFileTypes: true });
        return entries
            .filter(e => e.name.startsWith(base))
            .map(e => {
                // Reconstruire avec le même préfixe que l'original (relatif, absolu, ~)
                let full = join(dir, e.name);
                if (partial.startsWith('~/') || partial === '~') {
                    full = '~/' + full.slice(homedir().length + 1);
                }
                return e.isDirectory() ? full.replace(/\/?$/, '/') : full;
            })
            .sort();
    } catch {
        return [];
    }
}

export class Shell {
    constructor(program, logger, services) {
        this.program      = program;
        this.logger       = logger;
        this.services     = services;
        this._baseProfile = null;
        this._vars        = new Map(); // store de variables : nom → valeur JSON
        this._onError     = 'continue'; // 'continue' | 'stop'
    }

    // ── Démarrage interactif ──────────────────────────────────────────────────

    async start() {
        // @inquirer/core@9.x pipe une MuteStream vers process.stdout et appelle
        // output.end() après chaque prompt interactif. Le pipe déclenche
        // process.stdout.end() (dans un tick ultérieur), ce qui met stdout en état
        // "ended" → les prochains writes échouent avec ERR_STREAM_WRITE_AFTER_END.
        // Node.js protège déjà process.stdout.destroy = noop — on fait de même pour end.
        process.stdout.end = () => {};

        const applyExitOverride = (cmd) => {
            cmd.exitOverride();
            for (const sub of cmd.commands) applyExitOverride(sub);
        };
        applyExitOverride(this.program);

        const cm = this.services['configManager'];
        this._baseProfile = cm?.config?.currentProfile ?? 'default';

        const completer = (line) => {
            const trimmed   = line.trimStart();
            const firstSpace = trimmed.indexOf(' ');
            const tokens    = trimmed.split(/\s+/);
            const lastToken = tokens[tokens.length - 1] ?? '';
            const cmd       = tokens[0] ?? '';

            // Complétion de chemin : après 'run', ou si le token ressemble à un chemin
            const isRunArg  = firstSpace !== -1 && cmd === 'run';
            const isPathArg = /^[./~]/.test(lastToken);

            if (isRunArg || isPathArg) {
                const hits = _pathCompletions(lastToken);
                return [hits, lastToken];
            }

            // Complétion du nom de commande (premier token uniquement)
            if (firstSpace === -1) {
                const names = this.program.commands.map(c => c.name());
                const hits  = names.filter(n => n.startsWith(trimmed));
                return [hits.length ? hits : names, trimmed];
            }

            return [[], line];
        };

        const history = this._loadHistory();
        const rl = readline.createInterface({
            input: process.stdin, output: process.stdout,
            terminal: true, historySize: HISTORY_SIZE, completer,
        });
        if (history.length > 0) rl.history.push(...[...history].reverse());

        const prompt  = () => {
            const profile = cm?.config?.currentProfile ?? 'default';
            const status  = this._vars.get('?');
            const badge   = (status != null && status !== 0) ? chalk.red(' ✗') : '';
            return `${chalk.cyan.bold('rbw')}${chalk.dim('[' + profile + ']')}${badge} ${chalk.cyan('›')} `;
        };
        const rawMode = (on) => {
            if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function')
                process.stdin.setRawMode(on);
        };

        // Readline enregistre stdin.once('end'/'close', onend) à la création.
        // Quand inquirer laisse stdin dans un état qui émet ces events, readline ferme.
        // Solution : intercepter process.stdin.emit pour bloquer 'close'/'end'
        // sauf si on veut quitter explicitement.
        // Node.js >=20 : les méthodes internes de readline utilisent des Symbols,
        // pas des strings. 'rl._ttyWrite = () => {}' crée une propriété string sans
        // effet — readline appelle self[Symbol(_ttyWrite)]. Récupérer les vrais Symbols.
        const _rlProtoSymbols = Object.getOwnPropertySymbols(readline.Interface.prototype);
        const _kTtyWrite    = _rlProtoSymbols.find(s => s.toString() === 'Symbol(_ttyWrite)');
        const _kRefreshLine = _rlProtoSymbols.find(s => s.toString() === 'Symbol(_refreshLine)');
        const _kNormalWrite = _rlProtoSymbols.find(s => s.toString() === 'Symbol(_normalWrite)');

        let _allowStdinClose = false;
        let _inCommand = false; // vrai pendant l'exécution d'une commande interactive

        const origStdinEmit = process.stdin.emit.bind(process.stdin);
        process.stdin.emit = function(event, ...args) {
            if ((event === 'close' || event === 'end') && !_allowStdinClose) {
                // Bloquer — récupérer l'état du shell dans le tick suivant.
                // NE PAS appeler rl.resume() quand une commande est en cours :
                // resume() peut déclencher _refreshLine() qui redessine le prompt shell
                // au milieu des prompts @inquirer. @inquirer reprend stdin lui-même.
                process.nextTick(() => {
                    try { rawMode(true); } catch {}
                    try { if (rl.paused && !_inCommand) rl.resume(); } catch {}
                });
                return false;
            }
            return origStdinEmit(event, ...args);
        };

        this._rl = rl; // accessible depuis _runJsScript pour gérer Ctrl+C

        this._printBanner();
        rl.setPrompt(prompt());
        rl.prompt();

        rl.on('line', async (line) => {
            if (_inCommand) return; // garde de réentrance
            _inCommand = true;

            rl.pause();
            rawMode(true);

            // Neutraliser les handlers internes de readline via leurs vrais Symbols.
            // Node.js >=20 : les Symbols sont définis comme getters (accessors) sur le
            // prototype — une assignation directe `rl[sym] = fn` lève TypeError en mode
            // strict (ESM). Il faut Object.defineProperty pour créer une own property
            // sur l'instance qui shadow le getter du prototype.
            // Sans ces overrides, chaque frappe dans les prompts @inquirer transite par
            // le listener keypress de readline → event 'line' parasite → réexécution.
            const _noop = () => {};
            const _noopDesc = { value: _noop, configurable: true, writable: true };
            try {
                if (_kTtyWrite)    Object.defineProperty(rl, _kTtyWrite,    _noopDesc);
                if (_kRefreshLine) Object.defineProperty(rl, _kRefreshLine, _noopDesc);
                if (_kNormalWrite) Object.defineProperty(rl, _kNormalWrite, _noopDesc);
            } catch { /* silently ignore — les overrides sont best-effort */ }

            const input = line.trim();

            if (input === 'exit' || input === 'quit') {
                if (_kTtyWrite)    delete rl[_kTtyWrite];
                if (_kRefreshLine) delete rl[_kRefreshLine];
                if (_kNormalWrite) delete rl[_kNormalWrite];
                _inCommand = false;
                _allowStdinClose = true;
                this._saveHistory(rl.history);
                console.log('');
                process.exit(0);
            }

            if (input) {
                try {
                    await this._execLine(input);
                } catch (err) {
                    console.log(chalk.red(`  Error: ${err.message ?? err}`));
                }
            }

            // Supprimer les instance properties Symbol pour rétablir les méthodes prototype.
            if (_kTtyWrite)    delete rl[_kTtyWrite];
            if (_kRefreshLine) delete rl[_kRefreshLine];
            if (_kNormalWrite) delete rl[_kNormalWrite];
            _inCommand = false;
            // S'assurer que stdin reste actif et référencé dans la boucle d'événements.
            rawMode(true);
            if (typeof process.stdin.ref === 'function') process.stdin.ref();
            process.stdin.resume();
            rl.resume();
            rl.setPrompt(prompt());
            rl.prompt();
        });

        rl.on('SIGINT', () => { process.stdout.write('\n'); rl.setPrompt(prompt()); rl.prompt(); });
        rl.on('close',  () => {
            // Ne se déclenche normalement que pour Ctrl+D ou exit explicite
            _allowStdinClose = true;
            this._saveHistory(rl.history); console.log(''); process.exit(0);
        });
    }

    // ── Dispatcher principal ──────────────────────────────────────────────────
    // Utilisé en mode interactif ET dans le runner de scripts.

    async _execLine(input) {
        if (!input || input.startsWith('#')) return;

        // ── Built-ins ────────────────────────────────────────────────────────

        if (input === 'clear' || input === 'cls') {
            process.stdout.write('\x1Bc'); return;
        }

        // Commandes TUI (blessed) incompatibles avec le mode shell — elles prennent
        // le contrôle complet du terminal et entrent en conflit avec readline.
        const TUI_PREFIXES = [
            'api-docs browse', 'guides browse', 'docs browse',
            'webhook listen',  'ucs listen',
        ];
        const isTui = TUI_PREFIXES.some(p => input === p || input.startsWith(p + ' '))
                   || /(?:^|\s)(-i|--interactive)(?:\s|$)/.test(input);
        if (isTui) {
            console.log(chalk.yellow(`  ⚠  Cette commande TUI n'est pas disponible en mode shell.`));
            console.log(chalk.dim(`  Lancez-la directement : rbw ${input}`));
            return;
        }

        if (input === 'help' || input === '?') {
            try { await this.program.parseAsync(['node', 'rbw', '--help']); } catch { }
            return;
        }

        if (input === 'vars') {
            this._printVars(); return;
        }

        const profileMatch = input.match(/^profile\s+(\S+)$/);
        if (profileMatch) {
            const cm = this.services['configManager'];
            try {
                cm.useTemporaryProfile(profileMatch[1]);
                this._baseProfile = profileMatch[1];
                console.log(chalk.green(`  ✓ Profile switched to "${profileMatch[1]}"`));
            } catch (err) { console.log(chalk.red(`  ${err.message}`)); }
            return;
        }

        if (input.startsWith('echo') && (input.length === 4 || input[4] === ' ')) {
            const raw = input.slice(4).trim().replace(/^(["'])(.*)\1$/, '$2');
            console.log('  ' + this._interpolate(raw));
            return;
        }

        const unsetMatch = input.match(/^unset\s+\$([a-zA-Z_]\w*)$/);
        if (unsetMatch) {
            this._vars.delete(unsetMatch[1]);
            console.log(chalk.dim(`  $${unsetMatch[1]} supprimé`));
            return;
        }

        if (input.startsWith('run ')) {
            await this._runScript(input.slice(4).trim()); return;
        }

        // Commande OS : !<cmd>  (ex: !ls -la  !mkdir ./rapports  !cat file.txt)
        if (input.startsWith('!')) {
            const cmd = input.slice(1).trim();
            if (!cmd) {
                console.log(chalk.dim('  Usage: !<cmd>   ex: !ls -la   !mkdir ./rapports'));
                return;
            }
            await this._execShell(cmd);
            return;
        }

        // on-error stop | continue
        const onErrorMatch = input.match(/^on-error\s+(stop|continue)$/);
        if (onErrorMatch) {
            this._onError = onErrorMatch[1];
            console.log(chalk.dim(`  on-error: ${this._onError}`));
            return;
        }

        // if <condition>: <commande>
        const ifMatch = input.match(/^if\s+(.+?)\s*:\s*(.+)$/);
        if (ifMatch) {
            const [, condition, cmd] = ifMatch;
            if (this._evalCondition(condition)) {
                await this._execCommand(this._interpolate(cmd));
            }
            return;
        }

        // ── Scripting ────────────────────────────────────────────────────────

        // $var = commande  →  capture du résultat
        const assignMatch = input.match(/^\$([a-zA-Z_]\w*)\s*=\s*(.+)$/);
        if (assignMatch) {
            const [, varName, cmd] = assignMatch;
            const result = await this._execCommand(this._interpolate(cmd));
            if (result != null) {
                this._vars.set(varName, result);
                console.log(chalk.dim(`  → $${varName} = ${this._preview(result)}`));
            }
            return;
        }

        // foreach <expr> as <var>: <commande>
        const foreachMatch = input.match(/^foreach\s+(\S+)\s+as\s+([a-zA-Z_]\w*)\s*:\s*(.+)$/);
        if (foreachMatch) {
            await this._execForeach(foreachMatch[1], foreachMatch[2], foreachMatch[3]); return;
        }

        // Commande normale (avec interpolation des variables)
        await this._execCommand(this._interpolate(input));
    }

    // ── Exécution d'une commande via Commander ────────────────────────────────

    async _execCommand(input) {
        if (!input) return null;
        const tokens = this._tokenize(input);
        const cm     = this.services['configManager'];
        this.services['_shellMode'] = true;
        PrettyPrint._lastStatus = 0; // réinitialiser avant la commande
        let result = null;
        try {
            await this.program.parseAsync(['node', 'rbw', ...tokens]);
            result = await PrettyPrint._lastSpinner.catch(() => null);
        } catch (err) {
            PrettyPrint._lastStatus = 1;
            const code = err.code ?? '';
            if (code === 'commander.helpDisplayed' || code === 'commander.version') {
                PrettyPrint._lastStatus = 0; // aide affichée = pas une erreur
            } else if (code === 'commander.unknownCommand') {
                console.log(chalk.red(`  Unknown command: "${tokens[0]}"`));
                console.log(chalk.dim('  Type "help" to list available commands'));
            } else if (code.startsWith('commander.')) {
                console.log(chalk.red(`  ${err.message}`));
            } else if (err.message) {
                console.log(chalk.red(`  Error: ${err.message}`));
            }
        } finally {
            this.services['_shellMode'] = false;
            if (cm) {
                if (this.services['_shellProfileOverride']) {
                    // --profile utilisé pour cette commande → restaurer le profil de base
                    this.services['_shellProfileOverride'] = false;
                    cm._temporaryProfile     = this._baseProfile;
                    cm.config.currentProfile = this._baseProfile;
                } else if (cm.config.currentProfile !== this._baseProfile) {
                    // Le profil a changé via une commande (ex: config set-profile)
                    // → accepter le changement et mettre à jour le profil de base de la session
                    this._baseProfile = cm.config.currentProfile;
                }
            }
        }
        // $? = code de retour de la commande (0 succès, 1 échec)
        this._vars.set('?', PrettyPrint._lastStatus);
        return result;
    }

    // ── Foreach ───────────────────────────────────────────────────────────────

    async _execForeach(expr, varName, cmd) {
        const items = this._resolveExpr(expr);
        if (!Array.isArray(items)) {
            console.log(chalk.red(`  foreach: "${expr}" n'est pas un tableau (type: ${typeof items})`));
            if (items != null)
                console.log(chalk.dim(`  Valeur : ${this._preview(items)}`));
            return;
        }
        if (items.length === 0) {
            console.log(chalk.dim('  foreach: tableau vide')); return;
        }
        console.log(chalk.dim(`  foreach: ${items.length} élément(s)\n`));
        for (let i = 0; i < items.length; i++) {
            this._vars.set(varName, items[i]);
            const expanded = this._interpolate(cmd);
            console.log(chalk.dim(`  [${i + 1}/${items.length}] `) + chalk.white(expanded));
            await this._execCommand(expanded);
            if (this._onError === 'stop' && this._vars.get('?') !== 0) {
                console.log(chalk.yellow(`  foreach: arrêt [${i + 1}/${items.length}] — on-error stop ($? = ${this._vars.get('?')})`));
                break;
            }
        }
        // La variable de boucle reste disponible après (dernier élément)
    }

    // ── Runner de scripts ─────────────────────────────────────────────────────

    async _runScript(filename) {
        if (/\.js$/i.test(filename)) {
            await this._runJsScript(filename); return;
        }
        // ── Runner .rbw (mini-langage) ────────────────────────────────────────
        let content;
        try {
            content = readFileSync(filename, 'utf-8');
        } catch (err) {
            console.log(chalk.red(`  Impossible de lire "${filename}": ${err.message}`)); return;
        }
        const lines    = content.split('\n');
        const commands = lines.filter(l => l.trim() && !l.trim().startsWith('#'));
        console.log(chalk.bold(`\n  Script "${filename}" — ${commands.length} commande(s)\n`));

        for (const line of lines) {
            const l = line.trim();
            if (!l || l.startsWith('#')) continue;
            console.log(chalk.cyan('  › ') + chalk.white(l));
            await this._execLine(l);
            if (this._onError === 'stop' && this._vars.get('?') !== 0) {
                console.log(chalk.yellow(`  script: arrêt sur erreur — on-error stop ($? = ${this._vars.get('?')})`));
                break;
            }
        }
        console.log(chalk.dim('\n  Script terminé.'));
    }

    // ── Runner de scripts JavaScript (.js) ───────────────────────────────────

    async _runJsScript(filename) {
        let code;
        try {
            code = readFileSync(filename, 'utf-8');
        } catch (err) {
            console.log(chalk.red(`  Impossible de lire "${filename}": ${err.message}`)); return;
        }

        const self = this;
        const cm   = this.services['configManager'];
        let _aborted = false;

        // Ctrl+C : interrompre le script proprement sans quitter le shell.
        //
        // Problème : rl.pause() coupe les data events → rl.on('SIGINT') ne se
        // déclenche jamais pendant un script. Solution : override rl._ttyWrite
        // (le handler interne que readline appelle pour chaque touche) + stdin.resume()
        // pour que les data events circulent. On intercept \x03 (Ctrl+C) et on mange
        // les autres touches pour éviter toute saisie parasite pendant le script.
        // En mode headless (rbw run), on utilise process.on('SIGINT') classique.
        const rl = this._rl;
        const onAbort = () => {
            _aborted = true;
            process.stdout.write('^C\n');
            console.log(chalk.yellow('  Ctrl+C — script interrompu après la commande en cours…'));
        };

        let origTtyWrite = null;
        if (rl) {
            origTtyWrite = rl._ttyWrite?.bind(rl);
            rl._ttyWrite = (_s, key) => {
                if (key?.ctrl && key?.name === 'c') onAbort();
                // Manger toutes les autres touches pendant le script
            };
            process.stdin.resume(); // réactiver les data events (coupés par rl.pause())
        } else {
            process.once('SIGINT', onAbort);
        }

        // Pré-remplir les variables d'environnement utiles
        this._vars.set('profile', cm?.config?.currentProfile ?? 'default');
        if (cm?.config?.profiles?.[cm.config.currentProfile]?.host) {
            this._vars.set('host', cm.config.profiles[cm.config.currentProfile].host);
        }

        // Contexte injecté dans le script VM
        const ctx = {
            // ── Fonction principale ──────────────────────────────────────────
            /**
             * Exécute une commande CLI et retourne le résultat JSON.
             * Options : { silent: true } pour supprimer l'affichage.
             * Raccourci : runSilent(cmd) ≡ run(cmd, { silent: true })
             */
            /**
             * Exécute une commande, retourne le résultat JSON.
             * Silencieux par défaut (pas d'affichage) — utiliser runVerbose() pour voir la sortie.
             * Options : { verbose: true } pour afficher les résultats.
             */
            run: async (cmd, opts = {}) => {
                if (_aborted) throw Object.assign(new Error('Script interrompu par Ctrl+C'), { _rbwStop: true });
                const line    = String(cmd).trim();
                const verbose = opts?.verbose === true;
                if (!line) return null;

                if (verbose) console.log(chalk.cyan('  › ') + chalk.white(line));

                let result;
                if (!verbose) {
                    // Mode silencieux : supprimer stdout (display, table...) sauf erreurs
                    const origWrite = process.stdout.write.bind(process.stdout);
                    process.stdout.write = () => true;
                    try {
                        result = await self._execCommand(line);
                    } finally {
                        process.stdout.write = origWrite;
                    }
                } else {
                    result = await self._execCommand(line);
                }

                ctx.status = self._vars.get('?') ?? 0;
                ctx.ok     = ctx.status === 0;
                return result;
            },

            /** Exécute une commande en affichant › cmd et les résultats. */
            runVerbose: async (cmd) => ctx.run(cmd, { verbose: true }),
            /** Alias explicite pour run() silencieux (comportement par défaut). */
            runSilent:  async (cmd) => ctx.run(cmd),

            // ── Statut de la dernière commande ────────────────────────────────
            status: 0,   // 0 = succès, 1 = échec
            ok:     true,

            // ── Sortie ────────────────────────────────────────────────────────
            log:  (...a) => console.log(' ', ...a),
            echo: (...a) => console.log(' ', ...a),

            // ── Contrôle de la session ────────────────────────────────────────
            /** Change le profil actif pour la session. */
            profile: (name) => {
                try {
                    cm.useTemporaryProfile(name);
                    self._baseProfile = name;
                    self._vars.set('profile', name);
                    if (cm?.config?.profiles?.[name]?.host)
                        self._vars.set('host', cm.config.profiles[name].host);
                    console.log(chalk.green(`  ✓ Profile: "${name}"`));
                } catch (err) { console.log(chalk.red(`  ${err.message}`)); }
            },

            /** Définit le comportement sur erreur : 'stop' ou 'continue'. */
            onError: (mode) => { self._onError = mode; },

            /**
             * Exécute une commande OS et retourne sa sortie (stdout).
             * Contrairement à run(), exécute du shell natif (ls, mkdir, cat…).
             * Non interactif — utilise !cmd en mode shell pour les commandes interactives.
             */
            shell: (cmd) => {
                try {
                    const out = execSync(cmd, { encoding: 'utf-8', env: process.env,
                                               stdio: ['inherit', 'pipe', 'pipe'] });
                    ctx.status = 0; ctx.ok = true;
                    return out;
                } catch (err) {
                    if (err.stderr) process.stderr.write(err.stderr);
                    ctx.status = err.status ?? 1; ctx.ok = false;
                    return null;
                }
            },

            /** Arrête le script proprement (comme un return en haut niveau). */
            stop: (msg) => {
                if (msg) console.log(chalk.yellow(`  Stop: ${msg}`));
                throw Object.assign(new Error(msg ?? 'stop'), { _rbwStop: true });
            },

            /** Accès au store de variables du shell (partagé avec la session). */
            vars: this._vars,

            // ── Globals JS utiles ─────────────────────────────────────────────
            console, JSON, Math,
            parseInt, parseFloat, isNaN, isFinite,
            setTimeout, clearTimeout, setInterval, clearInterval,
            Promise, Error,
        };

        vm.createContext(ctx);
        console.log(chalk.bold(`\n  Script JS "${filename}"\n`));

        // Désactiver le spinner pour toute la durée du script (inutile en scripting)
        PrettyPrint._silentSpinner = true;
        try {
            // Enveloppe async → top-level await natif dans le script
            const wrapped = `(async () => {\n${code}\n})()`;
            const script  = new vm.Script(wrapped, { filename, lineOffset: -1 });
            await Promise.resolve(script.runInContext(ctx));
        } catch (err) {
            if (!err._rbwStop) {
                console.log(chalk.red(`\n  Erreur: ${err.message}`));
                const relevant = (err.stack ?? '').split('\n')
                    .filter(l => l.includes(filename) || l.includes('<anonymous>'))
                    .slice(0, 2);
                relevant.forEach(l => console.log(chalk.dim('  ' + l.trim())));
            }
        } finally {
            PrettyPrint._silentSpinner = false;
            if (rl) {
                // Restaurer _ttyWrite et re-pauser stdin
                // (rl.resume() dans le handler 'line' le rouvrira proprement ensuite)
                if (origTtyWrite) rl._ttyWrite = origTtyWrite;
                process.stdin.pause();
            } else {
                process.removeListener('SIGINT', onAbort);
            }
        }

        console.log(chalk.dim('\n  Script JS terminé.'));
    }

    // ── Interpolation de variables ────────────────────────────────────────────

    // ── Commandes OS (mode shell interactif) ─────────────────────────────────

    _execShell(cmd) {
        return new Promise((resolve) => {
            // Remettre le terminal en mode normal pour que le processus fils
            // puisse l'utiliser proprement (important pour vim, less, etc.)
            try { process.stdin.setRawMode(false); } catch {}

            const proc = spawn('sh', ['-c', cmd], {
                stdio: 'inherit',
                env:   process.env,
            });

            proc.on('close', (code) => {
                this._vars.set('?', code ?? 0);
                resolve();
            });

            proc.on('error', (err) => {
                console.error(chalk.red(`  Shell error: ${err.message}`));
                this._vars.set('?', 1);
                resolve();
            });
        });
    }

    /** Remplace $var, $var.field, $var.data[0].id, ${var.field}, $? dans une chaîne.
     *  Backtracking : $d.email.pdf → résout $d.email puis conserve .pdf comme suffixe. */
    _interpolate(str) {
        // 0. $? — variable spéciale (code de retour, ? n'est pas un \w)
        str = str.replace(/\$\?/g, () => {
            const v = this._vars.get('?');
            return v != null ? String(v) : '$?';
        });

        // 1. Syntaxe ${...} — explicite et non-ambiguë
        str = str.replace(/\$\{([a-zA-Z_]\w*)((?:\.\w+|\[\d+\])*)\}/g, (match, varName, path) => {
            if (!this._vars.has(varName)) return match;
            const val      = this._vars.get(varName);
            const resolved = path ? this._resolvePath(val, path.slice(1)) : val;
            if (resolved == null) return match;
            return typeof resolved === 'object' ? JSON.stringify(resolved) : String(resolved);
        });

        // 2. Syntaxe $var ou $var.path avec backtracking sur le plus long préfixe résolvable
        return str.replace(/\$([a-zA-Z_]\w*)((?:\.\w+|\[\d+\])*)/g, (match, varName, path) => {
            if (!this._vars.has(varName)) return match;
            const val = this._vars.get(varName);

            if (!path) {
                if (val == null) return match;
                return typeof val === 'object' ? JSON.stringify(val) : String(val);
            }

            // Essayer du plus long au plus court jusqu'à obtenir une résolution non-null
            const segments = path.match(/(?:\.\w+|\[\d+\])/g) || [];
            for (let len = segments.length; len >= 1; len--) {
                const tryPath  = segments.slice(0, len).join('').slice(1); // retirer le . initial
                const resolved = this._resolvePath(val, tryPath);
                if (resolved != null) {
                    const suffix   = segments.slice(len).join(''); // ex: ".pdf" conservé tel quel
                    const replaced = typeof resolved === 'object' ? JSON.stringify(resolved) : String(resolved);
                    return replaced + suffix;
                }
            }
            return match; // rien résolu, conserver l'original
        });
    }

    /** Résout une expression comme $var ou $var.data[0].id */
    _resolveExpr(expr) {
        const m = expr.match(/^\$([a-zA-Z_]\w*)((?:\.\w+|\[\d+\])*)$/);
        if (!m) return undefined;
        const [, varName, path] = m;
        if (!this._vars.has(varName)) return undefined;
        return path
            ? this._resolvePath(this._vars.get(varName), path.slice(1))
            : this._vars.get(varName);
    }

    /** Évalue une condition : "$? = 0", "$var != valeur", "$n > 5" … */
    _evalCondition(cond) {
        const m = cond.trim().match(/^(.+?)\s*(!=|>=|<=|=|>|<)\s*(.+)$/);
        if (!m) return false;
        const lv = this._interpolate(m[1].trim());
        const rv = m[3].trim().replace(/^(["'])(.*)\1$/, '$2'); // retirer guillemets optionnels
        const ln = Number(lv), rn = Number(rv);
        const num = !isNaN(ln) && !isNaN(rn) && lv !== '' && rv !== '';
        switch (m[2]) {
            case '=':  return num ? ln === rn : lv === rv;
            case '!=': return num ? ln !== rn : lv !== rv;
            case '>':  return num && ln > rn;
            case '<':  return num && ln < rn;
            case '>=': return num && ln >= rn;
            case '<=': return num && ln <= rn;
        }
        return false;
    }

    /** Accès par chemin : "data.items[0].name" → valeur */
    _resolvePath(obj, path) {
        if (!path) return obj;
        const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
        let cur = obj;
        for (const p of parts) {
            if (cur == null) return undefined;
            cur = cur[p];
        }
        return cur;
    }

    // ── Affichage variables ───────────────────────────────────────────────────

    _printVars() {
        if (this._vars.size === 0) {
            console.log(chalk.dim('\n  Aucune variable définie\n')); return;
        }
        console.log('');
        for (const [name, val] of this._vars) {
            console.log(`  ${chalk.cyan.bold('$' + name)}`);
            if (typeof val === 'object' && val !== null) {
                // Afficher les clés de premier niveau avec aperçu
                for (const [k, v] of Object.entries(val)) {
                    const preview = Array.isArray(v) ? `[${v.length} items]`
                        : typeof v === 'object' && v !== null ? '{…}'
                        : String(v ?? '');
                    console.log(`    ${chalk.dim('.' + k.padEnd(18))} ${chalk.white(String(preview).slice(0, 60))}`);
                }
            } else {
                console.log(`    ${chalk.dim(String(val ?? ''))}`);
            }
            console.log('');
        }
    }

    _preview(val) {
        if (val == null) return 'null';
        const s = typeof val === 'object' ? JSON.stringify(val) : String(val);
        return s.length > 80 ? s.slice(0, 77) + '…' : s;
    }

    // ── Bannière ──────────────────────────────────────────────────────────────

    _printBanner() {
        const profile = this.services['configManager']?.config?.currentProfile ?? 'default';
        const version = this.program._version ?? '';
        const dim = chalk.dim, w = chalk.white;
        console.log('');
        console.log(chalk.bold.cyan('  Rainbow CLI NG') + (version ? chalk.dim(` v${version}`) : '') + dim(' — shell mode'));
        console.log(dim(`  Profile  : ${profile}`));
        console.log('');
        console.log(dim('  Commands  : any rbw command without the "rbw" prefix'));
        console.log(dim('  Variables : ') + w('$me = whoami') + dim('  →  ') + w('company get $me.data.companyId'));
        console.log(dim('  Foreach   : ') + w('foreach $users.data as u: user:context $u.id'));
        console.log(dim('  Conditions: ') + w('if $? = 0: echo ok') + dim('  |  ') + w('if $? != 0: echo failed'));
        console.log(dim('  Scripts   : ') + w('run script.rbw / script.js') + dim('  |  on-error stop|continue'));
        console.log(dim('  JS API    : ') + w('run(cmd)') + dim(' silent  ') + w('runVerbose(cmd)') + dim(' with output'));
        console.log(dim('  OS shell  : ') + w('!ls -la') + dim('  !mkdir ./out  !cat file  (tout préfixe !)'));
        console.log(dim('  Built-ins : profile  echo  vars  unset $v  on-error  clear  help  exit'));
        console.log(dim('  Tab: auto-complete  ↑↓: history  Ctrl+D: exit'));
        console.log('');
    }

    // ── Tokenizer (gère les guillemets) ───────────────────────────────────────

    _tokenize(line) {
        const tokens = [];
        let current = '', quote = null;
        for (const ch of line) {
            if (quote) {
                if (ch === quote) quote = null; else current += ch;
            } else if (ch === '"' || ch === "'") {
                quote = ch;
            } else if (ch === ' ' || ch === '\t') {
                if (current) { tokens.push(current); current = ''; }
            } else { current += ch; }
        }
        if (current) tokens.push(current);
        return tokens;
    }

    // ── Historique persistant ─────────────────────────────────────────────────

    _loadHistory() {
        try { return JSON.parse(readFileSync(HISTORY_FILE, 'utf-8')); } catch { return []; }
    }

    _saveHistory(rlHistory) {
        try {
            mkdirSync(join(homedir(), '.rainbow-cli'), { recursive: true });
            writeFileSync(HISTORY_FILE,
                JSON.stringify([...rlHistory].reverse().slice(-HISTORY_SIZE), null, 2), 'utf-8');
        } catch { /* ignore */ }
    }
}
