/**
 * LuaSharedSandbox (v3, main-thread) — wasmoon-VM работает в MAIN потоке,
 * без Web Worker. Это позволяет:
 *   - Видеть точные Lua-ошибки в DevTools (через console.error)
 *   - Использовать debugger / breakpoints прямо в RobloxShim.js
 *   - Не возиться с молчаливыми Worker-падениями
 *
 * Цена: тяжёлые Lua-скрипты могут блокировать UI. Для KillBrick-style
 * скриптов это нестрашно — они быстрые.
 *
 * API совместим с ScriptSandbox: setOnCommand / sendEvent / sendGlobalEvent /
 * sendSceneSnapshot / sendGuiSnapshot / sendDataSnapshot / sendSkinsSnapshot /
 * sendTerrainHeightmap / stop / tick / target.
 *
 * Что добавлено сверх ScriptSandbox:
 *   - addScript(id, code, target) — добавить скрипт в общий VM. Можно
 *     до или после start().
 *   - start() — асинхронен (createEngine), но возвращает сразу. После init
 *     стартует main loop (Heartbeat + scheduler).
 */

import { LuaFactory } from 'wasmoon';
import { registerRobloxShim } from './RobloxShim.js';

export class LuaSharedSandbox {
    constructor() {
        this.vm = null;
        this.api = null;
        this._onCommand = null;
        this._isReady = false;
        this._isStopped = false;
        this._isKickedOff = false;
        this._pendingScripts = [];   // [{id, code, target, name}]
        this._scriptsById = new Map();
        this._scenes = null;
        this._guiTree = null;
        this._loopHandle = null;
        this._lastTickAt = 0;
        // Маркер для GameRuntime.routeEvent — этот sandbox принимает все
        // события и сам маршрутизирует через shim.fireTargetEvent.
        this._luaShared = true;
    }

    setOnCommand(cb) { this._onCommand = cb; }

    get target() { return null; }
    tick(_dt, _state) { /* no-op: main-loop запускается изнутри (setInterval) */ }

    addScript(id, code, target, name, extra) {
        const entry = {
            id: String(id || `lua_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`),
            code: String(code || ''),
            target: target == null ? null : target,
            name: name || null,
            toolName: extra?.toolName || null,
        };
        this._scriptsById.set(entry.id, entry);
        if (!this._isKickedOff) {
            this._pendingScripts.push(entry);
        } else {
            this._startSingleScript(entry);
        }
    }

    removeScript(id) {
        this._scriptsById.delete(String(id));
    }

    /** Стартует VM, регистрирует shim, запускает main-loop. */
    start() {
        if (this.vm || this._isStopped) return;
        // eslint-disable-next-line no-console
        console.log('[LuaSharedSandbox v3] starting Lua VM in main thread...');
        this._initAsync().catch((err) => {
            // eslint-disable-next-line no-console
            console.error('[LuaSharedSandbox] FATAL init error:', err);
            this._emit('log', { level: 'error', text: `Lua-runtime init: ${err?.message || err}` });
        });
    }

    async _initAsync() {
        const factory = new LuaFactory();
        this.vm = await factory.createEngine({ openStandardLibs: true });
        // eslint-disable-next-line no-console
        console.log('[LuaSharedSandbox] VM created. Registering Roblox shim...');

        // Адаптер для shim'а: он ожидает send(cmd, payload), getSceneSnapshot, getGuiTree, scheduleWait.
        const send = (cmd, payload) => this._emit(cmd, payload);

        this.api = registerRobloxShim(this.vm, {
            send,
            getSceneSnapshot: () => this._scenes,
            getGuiTree: () => this._guiTree,
            scheduleWait: () => null,
        });

        // eslint-disable-next-line no-console
        console.log('[LuaSharedSandbox] Shim registered. api keys:', Object.keys(this.api || {}));

        // Применим snapshot если он есть
        if (this._scenes && this.api?.onSceneSnapshot) {
            try { this.api.onSceneSnapshot(this._scenes); } catch (e) {
                console.error('[LuaSharedSandbox] onSceneSnapshot:', e);
            }
        }

        this._isReady = true;
        this._kickoff();
    }

    _kickoff() {
        if (this._isKickedOff || this._isStopped) return;
        this._isKickedOff = true;
        // eslint-disable-next-line no-console
        console.log(`[LuaSharedSandbox] kickoff: starting ${this._pendingScripts.length} scripts`);
        const pending = this._pendingScripts;
        this._pendingScripts = [];
        // Запускаем main-loop сразу — он начнёт tick'ать как только будут coroutines.
        this._lastTickAt = performance.now();
        this._startMainLoop();
        // Init батчами по 5 с задержкой 20мс между ними, чтобы UI отзывался.
        const BATCH_SIZE = 5;
        let idx = 0;
        const initBatch = () => {
            if (this._isStopped) return;
            const end = Math.min(idx + BATCH_SIZE, pending.length);
            for (let i = idx; i < end; i++) {
                try { this._startSingleScript(pending[i]); }
                catch (e) {
                    // eslint-disable-next-line no-console
                    console.error('[LuaSharedSandbox] init batch err:', e);
                }
            }
            idx = end;
            if (idx < pending.length) {
                setTimeout(initBatch, 20);
            } else {
                // eslint-disable-next-line no-console
                console.log(`[LuaSharedSandbox] all ${pending.length} scripts kicked off`);
                // После того как все скрипты подключили хендлеры — фейрим
                // events для уже существующих сущностей. Roblox-конвенция:
                // если игрок уже на сервере когда скрипт подключается,
                // Players.PlayerAdded не сработает повторно. Юзеру нужно
                // делать ручной обход GetPlayers() — но это редко кто помнит.
                // Мы дублируем событие через короткую задержку.
                setTimeout(() => {
                    try {
                        if (this.api?.fireExistingPlayers) {
                            this.api.fireExistingPlayers();
                        }
                    } catch (e) {
                        console.warn('[LuaSharedSandbox] fireExistingPlayers failed:', e);
                    }
                }, 100);
            }
        };
        setTimeout(initBatch, 0);
    }

    _startSingleScript(entry) {
        if (!this.vm || !entry || typeof entry.code !== 'string') return;
        let primId = null;
        if (typeof entry.target === 'number') primId = entry.target;
        else if (entry.target && typeof entry.target === 'object') {
            if (entry.target.kind === 'primitive') primId = entry.target.id ?? entry.target.ref;
        }
        const safeId = entry.id.replace(/[^a-zA-Z0-9_]/g, '_');
        const scriptName = entry.name || `Script_${safeId}`;
        // Скрипт оборачиваем в coroutine — это позволяет task.wait через yield.
        // Резюмим coroutine из main-loop когда наступило время.
        // Регистрируем coroutine в __rbxl_coroutines с id для возобновления.
        // Скрипт оборачиваем в coroutine. task.wait()→coroutine.yield(sec) возвращает
        // delay из resume → планируем следующий resume через scheduleResume.
        // Fallback Parent: если скрипт связан с Tool (по имени Tool из metadata) —
        // подсовываем виртуальный Tool как script.Parent. Иначе primitive по id,
        // иначе workspace.
        let parentExpr;
        if (entry.toolName) {
            // Tool создаётся в shim как Instance.new('Tool'). По имени достаём.
            // Если не нашли — fallback на новый Tool того же имени.
            const safeName = JSON.stringify(entry.toolName);
            parentExpr = `(function()
                local existing = __rbxl_get_tool_by_name(${safeName})
                if existing then return existing end
                local t = Instance.new("Tool")
                t.Name = ${safeName}
                return t
            end)()`;
        } else if (primId != null) {
            parentExpr = `(__rbxl_get_part_by_id(${Number(primId)}) or workspace)`;
        } else {
            parentExpr = 'workspace';
        }
        const wrapped = `
            do
                -- Если parentExpr вернул primitive — у него уже есть :FindFirstChild и пр.
                -- Если ничего не вернёт — workspace (всегда валидный).
                -- script.Parent.Parent (Tool.Parent = StarterPack / Backpack / workspace).
                local _scriptParent = ${parentExpr}
                if _scriptParent == nil then _scriptParent = workspace end
                if _scriptParent.Parent == nil then _scriptParent.Parent = workspace end
                local script = setmetatable({
                    Name = ${JSON.stringify(scriptName)},
                    Parent = _scriptParent,
                    ClassName = "Script",
                    Disabled = false,
                    Source = nil,
                }, {
                    -- Любой доступ к несуществующему полю → workspace
                    -- (на случай script.Foo:Bar() в старом коде)
                    __index = function(t, k)
                        if k == "FindFirstChild" or k == "WaitForChild" or k == "GetChildren" then
                            return function() return nil end
                        end
                        return workspace[k]
                    end,
                })
                local co = coroutine.create(function()
                    -- WATCHDOG: каждые 100000 инструкций — yield 1 кадр.
                    -- НЕ оборачиваем в pcall — внутри C-call boundary yield
                    -- упадёт ошибкой, что прервёт скрипт. Это лучше чем виснуть.
                    debug.sethook(function()
                        coroutine.yield(0.016)
                    end, "", 20000)
                    -- pcall защищает от runtime-ошибок которые иначе крашат
                    -- coroutine и могут повредить WASM-стейт. Возвраты
                    -- handler'а намеренно поглощаются.
                    local ok_, err_ = pcall(function()
                        ${entry.code}
                    end)
                    if not ok_ then
                        __rbxl_send_error(${JSON.stringify(entry.id)}, tostring(err_))
                    end
                end)
                __rbxl_register_coroutine(${JSON.stringify(entry.id)}, co)
                local ok, ret = coroutine.resume(co)
                if not ok then
                    __rbxl_send_error(${JSON.stringify(entry.id)}, tostring(ret))
                    __rbxl_unregister_coroutine(${JSON.stringify(entry.id)})
                elseif type(ret) == 'number' then
                    -- скрипт yield'нул с delay (через task.wait) — планируем resume
                    __rbxl_schedule_resume(${JSON.stringify(entry.id)}, ret)
                elseif coroutine.status(co) == 'dead' then
                    __rbxl_unregister_coroutine(${JSON.stringify(entry.id)})
                end
            end
        `;
        try {
            this.vm.doStringSync(wrapped);
            // eslint-disable-next-line no-console
            console.log(`[LuaSharedSandbox] script ${entry.id} initialized OK`);
        } catch (err) {
            // eslint-disable-next-line no-console
            console.error(`[LuaSharedSandbox] script ${entry.id} init FAILED:`, err);
            this._emit('log', { level: 'error', text: `[Lua ${entry.id}] ${err?.message || err}` });
        }
    }

    _startMainLoop() {
        const tick = () => {
            if (this._isStopped) return;
            try {
                const now = performance.now();
                const dt = Math.min(0.1, (now - this._lastTickAt) / 1000);
                this._lastTickAt = now;
                if (this.api?.tickScheduler) this.api.tickScheduler(dt);
                if (this.api?.fireHeartbeat) this.api.fireHeartbeat(dt);
            } catch (e) {
                // eslint-disable-next-line no-console
                console.error('[LuaSharedSandbox tick]', e);
            }
            this._loopHandle = setTimeout(tick, 16);
        };
        this._loopHandle = setTimeout(tick, 16);
    }

    _emit(cmd, payload) {
        if (typeof this._onCommand === 'function') {
            try { this._onCommand({ cmd, payload }); } catch (_) {}
        }
    }

    // ----- API совместимый с ScriptSandbox -----
    sendEvent(payload) {
        if (!this.api?.fireTargetEvent || !this._isReady) return;
        try { this.api.fireTargetEvent(payload); } catch (e) {
            console.error('[LuaSharedSandbox] sendEvent:', e);
        }
    }

    sendGlobalEvent(payload) {
        if (!this.api?.fireGlobalEvent || !this._isReady) return;
        try { this.api.fireGlobalEvent(payload); } catch (e) {
            console.error('[LuaSharedSandbox] sendGlobalEvent:', e);
        }
    }

    sendSceneSnapshot(snapshot) {
        this._scenes = snapshot;
        if (this.api?.onSceneSnapshot && this._isReady) {
            try { this.api.onSceneSnapshot(snapshot); } catch (e) {
                console.error('[LuaSharedSandbox] onSceneSnapshot:', e);
            }
        }
    }

    sendGuiSnapshot(snapshot) {
        this._guiTree = snapshot;
        if (this.api?.onGuiSnapshot && this._isReady) {
            try { this.api.onGuiSnapshot(snapshot); } catch (_) {}
        }
    }

    sendDataSnapshot(snapshot) {
        if (this.api?.onDataSnapshot && this._isReady) {
            try { this.api.onDataSnapshot(snapshot); } catch (_) {}
        }
    }

    sendSkinsSnapshot(_) { /* no-op для Этапа 3 */ }
    sendTerrainHeightmap(_) { /* no-op */ }

    stop() {
        this._isStopped = true;
        if (this._loopHandle) {
            clearTimeout(this._loopHandle);
            this._loopHandle = null;
        }
        if (this.vm) {
            try { this.vm.global.close(); } catch (_) {}
            this.vm = null;
        }
        this.api = null;
    }
}

export default LuaSharedSandbox;
