// ==========================================
// Chat Switchboard – REPL Console
// ==========================================
// Fourth tab in the debug modal (Ctrl+Shift+L).
// Evaluates JavaScript with top-level await support
// via AsyncFunction. Injects app globals for introspection.
//
// Features:
//   - AsyncFunction wrapper (top-level await)
//   - Injected globals: sw, DebugLog (v0.37.14: old globals removed)
//   - Pretty-print results (collapsible JSON, red errors with stack)
//   - Command history: ↑/↓ with sessionStorage
//   - Tab-completion on live object graphs
//   - Event label hints on Events.on('  / Events.emit('
//   - Multi-line: Shift+Enter for newline, Enter to run
//   - Admin-gated OR ?debug=1 URL param
// ==========================================
//
// Exports: window.REPL


const REPL = {

    // ── State ────────────────────────────────
    _history: [],
    _historyIndex: -1,
    _maxHistory: 100,
    _storageKey: 'cs::repl::history',
    _initialized: false,

    // ── Initialization ───────────────────────

    init() {
        if (this._initialized) return;
        this._initialized = true;

        // Restore history from sessionStorage
        try {
            const saved = sessionStorage.getItem(this._storageKey);
            if (saved) this._history = JSON.parse(saved);
        } catch { /* ignore */ }

        const input = document.getElementById('replInput');
        if (!input) return;

        input.addEventListener('keydown', (e) => this._onKeyDown(e));
        input.addEventListener('input', () => this._autoResize(input));

        // Gate: hide REPL tab if not admin and no ?debug=1
        this._applyGate();
    },

    /**
     * Hide the REPL tab unless user is admin or ?debug=1 is set.
     */
    _applyGate() {
        const tab = document.querySelector('.debug-tab[data-tab="repl"]');
        if (!tab) return;

        const isAdmin = window.sw?.auth?.user?.role === 'admin';
        const debugParam = new URLSearchParams(window.location.search).has('debug');

        if (!isAdmin && !debugParam) {
            tab.style.display = 'none';
        } else {
            tab.style.display = '';
        }
    },

    // ── Execution ────────────────────────────

    /**
     * Evaluate an expression with top-level await support.
     * Injects app globals as local variables.
     */
    async run(code) {
        if (!code.trim()) return;

        // Add to history
        this._history.push(code);
        if (this._history.length > this._maxHistory) this._history.shift();
        this._historyIndex = -1;
        this._saveHistory();

        // Render input
        this._appendInput(code);

        const startTime = performance.now();

        try {
            // Build an AsyncFunction with injected globals.
            // The last expression is auto-returned if no explicit return.
            const wrappedCode = this._wrapCode(code);

            // Injected globals accessible inside the REPL
            const globals = this._getGlobals();
            const argNames = Object.keys(globals);
            const argValues = Object.values(globals);

            // eslint-disable-next-line no-new-func
            const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
            const fn = new AsyncFunction(...argNames, wrappedCode);

            const result = await fn(...argValues);
            const elapsed = (performance.now() - startTime).toFixed(1);

            if (result !== undefined) {
                this._appendResult(result, elapsed);
            } else {
                this._appendInfo(`(undefined) [${elapsed}ms]`);
            }
        } catch (err) {
            this._appendError(err);
        }

        this._scrollToBottom();
    },

    /**
     * Wrap code for AsyncFunction evaluation.
     * If the code is a single expression (no semicolons, no let/const/var/return),
     * auto-return it. Otherwise, execute as-is.
     */
    _wrapCode(code) {
        const trimmed = code.trim();

        // Multi-statement: check for declarations or multiple statements
        if (/^(let |const |var |return |if |for |while |switch |try |class |function )/.test(trimmed)) {
            return trimmed;
        }

        // Single expression — auto-return
        // Handle edge case: object literals starting with { need wrapping
        if (trimmed.startsWith('{') && !trimmed.startsWith('{(')) {
            return `return (${trimmed})`;
        }

        return `return (${trimmed})`;
    },

    /**
     * Get the set of globals injected into REPL scope.
     */
    _getGlobals() {
        const globals = {};

        // Preact SDK (v0.37.14: replaces old API/Events/Extensions globals)
        if (window.sw) globals.sw = window.sw;
        if (typeof DebugLog !== 'undefined') globals.DebugLog = DebugLog;

        // Convenience aliases
        globals.$ = (sel) => document.querySelector(sel);
        globals.$$ = (sel) => [...document.querySelectorAll(sel)];
        globals.sleep = (ms) => new Promise(r => setTimeout(r, ms));

        return globals;
    },

    // ── Key Handling ─────────────────────────

    _onKeyDown(e) {
        const input = e.target;

        // Enter (without Shift) → execute
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            this.run(input.value);
            input.value = '';
            this._autoResize(input);
            return;
        }

        // ↑ / ↓ → history navigation (only when cursor is on first/last line)
        if (e.key === 'ArrowUp' && this._isCursorOnFirstLine(input)) {
            e.preventDefault();
            this._navigateHistory(-1, input);
            return;
        }
        if (e.key === 'ArrowDown' && this._isCursorOnLastLine(input)) {
            e.preventDefault();
            this._navigateHistory(1, input);
            return;
        }

        // Tab → completion
        if (e.key === 'Tab') {
            e.preventDefault();
            this._tabComplete(input);
            return;
        }
    },

    _isCursorOnFirstLine(input) {
        return input.value.substring(0, input.selectionStart).indexOf('\n') === -1;
    },

    _isCursorOnLastLine(input) {
        return input.value.substring(input.selectionStart).indexOf('\n') === -1;
    },

    _autoResize(input) {
        input.style.height = 'auto';
        input.style.height = Math.min(input.scrollHeight, 120) + 'px';
    },

    // ── History ──────────────────────────────

    _navigateHistory(direction, input) {
        if (this._history.length === 0) return;

        if (this._historyIndex === -1) {
            // Starting navigation — save current input
            this._historyScratch = input.value;
            this._historyIndex = this._history.length;
        }

        this._historyIndex += direction;

        if (this._historyIndex < 0) this._historyIndex = 0;
        if (this._historyIndex >= this._history.length) {
            // Past end → restore scratch
            this._historyIndex = -1;
            input.value = this._historyScratch || '';
        } else {
            input.value = this._history[this._historyIndex];
        }

        this._autoResize(input);
        // Move cursor to end
        input.selectionStart = input.selectionEnd = input.value.length;
    },

    _saveHistory() {
        try {
            sessionStorage.setItem(this._storageKey, JSON.stringify(this._history));
        } catch { /* quota exceeded — ignore */ }
    },

    // ── Tab Completion ──────────────────────

    _tabComplete(input) {
        const cursor = input.selectionStart;
        const text = input.value.substring(0, cursor);

        // Object property completion: obj.prop or obj.prop.sub
        const propMatch = text.match(/([\w$.]+)\.([\w]*)$/);
        if (propMatch) {
            this._completeProperty(input, cursor, propMatch[1], propMatch[2]);
            return;
        }

        // Top-level global completion
        const wordMatch = text.match(/([\w$]+)$/);
        if (wordMatch) {
            this._completeGlobal(input, cursor, wordMatch[1]);
        }
    },

    _completeProperty(input, cursor, objExpr, partial) {
        try {
            // Resolve the object in global scope
            const globals = this._getGlobals();
            // eslint-disable-next-line no-new-func
            const obj = new Function(...Object.keys(globals), `return ${objExpr}`)(...Object.values(globals));
            if (obj == null) return;

            const keys = [];
            // Own + prototype properties
            for (let o = obj; o && o !== Object.prototype; o = Object.getPrototypeOf(o)) {
                for (const k of Object.getOwnPropertyNames(o)) {
                    if (k.startsWith(partial) && !k.startsWith('_')) keys.push(k);
                }
            }
            const unique = [...new Set(keys)].sort();

            if (unique.length === 0) return;
            if (unique.length === 1) {
                this._insertCompletion(input, cursor, partial, unique[0]);
            } else {
                this._appendInfo('Completions: ' + unique.slice(0, 30).join(', ') + (unique.length > 30 ? ' …' : ''));
                const common = this._commonPrefix(unique);
                if (common.length > partial.length) {
                    this._insertCompletion(input, cursor, partial, common);
                }
            }
        } catch { /* can't resolve — ignore */ }
    },

    _completeGlobal(input, cursor, partial) {
        const globals = Object.keys(this._getGlobals());
        // Add common window globals
        const candidates = [...globals, 'document', 'window', 'console', 'fetch', 'JSON', 'Math',
                            'Array', 'Object', 'String', 'Number', 'Boolean', 'Promise', 'Map', 'Set'];
        const matches = [...new Set(candidates)].filter(c => c.startsWith(partial)).sort();

        if (matches.length === 0) return;
        if (matches.length === 1) {
            this._insertCompletion(input, cursor, partial, matches[0]);
        } else {
            this._appendInfo('Completions: ' + matches.join(', '));
            const common = this._commonPrefix(matches);
            if (common.length > partial.length) {
                this._insertCompletion(input, cursor, partial, common);
            }
        }
    },

    _insertCompletion(input, cursor, partial, completion) {
        const before = input.value.substring(0, cursor - partial.length);
        const after = input.value.substring(cursor);
        input.value = before + completion + after;
        const newPos = before.length + completion.length;
        input.selectionStart = input.selectionEnd = newPos;
    },

    _commonPrefix(strs) {
        if (strs.length === 0) return '';
        let prefix = strs[0];
        for (let i = 1; i < strs.length; i++) {
            while (!strs[i].startsWith(prefix)) {
                prefix = prefix.slice(0, -1);
            }
        }
        return prefix;
    },

    // ── Output Rendering ────────────────────

    _appendInput(code) {
        const output = document.getElementById('replOutput');
        if (!output) return;

        const div = document.createElement('div');
        div.className = 'repl-entry repl-entry-input';

        const pre = document.createElement('pre');
        pre.textContent = '> ' + code;
        div.appendChild(pre);
        output.appendChild(div);
    },

    _appendResult(value, elapsed) {
        const output = document.getElementById('replOutput');
        if (!output) return;

        const div = document.createElement('div');
        div.className = 'repl-entry repl-entry-result';

        const formatted = this._formatValue(value);
        div.appendChild(formatted);

        if (elapsed) {
            const time = document.createElement('span');
            time.className = 'repl-elapsed';
            time.textContent = `[${elapsed}ms]`;
            div.appendChild(time);
        }

        output.appendChild(div);
    },

    _appendError(err) {
        const output = document.getElementById('replOutput');
        if (!output) return;

        const div = document.createElement('div');
        div.className = 'repl-entry repl-entry-error';

        const msg = document.createElement('pre');
        msg.className = 'repl-error-msg';
        msg.textContent = err.message || String(err);
        div.appendChild(msg);

        if (err.stack) {
            const stack = document.createElement('pre');
            stack.className = 'repl-error-stack';
            stack.textContent = err.stack.split('\n').slice(1, 5).join('\n');
            stack.style.display = 'none';

            const toggle = document.createElement('button');
            toggle.className = 'repl-stack-toggle';
            toggle.textContent = '▸ stack';
            toggle.onclick = () => {
                const shown = stack.style.display !== 'none';
                stack.style.display = shown ? 'none' : '';
                toggle.textContent = shown ? '▸ stack' : '▾ stack';
            };

            div.appendChild(toggle);
            div.appendChild(stack);
        }

        output.appendChild(div);
    },

    _appendInfo(text) {
        const output = document.getElementById('replOutput');
        if (!output) return;

        const div = document.createElement('div');
        div.className = 'repl-entry repl-entry-info';
        div.textContent = text;
        output.appendChild(div);
    },

    /**
     * Format a JS value for display.
     * Objects/arrays get collapsible JSON; primitives get plain text.
     */
    _formatValue(value) {
        if (value === null) return this._textEl('null', 'repl-null');
        if (value === undefined) return this._textEl('undefined', 'repl-undefined');

        const type = typeof value;

        if (type === 'string') return this._textEl(JSON.stringify(value), 'repl-string');
        if (type === 'number' || type === 'bigint') return this._textEl(String(value), 'repl-number');
        if (type === 'boolean') return this._textEl(String(value), 'repl-boolean');
        if (type === 'function') return this._textEl(`[Function: ${value.name || 'anonymous'}]`, 'repl-function');
        if (type === 'symbol') return this._textEl(String(value), 'repl-symbol');

        if (value instanceof HTMLElement) {
            return this._textEl(`<${value.tagName.toLowerCase()}${value.id ? '#' + value.id : ''}${value.className ? '.' + value.className.split(' ').join('.') : ''}>`, 'repl-dom');
        }

        if (value instanceof Error) {
            return this._textEl(`${value.constructor.name}: ${value.message}`, 'repl-error-msg');
        }

        // Objects and arrays → collapsible JSON
        try {
            const json = JSON.stringify(value, null, 2);
            if (json.length < 200) {
                return this._textEl(json, 'repl-json');
            }
            return this._collapsibleJson(json, value);
        } catch {
            return this._textEl(String(value), 'repl-object');
        }
    },

    _textEl(text, className) {
        const pre = document.createElement('pre');
        pre.className = className || '';
        pre.textContent = text;
        return pre;
    },

    _collapsibleJson(json, value) {
        const container = document.createElement('div');
        container.className = 'repl-collapsible';

        const summary = Array.isArray(value)
            ? `Array(${value.length})`
            : `Object {${Object.keys(value).slice(0, 3).join(', ')}${Object.keys(value).length > 3 ? ', …' : ''}}`;

        const toggle = document.createElement('button');
        toggle.className = 'repl-json-toggle';
        toggle.textContent = '▸ ' + summary;

        const pre = document.createElement('pre');
        pre.className = 'repl-json';
        pre.textContent = json;
        pre.style.display = 'none';

        toggle.onclick = () => {
            const shown = pre.style.display !== 'none';
            pre.style.display = shown ? 'none' : '';
            toggle.textContent = (shown ? '▸ ' : '▾ ') + summary;
        };

        container.appendChild(toggle);
        container.appendChild(pre);
        return container;
    },

    _scrollToBottom() {
        const output = document.getElementById('replOutput');
        if (output) output.scrollTop = output.scrollHeight;
    },

    // ── Toolbar Actions ──────────────────────

    clear() {
        const output = document.getElementById('replOutput');
        if (output) output.innerHTML = '';
    },

    copyOutput() {
        const output = document.getElementById('replOutput');
        if (!output) return;
        const text = output.innerText;
        navigator.clipboard.writeText(text)
            .then(() => { if (typeof showToast === 'function') showToast('📋 REPL output copied', 'success'); })
            .catch(() => {});
    },

    showHelp() {
        const globals = Object.keys(this._getGlobals());
        const help = [
            '╭─ REPL Help ─────────────────────────────╮',
            '│ Enter          Run expression             │',
            '│ Shift+Enter    New line                   │',
            '│ ↑ / ↓          Command history            │',
            '│ Tab            Auto-complete               │',
            '╰──────────────────────────────────────────╯',
            '',
            'Injected globals: ' + globals.join(', '),
            '',
            'Top-level await is supported:',
            '  const r = await sw.api.get("/api/v1/profile")',
            '',
            'Shortcuts:',
            '  $(sel)   → document.querySelector(sel)',
            '  $$(sel)  → document.querySelectorAll(sel)',
            '  sleep(ms) → await sleep(1000)',
        ];
        help.forEach(line => this._appendInfo(line));
        this._scrollToBottom();
    },
};

// Initialized from app.js startApp() after auth is confirmed,
// ensuring auth is available for admin gate check.

// ── Exports (v0.37.14: window.* replaces sb.ns) ──
window.REPL = REPL;
