#!/usr/bin/env node
/**
 * Evaluate SX (or inspect HS compiler/parser output) in the full WASM kernel.
 *
 * Environment variables (preferred — avoids shell escaping):
 *   HS_EVAL_EXPR    SX expression to evaluate (required unless --expr arg given)
 *   HS_EVAL_SETUP   SX setup expression run before main eval
 *   HS_EVAL_FILES   Comma-separated list of .sx files to load first
 *   HS_EVAL_MODE    'eval' (default) | 'compile' | 'parse'
 *                     compile: wraps expr as hs-compile arg, returns SX AST string
 *                     parse:   wraps expr as hs-parse arg, returns parse tree string
 *
 * CLI fallback: first positional arg used as expression if HS_EVAL_EXPR not set.
 *
 * Output: JSON to stdout  { ok: true, result: "..." }
 *                     or  { ok: false, error: "..." }
 * Progress / load errors go to stderr.
 */

'use strict';
const fs   = require('fs');
const path = require('path');

const PROJECT  = path.resolve(__dirname, '..');
const WASM_DIR = path.join(PROJECT, 'shared/static/wasm');
const SX_DIR   = path.join(WASM_DIR, 'sx');

// ── Load WASM kernel ────────────────────────────────────────────
eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
const K = globalThis.SxKernel;

// ── Minimal DOM mock ────────────────────────────────────────────
class CL {
  constructor() { this._s = new Set(); }
  add(c)      { if (c) this._s.add(c); }
  remove(c)   { this._s.delete(c); }
  contains(c) { return this._s.has(c); }
  toggle(c)   { this._s.has(c) ? this.remove(c) : this.add(c); return this._s.has(c); }
  _sync(v)    { this._s = new Set((v||'').split(' ').filter(Boolean)); }
}
class El {
  constructor(t) {
    this.tagName = t.toUpperCase(); this.nodeName = this.tagName; this.nodeType = 1;
    this.id = ''; this.className = ''; this.textContent = ''; this.innerHTML = '';
    this.value = ''; this.checked = false; this.disabled = false; this.type = '';
    this.style = { setProperty(p,v){this[p]=v;}, getPropertyValue(p){return this[p]||'';} };
    this.attributes = {}; this.children = []; this.childNodes = [];
    this.childNodes.item = i => this.childNodes[i] || null;
    this.parentNode = null; this.parentElement = null; this._listeners = {};
    this.classList = new CL();
    this.dataset = {};
    this.open = false; this.multiple = false; this.selected = false;
  }
  setAttribute(n,v) {
    this.attributes[n] = String(v);
    if (n==='id')    this.id = v;
    if (n==='class') { this.className = v; this.classList._sync(v); }
    if (n==='value') this.value = v;
  }
  getAttribute(n)   { return this.attributes[n] !== undefined ? this.attributes[n] : null; }
  removeAttribute(n){ delete this.attributes[n]; }
  hasAttribute(n)   { return n in this.attributes; }
  appendChild(c)    { if(c){ c.parentNode=this; c.parentElement=this; this.children.push(c); this.childNodes.push(c); } return c; }
  removeChild(c)    { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); if(c){c.parentNode=null;c.parentElement=null;} return c; }
  remove()          { if(this.parentNode) this.parentNode.removeChild(this); }
  prepend(c)        { if(c){ c.parentNode=this; this.children.unshift(c); this.childNodes.unshift(c); } }
  insertBefore(c,r) { if(!r) return this.appendChild(c); const i=this.childNodes.indexOf(r); if(i<0) return this.appendChild(c); this.childNodes.splice(i,0,c); this.children.splice(i,0,c); c.parentNode=this; return c; }
  replaceChild(n,o) { const i=this.childNodes.indexOf(o); if(i>=0){ this.childNodes[i]=n; this.children[i]=n; n.parentNode=this; o.parentNode=null; } return o; }
  cloneNode(deep)   { const c=new El(this.tagName); if(deep) for(const ch of this.childNodes) c.appendChild(ch.cloneNode&&ch.cloneNode(true)||{...ch}); return c; }
  addEventListener(t,h)    { if(!this._listeners[t]) this._listeners[t]=[]; this._listeners[t].push(h); }
  removeEventListener(t,h) { if(this._listeners[t]) this._listeners[t]=this._listeners[t].filter(x=>x!==h); }
  dispatchEvent(ev) { (this._listeners[ev&&ev.type]||[]).forEach(h=>{ try{h(ev);}catch(e){} }); return true; }
  querySelector(sel) {
    if (!sel) return null;
    if (sel.startsWith('#')) { const id=sel.slice(1); if(this.id===id) return this; for(const c of this.childNodes){const r=c.querySelector&&c.querySelector(sel); if(r) return r;} return null; }
    return null;
  }
  querySelectorAll() { return []; }
  closest(sel)  { return sel && this.matches(sel) ? this : (this.parentNode && this.parentNode.closest ? this.parentNode.closest(sel) : null); }
  matches(sel)  {
    if (!sel) return false;
    if (sel.startsWith('#')) return this.id === sel.slice(1);
    if (sel.startsWith('.')) return this.classList.contains(sel.slice(1));
    return this.tagName.toLowerCase() === sel.toLowerCase();
  }
  focus()  {}
  blur()   {}
  click()  { this.dispatchEvent(new Ev('click',{bubbles:true})); }
  getBoundingClientRect() { return {width:0,height:0,top:0,left:0,right:0,bottom:0}; }
}
class Ev {
  constructor(t,o) { this.type=t; const opts=o||{}; this.bubbles=opts.bubbles!==false; this.detail=opts.detail||null; this.target=null; this.currentTarget=null; }
  preventDefault()  {}
  stopPropagation() {}
}

const _body = new El('body');
const _head = new El('head');
const _docListeners = {};
const _domRegistry = new Map(); // id -> El

function _findById(id) {
  function find(el) {
    if (!(el instanceof El)) return null;
    if (el.id === id) return el;
    for (const c of (el.childNodes||[])) { const r = find(c); if (r) return r; }
    return null;
  }
  return find(_body);
}

globalThis.document = {
  body: _body, head: _head, title: '',
  createElement: t => new El(t),
  createElementNS: (ns,t) => new El(t),
  createTextNode: s => ({ nodeType:3, textContent:String(s||''), nodeName:'#text', parentNode:null }),
  createDocumentFragment: () => { const f=new El('fragment'); f.nodeType=11; return f; },
  createComment: s => ({ nodeType:8, textContent:s, nodeName:'#comment' }),
  getElementById: id => _findById(id),
  querySelector: sel => sel && sel.startsWith('#') ? _findById(sel.slice(1)) : null,
  querySelectorAll: () => [],
  addEventListener: (t,h) => { if(!_docListeners[t]) _docListeners[t]=[]; _docListeners[t].push(h); },
  removeEventListener: (t,h) => { if(_docListeners[t]) _docListeners[t]=_docListeners[t].filter(x=>x!==h); },
  dispatchEvent: ev => { (_docListeners[ev&&ev.type]||[]).forEach(h=>{ try{h(ev);}catch(e){} }); },
  activeElement: null,
};
globalThis.CustomEvent = Ev;
globalThis.Event = Ev;
globalThis.window = globalThis;
try { globalThis.navigator = { userAgent: 'node' }; } catch(e) { Object.defineProperty(globalThis, 'navigator', { value: { userAgent: 'node' }, writable: true, configurable: true }); }
globalThis.location = { href:'http://localhost/', pathname:'/', search:'', hash:'' };
globalThis.history = { pushState(){}, replaceState(){} };
globalThis.getSelection = () => ({ toString: () => '' });
globalThis.console = { log:()=>{}, error:()=>{}, warn:()=>{}, info:()=>{}, debug:()=>{} };
globalThis.ResizeObserver = class { observe(){} unobserve(){} disconnect(){} };
globalThis.IntersectionObserver = class { constructor(cb){} observe(){} unobserve(){} disconnect(){} takeRecords(){return[];} };

// ── FFI registrations ───────────────────────────────────────────
K.registerNative('hs-ref-eq',   a => a[0]===a[1]);
K.registerNative('host-global', a => { const n=a[0]; return (n in globalThis)?globalThis[n]:null; });
K.registerNative('host-get',    a => {
  if (a[0]==null) return null;
  if (a[0] && a[0]._type==='list' && (a[1]==='length'||a[1]==='size')) return a[0].items.length;
  if (a[0] instanceof El && a[1]==='innerText') return String(a[0].textContent||'');
  const v = a[0][a[1]]; return v===undefined ? null : v;
});
K.registerNative('host-set!',   a => { if(a[0]!=null){ a[0][a[1]]=a[2]; if(a[0] instanceof El && a[1]==='id' && a[2]) a[0].id=a[2]; } return a[2]; });
K.registerNative('host-call',   a => {
  const [o,m,...r]=a;
  if(o==null){ const f=globalThis[m]; return typeof f==='function'?f.apply(null,r):null; }
  if(o && typeof o[m]==='function'){ try{ const v=o[m].apply(o,r); return v===undefined?null:v; }catch(e){ return null; } }
  return null;
});
K.registerNative('host-call-fn', a => {
  const [fn,argList]=a;
  if(!fn) return null;
  const args=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);
  if(fn&&fn.__sx_handle!==undefined) return K.callFn(fn,args);
  try{ return fn.apply(null,args); }catch(e){ return null; }
});
K.registerNative('host-new',     a => { const C=typeof a[0]==='string'?globalThis[a[0]]:a[0]; return typeof C==='function'?new C(...a.slice(1)):null; });
K.registerNative('host-callback',a => {
  const fn=a[0];
  if(fn&&fn.__sx_handle!==undefined) return function(){ const r=K.callFn(fn,Array.from(arguments)); if(globalThis._driveAsync) globalThis._driveAsync(r); return r; };
  return typeof fn==='function'?fn:function(){};
});
K.registerNative('host-typeof',  a => { const o=a[0]; if(o==null) return 'nil'; if(o instanceof El) return 'element'; if(o instanceof Ev) return 'event'; return typeof o; });
K.registerNative('host-iter?',   ([obj]) => obj!=null && typeof obj[Symbol.iterator]==='function');
K.registerNative('host-to-list', ([obj]) => { try{ return [...obj]; }catch(e){ return []; } });
K.registerNative('host-await',   () => {});
K.registerNative('host-new-function', a => { const p=(a[0]&&a[0]._type==='list')?Array.from(a[0].items):[]; try{ return new Function(...p,a[1]); }catch(e){ return null; } });
K.registerNative('host-promise-state', a => { const p=a[0]; if(!p||typeof p.then!=='function') return null; const s=globalThis._promiseStates&&globalThis._promiseStates.get(p); return s?{ok:s.ok,value:s.value}:null; });
K.registerNative('load-library!', () => false);

// Async IO driver
let _evalDeadline = 0;
globalThis._driveAsync = function driveAsync(r, depth) {
  depth = depth||0;
  if (_evalDeadline && Date.now() > _evalDeadline) throw new Error('TIMEOUT: wall clock exceeded');
  if (!r || !r.suspended || depth > 200) return;
  const req = r.request;
  const items = req && (req.items || req);
  const op = items && items[0];
  const opName = typeof op==='string' ? op : (op&&op.name)||String(op);
  function doResume(v) { try{ const x=r.resume(v); driveAsync(x,depth+1); }catch(e){} }
  if (opName==='io-sleep'||opName==='wait') doResume(null);
  else if (opName==='io-wait-event') {
    const target=items&&items[1];
    const evName=typeof items[2]==='string'?items[2]:'';
    const timeout=items&&items.length>3?items[3]:undefined;
    if (typeof timeout==='number') { doResume(null); }
    else if (target && target instanceof El && evName) {
      const handler=function(ev){ target.removeEventListener(evName,handler); doResume(ev); };
      target.addEventListener(evName,handler);
    } else { doResume(null); }
  }
  else if (opName==='io-transition') doResume(null);
  else doResume(null);
};

// ── SX aliases ──────────────────────────────────────────────────
K.eval('(define SX_VERSION "hs-eval-1.0")');
K.eval('(define SX_ENGINE "ocaml-vm-sandbox")');
K.eval('(define parse sx-parse)');
K.eval('(define serialize sx-serialize)');

// ── Load HS modules ─────────────────────────────────────────────
const WEB = ['render','core-signals','signals','deps','router','page-helpers','freeze','dom','browser',
             'adapter-html','adapter-sx','adapter-dom','boot-helpers','hypersx','engine','orchestration','boot'];
const HS  = ['hs-tokenizer','hs-parser','hs-compiler','hs-runtime','hs-worker','hs-prolog','hs-integration'];
const HS_PLUGINS = new Set(['hs-worker','hs-prolog']);
K.beginModuleLoad();
for (const mod of [...WEB, ...HS]) {
  const sp = path.join(SX_DIR, mod+'.sx');
  const stem = mod.replace(/^hs-/,'');
  const lp = HS_PLUGINS.has(mod)
    ? path.join(PROJECT, 'lib/hyperscript/plugins', stem+'.sx')
    : path.join(PROJECT, 'lib/hyperscript', stem+'.sx');
  let s;
  try {
    const lpExists = mod.startsWith('hs-') && fs.existsSync(lp);
    s = lpExists ? fs.readFileSync(lp,'utf8')
      : fs.existsSync(sp) ? fs.readFileSync(sp,'utf8')
      : fs.readFileSync(lp,'utf8');
  } catch(e) { continue; }
  try { K.load(s); } catch(e) { process.stderr.write(`LOAD ERROR: ${mod}: ${e.message}\n`); }
}
K.endModuleLoad();

// ── Extra files ─────────────────────────────────────────────────
const extraFiles = (process.env.HS_EVAL_FILES || '').split(',').filter(Boolean);
for (const f of extraFiles) {
  try { K.load(fs.readFileSync(f.trim(),'utf8')); }
  catch(e) { process.stderr.write(`FILE ERROR: ${f}: ${e.message}\n`); }
}

// ── Setup expression ────────────────────────────────────────────
const setup = process.env.HS_EVAL_SETUP || '';
if (setup) {
  try { K.eval(setup); }
  catch(e) {
    process.stdout.write(JSON.stringify({ok:false,error:`Setup error: ${e.message||String(e)}`})+'\n');
    process.exit(1);
  }
}

// ── Main evaluation ─────────────────────────────────────────────
const mode    = process.env.HS_EVAL_MODE || 'eval';
const rawExpr = process.env.HS_EVAL_EXPR || process.argv[2] || '';
if (!rawExpr) {
  process.stdout.write(JSON.stringify({ok:false,error:'No expression provided. Set HS_EVAL_EXPR or pass as first argument.'})+'\n');
  process.exit(1);
}

const expr = mode==='compile' ? `(str (hs-compile ${JSON.stringify(rawExpr)}))`
           : mode==='parse'   ? `(str (hs-parse ${JSON.stringify(rawExpr)}))`
           : rawExpr;

_evalDeadline = Date.now() + parseInt(process.env.HS_EVAL_TIMEOUT_MS||'30000');
try {
  const result = K.eval(expr);
  let resultStr;
  try { resultStr = JSON.stringify(result); } catch(e) { resultStr = String(result); }
  process.stdout.write(JSON.stringify({ok:true,result:resultStr})+'\n');
} catch(e) {
  process.stdout.write(JSON.stringify({ok:false,error:e.message||String(e)})+'\n');
}
