// main.el — el-lsp
//
// Language Server for El. Pure-El port of the el-lsp Rust crate.
//
// Entry surface (callable from any El program that imports this file):
//
//   lsp_complete(source: String, pos: Int)        -> String  // JSON array
//   lsp_hover(source: String, pos: Int)           -> String  // JSON object or "null"
//   lsp_diagnostics(source: String)               -> String  // JSON array
//   lsp_outline(source: String)                   -> String  // JSON array
//   lsp_format(source: String)                    -> String  // JSON { content, changed }
//   lsp_type_graph(source: String)                -> String  // JSON { nodes, edges }
//   lsp_definition(source: String, pos: Int)      -> String  // JSON object or "null"
//   lsp_references(source: String, pos: Int)      -> String  // JSON array
//   lsp_jsonrpc(method: String, params_json: String) -> String
//   lsp_stdio_loop()                              -> Void
//
// Tokenisation reuses the lexer.el idiom (chars list + index walk) for the
// pieces we need (identifier scan, string scan, line counting). The full
// type checker referenced by the Rust crate is not yet available in El, so
// hover/completion/diagnostics fall back to source-scan heuristics over the
// declared types/enums/functions in the file. The wire shape is preserved.

// ── Char helpers (mirrors lexer.el) ──────────────────────────────────────────

fn is_digit_ch(ch: String) -> Bool {
    if ch == "0" { return true } if ch == "1" { return true }
    if ch == "2" { return true } if ch == "3" { return true }
    if ch == "4" { return true } if ch == "5" { return true }
    if ch == "6" { return true } if ch == "7" { return true }
    if ch == "8" { return true } if ch == "9" { return true }
    false
}

fn is_alpha_ch(ch: String) -> Bool {
    let lower: String = str_to_lower(ch)
    if str_eq(ch, "") { return false }
    if !str_eq(lower, str_to_upper(lower)) { return true }   // letters differ in case
    // covers a-z (lower-only) — uppercase already handled by inverse case test above
    if str_contains("abcdefghijklmnopqrstuvwxyz", ch) { return true }
    if str_contains("ABCDEFGHIJKLMNOPQRSTUVWXYZ", ch) { return true }
    false
}

fn is_ident_ch(ch: String) -> Bool {
    if is_digit_ch(ch) { return true }
    if is_alpha_ch(ch) { return true }
    if ch == "_" { return true }
    false
}

// ── Source navigation ────────────────────────────────────────────────────────

// Extract the identifier fragment immediately before `pos` (cursor).
fn extract_prefix(source: String, pos: Int) -> String {
    let n: Int = str_len(source)
    let end: Int = pos
    if end > n { let end = n }
    let start: Int = end
    let chars: [String] = native_string_chars(source)
    let total: Int = native_list_len(chars)
    let scanning: Bool = true
    while scanning {
        if start <= 0 {
            let scanning = false
        } else {
            let prev: String = native_list_get(chars, start - 1)
            if is_ident_ch(prev) {
                let start = start - 1
            } else {
                let scanning = false
            }
        }
    }
    str_slice(source, start, end)
}

// Extract the full token (alphanumerics + underscore) surrounding `pos`.
fn extract_token(source: String, pos: Int) -> String {
    let n: Int = str_len(source)
    if pos > n { return "" }
    let chars: [String] = native_string_chars(source)
    let total: Int = native_list_len(chars)
    let start: Int = pos
    let go_left: Bool = true
    while go_left {
        if start <= 0 {
            let go_left = false
        } else {
            let prev: String = native_list_get(chars, start - 1)
            if is_ident_ch(prev) {
                let start = start - 1
            } else {
                let go_left = false
            }
        }
    }
    let end: Int = pos
    let go_right: Bool = true
    while go_right {
        if end >= total {
            let go_right = false
        } else {
            let cur: String = native_list_get(chars, end)
            if is_ident_ch(cur) {
                let end = end + 1
            } else {
                let go_right = false
            }
        }
    }
    str_slice(source, start, end)
}

// Translate a byte offset into 1-based (line, col).
fn pos_to_line_col(source: String, pos: Int) -> Map<String, Any> {
    let n: Int = str_len(source)
    let cap: Int = pos
    if cap > n { let cap = n }
    let chars: [String] = native_string_chars(source)
    let i: Int = 0
    let line: Int = 1
    let col: Int = 1
    while i < cap {
        let ch: String = native_list_get(chars, i)
        if ch == "\n" {
            let line = line + 1
            let col = 1
        } else {
            let col = col + 1
        }
        let i = i + 1
    }
    { "line": line, "col": col }
}

// ── JSON encoding helpers ────────────────────────────────────────────────────

// Escape a string for JSON. Handles ", \, newline, tab.
fn json_escape(s: String) -> String {
    let chars: [String] = native_string_chars(s)
    let n: Int = native_list_len(chars)
    let out: String = ""
    let i: Int = 0
    while i < n {
        let ch: String = native_list_get(chars, i)
        if ch == "\"" { let out = out + "\\\"" }
        else { if ch == "\\" { let out = out + "\\\\" }
        else { if ch == "\n" { let out = out + "\\n" }
        else { if ch == "\r" { let out = out + "\\r" }
        else { if ch == "\t" { let out = out + "\\t" }
        else { let out = out + ch } } } } }
        let i = i + 1
    }
    out
}

fn json_str(s: String) -> String {
    "\"" + json_escape(s) + "\""
}

fn json_kv_str(k: String, v: String) -> String {
    json_str(k) + ":" + json_str(v)
}

fn json_kv_int(k: String, v: Int) -> String {
    json_str(k) + ":" + int_to_str(v)
}

fn json_kv_float(k: String, v: Float) -> String {
    json_str(k) + ":" + float_to_str(v)
}

fn json_kv_raw(k: String, raw_v: String) -> String {
    json_str(k) + ":" + raw_v
}

// Join a list of pre-encoded JSON strings into a JSON array.
fn json_array_of(items: [String]) -> String {
    let n: Int = native_list_len(items)
    let out: String = "["
    let i: Int = 0
    while i < n {
        if i > 0 { let out = out + "," }
        let out = out + native_list_get(items, i)
        let i = i + 1
    }
    out + "]"
}

// ── Keyword/builtin tables ───────────────────────────────────────────────────

fn keywords_list() -> [String] {
    let ks: [String] = native_list_empty()
    let ks = native_list_append(ks, "let")
    let ks = native_list_append(ks, "fn")
    let ks = native_list_append(ks, "type")
    let ks = native_list_append(ks, "enum")
    let ks = native_list_append(ks, "match")
    let ks = native_list_append(ks, "return")
    let ks = native_list_append(ks, "activate")
    let ks = native_list_append(ks, "where")
    let ks = native_list_append(ks, "sealed")
    let ks = native_list_append(ks, "if")
    let ks = native_list_append(ks, "else")
    let ks = native_list_append(ks, "for")
    let ks = native_list_append(ks, "in")
    let ks = native_list_append(ks, "while")
    let ks = native_list_append(ks, "true")
    let ks = native_list_append(ks, "false")
    let ks = native_list_append(ks, "test")
    let ks = native_list_append(ks, "seed")
    let ks = native_list_append(ks, "assert")
    let ks = native_list_append(ks, "target")
    let ks = native_list_append(ks, "protocol")
    let ks = native_list_append(ks, "impl")
    let ks = native_list_append(ks, "import")
    let ks = native_list_append(ks, "from")
    let ks = native_list_append(ks, "as")
    let ks = native_list_append(ks, "with")
    let ks = native_list_append(ks, "retry")
    let ks = native_list_append(ks, "times")
    let ks = native_list_append(ks, "fallback")
    let ks = native_list_append(ks, "reason")
    let ks = native_list_append(ks, "parallel")
    let ks = native_list_append(ks, "trace")
    let ks = native_list_append(ks, "requires")
    let ks = native_list_append(ks, "deploy")
    let ks = native_list_append(ks, "to")
    let ks = native_list_append(ks, "via")
    let ks = native_list_append(ks, "vessel")
    let ks = native_list_append(ks, "cgi")
    ks
}

// Built-in primitive type names.
fn builtin_type_names() -> [String] {
    let ts: [String] = native_list_empty()
    let ts = native_list_append(ts, "Int")
    let ts = native_list_append(ts, "Float")
    let ts = native_list_append(ts, "String")
    let ts = native_list_append(ts, "Bool")
    let ts = native_list_append(ts, "Uuid")
    let ts = native_list_append(ts, "Void")
    let ts = native_list_append(ts, "Any")
    ts
}

fn builtin_type_doc(name: String) -> String {
    if str_eq(name, "Int") { return "64-bit signed integer" }
    if str_eq(name, "Float") { return "64-bit IEEE 754 double" }
    if str_eq(name, "String") { return "UTF-8 string" }
    if str_eq(name, "Bool") { return "Boolean value" }
    if str_eq(name, "Uuid") { return "RFC 4122 UUID" }
    if str_eq(name, "Void") { return "Unit type — no value" }
    if str_eq(name, "Any") { return "Dynamically-typed value" }
    ""
}

// Built-in function names → signatures. Encoded as flat [name, sig, name, sig, ...]
// to keep type maps simple.
fn builtin_fns() -> [String] {
    let xs: [String] = native_list_empty()
    let xs = native_list_append(xs, "println")     let xs = native_list_append(xs, "fn(value: String) -> Void")
    let xs = native_list_append(xs, "print")       let xs = native_list_append(xs, "fn(value: String) -> Void")
    let xs = native_list_append(xs, "readline")    let xs = native_list_append(xs, "fn() -> String")
    let xs = native_list_append(xs, "args")        let xs = native_list_append(xs, "fn() -> [String]")
    let xs = native_list_append(xs, "int_to_str")  let xs = native_list_append(xs, "fn(n: Int) -> String")
    let xs = native_list_append(xs, "str_to_int")  let xs = native_list_append(xs, "fn(s: String) -> Int")
    let xs = native_list_append(xs, "str_len")     let xs = native_list_append(xs, "fn(s: String) -> Int")
    let xs = native_list_append(xs, "str_slice")   let xs = native_list_append(xs, "fn(s: String, start: Int, end: Int) -> String")
    let xs = native_list_append(xs, "str_concat")  let xs = native_list_append(xs, "fn(a: String, b: String) -> String")
    let xs = native_list_append(xs, "str_contains")    let xs = native_list_append(xs, "fn(s: String, sub: String) -> Bool")
    let xs = native_list_append(xs, "str_starts_with") let xs = native_list_append(xs, "fn(s: String, p: String) -> Bool")
    let xs = native_list_append(xs, "str_ends_with")   let xs = native_list_append(xs, "fn(s: String, suf: String) -> Bool")
    let xs = native_list_append(xs, "str_to_upper")    let xs = native_list_append(xs, "fn(s: String) -> String")
    let xs = native_list_append(xs, "str_to_lower")    let xs = native_list_append(xs, "fn(s: String) -> String")
    let xs = native_list_append(xs, "str_split")       let xs = native_list_append(xs, "fn(s: String, sep: String) -> [String]")
    let xs = native_list_append(xs, "str_trim")        let xs = native_list_append(xs, "fn(s: String) -> String")
    let xs = native_list_append(xs, "str_replace")     let xs = native_list_append(xs, "fn(s: String, from: String, to: String) -> String")
    let xs = native_list_append(xs, "str_index_of")    let xs = native_list_append(xs, "fn(s: String, sub: String) -> Int")
    let xs = native_list_append(xs, "float_to_str")    let xs = native_list_append(xs, "fn(f: Float) -> String")
    let xs = native_list_append(xs, "str_to_float")    let xs = native_list_append(xs, "fn(s: String) -> Float")
    let xs = native_list_append(xs, "math_sqrt")       let xs = native_list_append(xs, "fn(n: Float) -> Float")
    let xs = native_list_append(xs, "math_log")        let xs = native_list_append(xs, "fn(n: Float) -> Float")
    let xs = native_list_append(xs, "math_sin")        let xs = native_list_append(xs, "fn(n: Float) -> Float")
    let xs = native_list_append(xs, "math_cos")        let xs = native_list_append(xs, "fn(n: Float) -> Float")
    let xs = native_list_append(xs, "math_pi")         let xs = native_list_append(xs, "fn() -> Float")
    let xs = native_list_append(xs, "el_abs")          let xs = native_list_append(xs, "fn(n: Int) -> Int")
    let xs = native_list_append(xs, "el_min")          let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int")
    let xs = native_list_append(xs, "el_max")          let xs = native_list_append(xs, "fn(a: Int, b: Int) -> Int")
    let xs = native_list_append(xs, "el_list_empty")   let xs = native_list_append(xs, "fn() -> [Any]")
    let xs = native_list_append(xs, "el_list_len")     let xs = native_list_append(xs, "fn(l: [Any]) -> Int")
    let xs = native_list_append(xs, "el_list_get")     let xs = native_list_append(xs, "fn(l: [Any], i: Int) -> Any")
    let xs = native_list_append(xs, "el_list_append")  let xs = native_list_append(xs, "fn(l: [Any], v: Any) -> [Any]")
    let xs = native_list_append(xs, "list_join")       let xs = native_list_append(xs, "fn(l: [String], sep: String) -> String")
    let xs = native_list_append(xs, "list_range")      let xs = native_list_append(xs, "fn(start: Int, end: Int) -> [Int]")
    let xs = native_list_append(xs, "el_map_get")      let xs = native_list_append(xs, "fn(m: Map, k: String) -> Any")
    let xs = native_list_append(xs, "el_map_set")      let xs = native_list_append(xs, "fn(m: Map, k: String, v: Any) -> Map")
    let xs = native_list_append(xs, "json_parse")      let xs = native_list_append(xs, "fn(s: String) -> Any")
    let xs = native_list_append(xs, "json_stringify")  let xs = native_list_append(xs, "fn(v: Any) -> String")
    let xs = native_list_append(xs, "json_get_string") let xs = native_list_append(xs, "fn(j: String, k: String) -> String")
    let xs = native_list_append(xs, "json_get_int")    let xs = native_list_append(xs, "fn(j: String, k: String) -> Int")
    let xs = native_list_append(xs, "json_get_bool")   let xs = native_list_append(xs, "fn(j: String, k: String) -> Bool")
    let xs = native_list_append(xs, "http_get")        let xs = native_list_append(xs, "fn(url: String) -> String")
    let xs = native_list_append(xs, "http_post")       let xs = native_list_append(xs, "fn(url: String, body: String) -> String")
    let xs = native_list_append(xs, "fs_read")         let xs = native_list_append(xs, "fn(path: String) -> String")
    let xs = native_list_append(xs, "fs_write")        let xs = native_list_append(xs, "fn(path: String, content: String) -> Bool")
    let xs = native_list_append(xs, "fs_list")         let xs = native_list_append(xs, "fn(path: String) -> [String]")
    let xs = native_list_append(xs, "env")             let xs = native_list_append(xs, "fn(key: String) -> String")
    let xs = native_list_append(xs, "uuid_new")        let xs = native_list_append(xs, "fn() -> String")
    let xs = native_list_append(xs, "time_now")        let xs = native_list_append(xs, "fn() -> Int")
    let xs = native_list_append(xs, "time_now_utc")    let xs = native_list_append(xs, "fn() -> Int")
    let xs = native_list_append(xs, "sleep_ms")        let xs = native_list_append(xs, "fn(ms: Int) -> Void")
    let xs = native_list_append(xs, "engram_node")        let xs = native_list_append(xs, "fn(content: String, kind: String, salience: Float) -> String")
    let xs = native_list_append(xs, "engram_search")      let xs = native_list_append(xs, "fn(q: String, limit: Int) -> [Map]")
    let xs = native_list_append(xs, "engram_activate")    let xs = native_list_append(xs, "fn(q: String, depth: Int) -> [Map]")
    let xs = native_list_append(xs, "engram_neighbors")   let xs = native_list_append(xs, "fn(id: String) -> [Map]")
    let xs = native_list_append(xs, "engram_connect")     let xs = native_list_append(xs, "fn(from: String, to: String, w: Float, rel: String) -> Void")
    let xs = native_list_append(xs, "dharma_connect")     let xs = native_list_append(xs, "fn(cgi: String) -> String")
    let xs = native_list_append(xs, "dharma_send")        let xs = native_list_append(xs, "fn(channel: String, content: String) -> String")
    let xs = native_list_append(xs, "dharma_emit")        let xs = native_list_append(xs, "fn(event: String, payload: String) -> Void")
    let xs = native_list_append(xs, "dharma_field")       let xs = native_list_append(xs, "fn(event: String) -> Map")
    let xs = native_list_append(xs, "llm_call")           let xs = native_list_append(xs, "fn(model: String, prompt: String) -> String")
    xs
}

fn keyword_doc(kw: String) -> String {
    if str_eq(kw, "let")      { return "let name: Type = expr — declare an immutable binding." }
    if str_eq(kw, "fn")       { return "fn name(params) -> Ret { body } — define a function." }
    if str_eq(kw, "type")     { return "type Name { field: Type } — struct definition." }
    if str_eq(kw, "enum")     { return "enum Name { Variant1, Variant2(Type) } — enum definition." }
    if str_eq(kw, "match")    { return "match expr { Pattern => body } — pattern match." }
    if str_eq(kw, "return")   { return "return value — exit the enclosing function." }
    if str_eq(kw, "if")       { return "if cond { ... } else { ... } — conditional." }
    if str_eq(kw, "for")      { return "for item in collection { ... } — iterate." }
    if str_eq(kw, "while")    { return "while cond { ... } — loop while cond is truthy." }
    if str_eq(kw, "import")   { return "import \"path.el\" — pull in another module." }
    if str_eq(kw, "from")     { return "from module import { Name } — selective import." }
    if str_eq(kw, "vessel")   { return "vessel \"name\" { ... } — manifest declaration." }
    if str_eq(kw, "cgi")      { return "cgi \"name\" { dharma_id: ... } — CGI identity." }
    if str_eq(kw, "activate") { return "activate Type where \"query\" — spreading-activation retrieval." }
    if str_eq(kw, "sealed")   { return "sealed { ... } — capability-restricted block." }
    if str_eq(kw, "protocol") { return "protocol Name { fn method(self) -> R } — trait-like." }
    if str_eq(kw, "impl")     { return "impl Protocol for Type { ... } — protocol implementation." }
    if str_eq(kw, "with")     { return "with retry/fallback clause." }
    if str_eq(kw, "retry")    { return "retry times N — retry policy." }
    if str_eq(kw, "fallback") { return "fallback { default } — fallback on failure." }
    if str_eq(kw, "true")     { return "Boolean literal." }
    if str_eq(kw, "false")    { return "Boolean literal." }
    ""
}

// ── Source-driven type/fn discovery (no full parser) ─────────────────────────
//
// The Rust crate calls into el-types::TypeChecker for a real env. That
// module isn't available in El yet, so we approximate by line-scan: find
// every `type X {`, `enum X {`, `protocol X {`, and `fn X(` declaration.
// Returns four parallel lists merged into one map for easy access.

fn first_ident_in(rest: String) -> String {
    let trimmed: String = str_trim(rest)
    let n: Int = str_len(trimmed)
    if n == 0 { return "" }
    let chars: [String] = native_string_chars(trimmed)
    let i: Int = 0
    let total: Int = native_list_len(chars)
    while i < total {
        let ch: String = native_list_get(chars, i)
        if !is_ident_ch(ch) {
            return str_slice(trimmed, 0, i)
        }
        let i = i + 1
    }
    trimmed
}

// scan_decls — produce a Map<String, Any> with parallel lists:
//   "types"      : [String]   user-defined struct/enum/protocol names
//   "type_kind"  : [String]   "type" | "enum" | "protocol" matching above
//   "fns"        : [String]   user-defined function names
//   "type_lines" : [Int]      1-based line numbers
//   "fn_lines"   : [Int]
fn scan_decls(source: String) -> Map<String, Any> {
    let lines: [String] = str_split(source, "\n")
    let n: Int = native_list_len(lines)
    let types: [String] = native_list_empty()
    let kinds: [String] = native_list_empty()
    let fns: [String] = native_list_empty()
    let tlines: [Int] = native_list_empty()
    let flines: [Int] = native_list_empty()
    let i: Int = 0
    while i < n {
        let line: String = native_list_get(lines, i)
        let trimmed: String = str_trim(line)
        let lineno: Int = i + 1
        if str_starts_with(trimmed, "fn ") {
            let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed)))
            if !str_eq(name, "") {
                let fns = native_list_append(fns, name)
                let flines = native_list_append(flines, lineno)
            }
        } else { if str_starts_with(trimmed, "type ") {
            let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed)))
            if !str_eq(name, "") {
                let types = native_list_append(types, name)
                let kinds = native_list_append(kinds, "type")
                let tlines = native_list_append(tlines, lineno)
            }
        } else { if str_starts_with(trimmed, "enum ") {
            let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed)))
            if !str_eq(name, "") {
                let types = native_list_append(types, name)
                let kinds = native_list_append(kinds, "enum")
                let tlines = native_list_append(tlines, lineno)
            }
        } else { if str_starts_with(trimmed, "protocol ") {
            let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed)))
            if !str_eq(name, "") {
                let types = native_list_append(types, name)
                let kinds = native_list_append(kinds, "protocol")
                let tlines = native_list_append(tlines, lineno)
            }
        } } } }
        let i = i + 1
    }
    {
        "types": types,
        "type_kind": kinds,
        "fns": fns,
        "type_lines": tlines,
        "fn_lines": flines
    }
}

// list_contains — case-sensitive membership test on [String].
fn list_contains_str(xs: [String], needle: String) -> Bool {
    let n: Int = native_list_len(xs)
    let i: Int = 0
    while i < n {
        if str_eq(native_list_get(xs, i), needle) { return true }
        let i = i + 1
    }
    false
}

// Score a label against a prefix: 0 if no prefix match, else (prefix_len/label_len).
fn prefix_score(label: String, prefix: String) -> Float {
    if str_eq(prefix, "") { return 0.5 }
    let lower_label: String = str_to_lower(label)
    let lower_prefix: String = str_to_lower(prefix)
    if str_starts_with(lower_label, lower_prefix) {
        let lp: Int = str_len(lower_prefix)
        let ll: Int = str_len(lower_label)
        if ll == 0 { return 0.0 }
        return int_to_float(lp) / int_to_float(ll)
    }
    0.0
}

// Render a single completion item to JSON.
fn completion_json(label: String, kind: String, detail: String, doc: String, score: Float) -> String {
    let parts: [String] = native_list_empty()
    let parts = native_list_append(parts, json_kv_str("label", label))
    let parts = native_list_append(parts, json_kv_str("kind", kind))
    let parts = native_list_append(parts, json_kv_str("detail", detail))
    if str_eq(doc, "") {
        let parts = native_list_append(parts, json_kv_raw("documentation", "null"))
    } else {
        let parts = native_list_append(parts, json_kv_str("documentation", doc))
    }
    let parts = native_list_append(parts, json_kv_float("score", score))
    "{" + list_join(parts, ",") + "}"
}

// ── Public: completion ───────────────────────────────────────────────────────

fn lsp_complete(source: String, pos: Int) -> String {
    let prefix: String = extract_prefix(source, pos)
    let lower_prefix: String = str_to_lower(prefix)
    let prefix_empty: Bool = str_eq(prefix, "")
    let items: [String] = native_list_empty()

    // Keywords
    let kws: [String] = keywords_list()
    let nk: Int = native_list_len(kws)
    let i: Int = 0
    while i < nk {
        let kw: String = native_list_get(kws, i)
        if prefix_empty {
            let s: Float = prefix_score(kw, prefix)
            let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s))
        } else { if str_starts_with(kw, prefix) {
            let s: Float = prefix_score(kw, prefix)
            let items = native_list_append(items, completion_json(kw, "keyword", "keyword", keyword_doc(kw), s))
        } }
        let i = i + 1
    }

    // Built-in types
    let bts: [String] = builtin_type_names()
    let nb: Int = native_list_len(bts)
    let i: Int = 0
    while i < nb {
        let name: String = native_list_get(bts, i)
        let lname: String = str_to_lower(name)
        if prefix_empty {
            let s: Float = 0.5 + 0.1
            let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s))
        } else { if str_starts_with(lname, lower_prefix) {
            let s: Float = prefix_score(name, prefix) + 0.1
            let items = native_list_append(items, completion_json(name, "type", builtin_type_doc(name), "Built-in type: " + builtin_type_doc(name), s))
        } }
        let i = i + 1
    }

    // User-defined types
    let decls: Map<String, Any> = scan_decls(source)
    let utypes: [String] = decls["types"]
    let ukinds: [String] = decls["type_kind"]
    let nu: Int = native_list_len(utypes)
    let i: Int = 0
    while i < nu {
        let name: String = native_list_get(utypes, i)
        let kind: String = native_list_get(ukinds, i)
        let lname: String = str_to_lower(name)
        let include: Bool = false
        if prefix_empty { let include = true }
        if str_starts_with(lname, lower_prefix) { let include = true }
        if include {
            let s: Float = prefix_score(name, prefix) + 0.15
            let items = native_list_append(items, completion_json(name, "type", kind + " " + name, "User-defined " + kind + " " + name, s))
        }
        let i = i + 1
    }

    // Built-in functions
    let bfns: [String] = builtin_fns()
    let nf: Int = native_list_len(bfns)
    let i: Int = 0
    while i < nf {
        let name: String = native_list_get(bfns, i)
        let sig: String = native_list_get(bfns, i + 1)
        let lname: String = str_to_lower(name)
        let include: Bool = false
        if prefix_empty { let include = true }
        if str_starts_with(lname, lower_prefix) { let include = true }
        if include {
            let s: Float = prefix_score(name, prefix) + 0.11
            let items = native_list_append(items, completion_json(name, "function", sig, "Built-in: " + name + " :: " + sig, s))
        }
        let i = i + 2
    }

    // User-defined functions
    let ufns: [String] = decls["fns"]
    let nuf: Int = native_list_len(ufns)
    let i: Int = 0
    while i < nuf {
        let name: String = native_list_get(ufns, i)
        let lname: String = str_to_lower(name)
        let include: Bool = false
        if prefix_empty { let include = true }
        if str_starts_with(lname, lower_prefix) { let include = true }
        if include {
            let s: Float = prefix_score(name, prefix) + 0.12
            let items = native_list_append(items, completion_json(name, "function", "fn " + name, "User function: " + name, s))
        }
        let i = i + 1
    }

    // Output is a list of pre-encoded JSON objects. Sorting by score is a
    // stretch goal — el's runtime has no sort-by-key primitive. Items are
    // already ordered by category (keywords first), which matches the most
    // common editor expectation.
    json_array_of(items)
}

// ── Public: hover ────────────────────────────────────────────────────────────

fn lsp_hover(source: String, pos: Int) -> String {
    let token: String = extract_token(source, pos)
    if str_eq(token, "") { return "null" }

    // Built-in primitives
    let doc: String = builtin_type_doc(token)
    if !str_eq(doc, "") {
        let parts: [String] = native_list_empty()
        let parts = native_list_append(parts, json_kv_str("type_name", token))
        let parts = native_list_append(parts, json_kv_str("documentation", doc))
        let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
        return "{" + list_join(parts, ",") + "}"
    }

    // User-declared types
    let decls: Map<String, Any> = scan_decls(source)
    let utypes: [String] = decls["types"]
    let ukinds: [String] = decls["type_kind"]
    let nu: Int = native_list_len(utypes)
    let i: Int = 0
    while i < nu {
        let name: String = native_list_get(utypes, i)
        if str_eq(name, token) {
            let kind: String = native_list_get(ukinds, i)
            let parts: [String] = native_list_empty()
            let parts = native_list_append(parts, json_kv_str("type_name", token))
            let parts = native_list_append(parts, json_kv_str("documentation", kind + " " + token))
            let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
            return "{" + list_join(parts, ",") + "}"
        }
        let i = i + 1
    }

    // User-declared functions
    let ufns: [String] = decls["fns"]
    let nuf: Int = native_list_len(ufns)
    let i: Int = 0
    while i < nuf {
        let name: String = native_list_get(ufns, i)
        if str_eq(name, token) {
            let parts: [String] = native_list_empty()
            let parts = native_list_append(parts, json_kv_str("type_name", token))
            let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token))
            let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
            return "{" + list_join(parts, ",") + "}"
        }
        let i = i + 1
    }

    // Built-in functions
    let bfns: [String] = builtin_fns()
    let nf: Int = native_list_len(bfns)
    let i: Int = 0
    while i < nf {
        let name: String = native_list_get(bfns, i)
        if str_eq(name, token) {
            let sig: String = native_list_get(bfns, i + 1)
            let parts: [String] = native_list_empty()
            let parts = native_list_append(parts, json_kv_str("type_name", token))
            let parts = native_list_append(parts, json_kv_str("documentation", "fn " + token + " :: " + sig))
            let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
            return "{" + list_join(parts, ",") + "}"
        }
        let i = i + 2
    }

    // Keyword
    let kdoc: String = keyword_doc(token)
    if !str_eq(kdoc, "") {
        let parts: [String] = native_list_empty()
        let parts = native_list_append(parts, json_kv_str("type_name", token))
        let parts = native_list_append(parts, json_kv_str("documentation", kdoc))
        let parts = native_list_append(parts, json_kv_raw("engram_node_type", "null"))
        return "{" + list_join(parts, ",") + "}"
    }

    "null"
}

// ── Public: diagnostics ──────────────────────────────────────────────────────
//
// Without the type checker port we surface only structural diagnostics:
//   * unterminated string literal
//   * unbalanced braces / parens / brackets
//   * unknown type referenced after `:` in a let binding (unknown to source +
//     builtins) — best-effort, conservative.

fn diag_json(message: String, severity: String, line: Int, col: Int) -> String {
    let parts: [String] = native_list_empty()
    let parts = native_list_append(parts, json_kv_str("message", message))
    let parts = native_list_append(parts, json_kv_str("severity", severity))
    if line > 0 {
        let parts = native_list_append(parts, json_kv_int("line", line))
        let parts = native_list_append(parts, json_kv_int("col", col))
    } else {
        let parts = native_list_append(parts, json_kv_raw("line", "null"))
        let parts = native_list_append(parts, json_kv_raw("col", "null"))
    }
    "{" + list_join(parts, ",") + "}"
}

fn lsp_diagnostics(source: String) -> String {
    let chars: [String] = native_string_chars(source)
    let n: Int = native_list_len(chars)
    let diags: [String] = native_list_empty()

    // Bracket balance + unterminated string scan
    let depth_brace: Int = 0
    let depth_paren: Int = 0
    let depth_brack: Int = 0
    let i: Int = 0
    let line: Int = 1
    let col: Int = 1
    let in_string: Bool = false
    let string_start_line: Int = 0
    let string_start_col: Int = 0
    let in_comment: Bool = false
    while i < n {
        let ch: String = native_list_get(chars, i)
        if in_comment {
            if ch == "\n" {
                let in_comment = false
                let line = line + 1
                let col = 1
            } else {
                let col = col + 1
            }
            let i = i + 1
        } else {
            if in_string {
                if ch == "\\" {
                    let i = i + 1
                    let col = col + 1
                    if i < n {
                        let esc: String = native_list_get(chars, i)
                        if esc == "\n" { let line = line + 1 let col = 1 } else { let col = col + 1 }
                        let i = i + 1
                    }
                } else { if ch == "\"" {
                    let in_string = false
                    let i = i + 1
                    let col = col + 1
                } else { if ch == "\n" {
                    let line = line + 1
                    let col = 1
                    let i = i + 1
                } else {
                    let col = col + 1
                    let i = i + 1
                } } }
            } else {
                if ch == "\"" {
                    let in_string = true
                    let string_start_line = line
                    let string_start_col = col
                    let i = i + 1
                    let col = col + 1
                } else { if ch == "/" {
                    let next_i: Int = i + 1
                    let is_line_comment: Bool = false
                    if next_i < n {
                        let next_ch: String = native_list_get(chars, next_i)
                        if next_ch == "/" { let is_line_comment = true }
                    }
                    if is_line_comment {
                        let in_comment = true
                        let i = i + 2
                        let col = col + 2
                    } else {
                        let i = i + 1
                        let col = col + 1
                    }
                } else { if ch == "{" {
                    let depth_brace = depth_brace + 1
                    let i = i + 1 let col = col + 1
                } else { if ch == "}" {
                    let depth_brace = depth_brace - 1
                    if depth_brace < 0 {
                        let diags = native_list_append(diags, diag_json("unmatched `}`", "error", line, col))
                        let depth_brace = 0
                    }
                    let i = i + 1 let col = col + 1
                } else { if ch == "(" {
                    let depth_paren = depth_paren + 1
                    let i = i + 1 let col = col + 1
                } else { if ch == ")" {
                    let depth_paren = depth_paren - 1
                    if depth_paren < 0 {
                        let diags = native_list_append(diags, diag_json("unmatched `)`", "error", line, col))
                        let depth_paren = 0
                    }
                    let i = i + 1 let col = col + 1
                } else { if ch == "[" {
                    let depth_brack = depth_brack + 1
                    let i = i + 1 let col = col + 1
                } else { if ch == "]" {
                    let depth_brack = depth_brack - 1
                    if depth_brack < 0 {
                        let diags = native_list_append(diags, diag_json("unmatched `]`", "error", line, col))
                        let depth_brack = 0
                    }
                    let i = i + 1 let col = col + 1
                } else { if ch == "\n" {
                    let line = line + 1
                    let col = 1
                    let i = i + 1
                } else {
                    let i = i + 1
                    let col = col + 1
                } } } } } } } } }
            }
        }
    }

    if in_string {
        let diags = native_list_append(diags, diag_json("unterminated string literal", "error", string_start_line, string_start_col))
    }
    if depth_brace > 0 { let diags = native_list_append(diags, diag_json("unclosed `{` (missing `}`)", "error", 0, 0)) }
    if depth_paren > 0 { let diags = native_list_append(diags, diag_json("unclosed `(` (missing `)`)", "error", 0, 0)) }
    if depth_brack > 0 { let diags = native_list_append(diags, diag_json("unclosed `[` (missing `]`)", "error", 0, 0)) }

    json_array_of(diags)
}

// ── Public: outline ──────────────────────────────────────────────────────────

fn outline_item_json(kind: String, name: String, line: Int) -> String {
    let parts: [String] = native_list_empty()
    let parts = native_list_append(parts, json_kv_str("kind", kind))
    let parts = native_list_append(parts, json_kv_str("name", name))
    let parts = native_list_append(parts, json_kv_int("line", line))
    "{" + list_join(parts, ",") + "}"
}

fn lsp_outline(source: String) -> String {
    let lines: [String] = str_split(source, "\n")
    let n: Int = native_list_len(lines)
    let items: [String] = native_list_empty()
    let i: Int = 0
    while i < n {
        let line: String = native_list_get(lines, i)
        let trimmed: String = str_trim(line)
        let lineno: Int = i + 1
        if str_starts_with(trimmed, "fn ") {
            let name: String = first_ident_in(str_slice(trimmed, 3, str_len(trimmed)))
            if !str_eq(name, "") {
                let items = native_list_append(items, outline_item_json("fn", name, lineno))
            }
        } else { if str_starts_with(trimmed, "type ") {
            let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed)))
            if !str_eq(name, "") {
                let items = native_list_append(items, outline_item_json("type", name, lineno))
            }
        } else { if str_starts_with(trimmed, "enum ") {
            let name: String = first_ident_in(str_slice(trimmed, 5, str_len(trimmed)))
            if !str_eq(name, "") {
                let items = native_list_append(items, outline_item_json("enum", name, lineno))
            }
        } else { if str_starts_with(trimmed, "protocol ") {
            let name: String = first_ident_in(str_slice(trimmed, 9, str_len(trimmed)))
            if !str_eq(name, "") {
                let items = native_list_append(items, outline_item_json("protocol", name, lineno))
            }
        } else { if str_starts_with(trimmed, "impl ") {
            let rest: String = str_trim(str_slice(trimmed, 5, str_len(trimmed)))
            let brace: Int = str_index_of(rest, "{")
            let name: String = rest
            if brace > 0 { let name = str_trim(str_slice(rest, 0, brace)) }
            if !str_eq(name, "") {
                let items = native_list_append(items, outline_item_json("impl", name, lineno))
            }
        } } } } }
        let i = i + 1
    }
    json_array_of(items)
}

// ── Public: format ───────────────────────────────────────────────────────────
//
// Idiomatic-El formatter. Until `el fmt` is a real thing, we apply a minimal
// canonicalisation pass:
//   * trim trailing whitespace on every line
//   * collapse 3+ blank lines to 2
//   * ensure file ends with exactly one newline
// This is safe for round-tripping and a noticeable quality-of-life win in
// the editor.

fn format_strip_trailing(line: String) -> String {
    // str_trim trims both ends; we want only trailing whitespace stripped.
    let n: Int = str_len(line)
    if n == 0 { return line }
    let chars: [String] = native_string_chars(line)
    let i: Int = n - 1
    let trimming: Bool = true
    while trimming {
        if i < 0 {
            let trimming = false
        } else {
            let ch: String = native_list_get(chars, i)
            if ch == " " { let i = i - 1 } else { if ch == "\t" { let i = i - 1 } else { let trimming = false } }
        }
    }
    str_slice(line, 0, i + 1)
}

fn lsp_format(source: String) -> String {
    let lines: [String] = str_split(source, "\n")
    let n: Int = native_list_len(lines)
    let cleaned: [String] = native_list_empty()
    let i: Int = 0
    let blank_run: Int = 0
    while i < n {
        let line: String = native_list_get(lines, i)
        let stripped: String = format_strip_trailing(line)
        if str_eq(stripped, "") {
            let blank_run = blank_run + 1
            if blank_run <= 2 {
                let cleaned = native_list_append(cleaned, "")
            }
        } else {
            let blank_run = 0
            let cleaned = native_list_append(cleaned, stripped)
        }
        let i = i + 1
    }
    let joined: String = list_join(cleaned, "\n")
    // Ensure trailing newline.
    let final: String = joined
    if !str_ends_with(final, "\n") {
        let final = final + "\n"
    }
    let changed: String = "false"
    if !str_eq(final, source) { let changed = "true" }
    "{" + json_kv_str("content", final) + "," + json_kv_raw("changed", changed) + "}"
}

// ── Public: type graph ───────────────────────────────────────────────────────

fn lsp_type_graph(source: String) -> String {
    let nodes: [String] = native_list_empty()
    let edges: [String] = native_list_empty()

    // Built-in nodes
    let bts: [String] = builtin_type_names()
    let nb: Int = native_list_len(bts)
    let i: Int = 0
    while i < nb {
        let name: String = native_list_get(bts, i)
        let parts: [String] = native_list_empty()
        let parts = native_list_append(parts, json_kv_str("id", name))
        let parts = native_list_append(parts, json_kv_str("name", name))
        let parts = native_list_append(parts, json_kv_str("kind", "builtin"))
        let parts = native_list_append(parts, json_kv_raw("fields", "[]"))
        let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}")
        let i = i + 1
    }

    // User-declared types
    let decls: Map<String, Any> = scan_decls(source)
    let utypes: [String] = decls["types"]
    let ukinds: [String] = decls["type_kind"]
    let tlines: [Int] = decls["type_lines"]
    let nu: Int = native_list_len(utypes)
    let i: Int = 0
    let bts_dup: [String] = builtin_type_names()
    while i < nu {
        let name: String = native_list_get(utypes, i)
        let kind: String = native_list_get(ukinds, i)
        // Skip if already a builtin (paranoia)
        if !list_contains_str(bts_dup, name) {
            let fields: [String] = collect_struct_fields(source, name)
            let field_jsons: [String] = native_list_empty()
            let fn2: Int = native_list_len(fields)
            let j: Int = 0
            while j < fn2 {
                let field_jsons = native_list_append(field_jsons, json_str(native_list_get(fields, j)))
                let j = j + 1
            }
            let parts: [String] = native_list_empty()
            let parts = native_list_append(parts, json_kv_str("id", name))
            let parts = native_list_append(parts, json_kv_str("name", name))
            let parts = native_list_append(parts, json_kv_str("kind", kind))
            let parts = native_list_append(parts, json_kv_raw("fields", json_array_of(field_jsons)))
            let nodes = native_list_append(nodes, "{" + list_join(parts, ",") + "}")

            // Edges: scan each field for a known type reference.
            let j: Int = 0
            while j < fn2 {
                let field_str: String = native_list_get(fields, j)
                let target: String = referenced_type(field_str, utypes)
                if !str_eq(target, "") {
                    let eparts: [String] = native_list_empty()
                    let eparts = native_list_append(eparts, json_kv_str("from", name))
                    let eparts = native_list_append(eparts, json_kv_str("to", target))
                    let eparts = native_list_append(eparts, json_kv_str("label", "field"))
                    let edges = native_list_append(edges, "{" + list_join(eparts, ",") + "}")
                }
                let j = j + 1
            }
        }
        let i = i + 1
    }

    "{" + json_kv_raw("nodes", json_array_of(nodes)) + "," + json_kv_raw("edges", json_array_of(edges)) + "}"
}

// Extract the field list of a `type Name { ... }` block as ["field: Type", ...]
// or for an enum, the variant labels. Best-effort line-by-line scan.
fn collect_struct_fields(source: String, type_name: String) -> [String] {
    let lines: [String] = str_split(source, "\n")
    let n: Int = native_list_len(lines)
    let fields: [String] = native_list_empty()
    let inside: Bool = false
    let i: Int = 0
    let header_a: String = "type " + type_name
    let header_b: String = "enum " + type_name
    let header_c: String = "protocol " + type_name
    while i < n {
        let line: String = native_list_get(lines, i)
        let trimmed: String = str_trim(line)
        if !inside {
            if str_starts_with(trimmed, header_a) { let inside = true }
            if str_starts_with(trimmed, header_b) { let inside = true }
            if str_starts_with(trimmed, header_c) { let inside = true }
        } else {
            if str_starts_with(trimmed, "}") {
                return fields
            }
            // skip the opening "{" line
            if !str_eq(trimmed, "{") {
                if !str_eq(trimmed, "") {
                    if !str_starts_with(trimmed, "//") {
                        // strip a trailing comma if present
                        let entry: String = trimmed
                        if str_ends_with(entry, ",") {
                            let entry = str_slice(entry, 0, str_len(entry) - 1)
                        }
                        let fields = native_list_append(fields, str_trim(entry))
                    }
                }
            }
        }
        let i = i + 1
    }
    fields
}

// Pick the first known type name out of a `field: Type` string. Stops at non-ident.
fn referenced_type(field_str: String, known_types: [String]) -> String {
    // Find ":" — type follows.
    let colon: Int = str_index_of(field_str, ":")
    if colon < 0 { return "" }
    let after: String = str_trim(str_slice(field_str, colon + 1, str_len(field_str)))
    // Strip array brackets and optional marker.
    let trimmed_l: String = after
    if str_starts_with(trimmed_l, "[") {
        let end: Int = str_index_of(trimmed_l, "]")
        if end > 1 {
            let trimmed_l = str_trim(str_slice(trimmed_l, 1, end))
        }
    }
    // First identifier.
    let ident: String = first_ident_in(trimmed_l)
    if str_eq(ident, "") { return "" }
    let bts: [String] = builtin_type_names()
    if list_contains_str(bts, ident) { return ident }
    if list_contains_str(known_types, ident) { return ident }
    ""
}

// ── Public: definition / references ──────────────────────────────────────────

fn lsp_definition(source: String, pos: Int) -> String {
    let word: String = extract_token(source, pos)
    if str_eq(word, "") { return "null" }
    let patterns: [String] = native_list_empty()
    let patterns = native_list_append(patterns, "fn " + word)
    let patterns = native_list_append(patterns, "type " + word)
    let patterns = native_list_append(patterns, "enum " + word)
    let patterns = native_list_append(patterns, "protocol " + word)
    let np: Int = native_list_len(patterns)
    let lines: [String] = str_split(source, "\n")
    let nl: Int = native_list_len(lines)
    let i: Int = 0
    while i < nl {
        let line: String = native_list_get(lines, i)
        let j: Int = 0
        while j < np {
            let pat: String = native_list_get(patterns, j)
            let idx: Int = str_index_of(line, pat)
            if idx >= 0 {
                let parts: [String] = native_list_empty()
                let parts = native_list_append(parts, json_kv_int("line", i + 1))
                let parts = native_list_append(parts, json_kv_int("col", idx + 1))
                let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line)))
                return "{" + list_join(parts, ",") + "}"
            }
            let j = j + 1
        }
        let i = i + 1
    }
    "null"
}

fn lsp_references(source: String, pos: Int) -> String {
    let word: String = extract_token(source, pos)
    if str_eq(word, "") { return "[]" }
    let lines: [String] = str_split(source, "\n")
    let nl: Int = native_list_len(lines)
    let refs: [String] = native_list_empty()
    let i: Int = 0
    while i < nl {
        let line: String = native_list_get(lines, i)
        let line_chars: [String] = native_string_chars(line)
        let llen: Int = native_list_len(line_chars)
        let wlen: Int = str_len(word)
        let off: Int = 0
        let scanning: Bool = true
        while scanning {
            let idx: Int = str_index_of(str_slice(line, off, str_len(line)), word)
            if idx < 0 {
                let scanning = false
            } else {
                let abs: Int = off + idx
                // Word-boundary check
                let before_ok: Bool = true
                if abs > 0 {
                    let prev: String = native_list_get(line_chars, abs - 1)
                    if is_ident_ch(prev) { let before_ok = false }
                }
                let after_ok: Bool = true
                let after_pos: Int = abs + wlen
                if after_pos < llen {
                    let nxt: String = native_list_get(line_chars, after_pos)
                    if is_ident_ch(nxt) { let after_ok = false }
                }
                if before_ok {
                    if after_ok {
                        let parts: [String] = native_list_empty()
                        let parts = native_list_append(parts, json_kv_int("line", i + 1))
                        let parts = native_list_append(parts, json_kv_int("col", abs + 1))
                        let parts = native_list_append(parts, json_kv_str("snippet", str_trim(line)))
                        let refs = native_list_append(refs, "{" + list_join(parts, ",") + "}")
                    }
                }
                let off = abs + wlen
                if off >= str_len(line) { let scanning = false }
            }
        }
        let i = i + 1
    }
    json_array_of(refs)
}

// ── JSON-RPC dispatch ────────────────────────────────────────────────────────
//
// Method names follow LSP-ish conventions but stay aligned with the Rust crate's
// HTTP surface. Params are a JSON string the dispatcher pulls fields out of via
// json_get_string / json_get_int.
//
//   "textDocument/completion"   → params { source, pos }
//   "textDocument/hover"        → params { source, pos }
//   "textDocument/diagnostic"   → params { source }
//   "textDocument/formatting"   → params { source }
//   "textDocument/documentSymbol"→ params { source }
//   "textDocument/definition"   → params { source, pos }
//   "textDocument/references"   → params { source, pos }
//   "el/typeGraph"              → params { source }

fn lsp_jsonrpc(method: String, params_json: String) -> String {
    let source: String = json_get_string(params_json, "source")
    let pos: Int = json_get_int(params_json, "pos")
    if str_eq(method, "textDocument/completion") { return lsp_complete(source, pos) }
    if str_eq(method, "textDocument/hover") { return lsp_hover(source, pos) }
    if str_eq(method, "textDocument/diagnostic") { return lsp_diagnostics(source) }
    if str_eq(method, "textDocument/formatting") { return lsp_format(source) }
    if str_eq(method, "textDocument/documentSymbol") { return lsp_outline(source) }
    if str_eq(method, "textDocument/definition") { return lsp_definition(source, pos) }
    if str_eq(method, "textDocument/references") { return lsp_references(source, pos) }
    if str_eq(method, "el/typeGraph") { return lsp_type_graph(source) }
    "{\"error\":\"unknown method: " + json_escape(method) + "\"}"
}

// ── stdio loop (best-effort; see runtime gap note in the report) ─────────────
//
// LSP framing is `Content-Length: N\r\n\r\n` then exactly N bytes. The El
// runtime exposes only line-buffered `readline()`, which strips `\n` and is
// hostile to the binary body read. We approximate by reading lines until a
// blank line ends the headers, then reading exactly one more line as the
// body (works for compact, no-newline payloads — i.e. minified JSON-RPC).
// A dedicated `read_n(bytes)` runtime primitive would unblock the strict
// implementation. See report.

fn read_headers() -> Map<String, Any> {
    let content_len: Int = 0
    let scanning: Bool = true
    while scanning {
        let line: String = readline()
        if str_eq(line, "") {
            let scanning = false
        } else {
            let cl_marker: String = "Content-Length:"
            if str_starts_with(line, cl_marker) {
                let val: String = str_trim(str_slice(line, str_len(cl_marker), str_len(line)))
                let content_len = str_to_int(val)
            }
        }
    }
    { "content_length": content_len }
}

fn lsp_stdio_loop() -> Void {
    let running: Bool = true
    while running {
        let headers: Map<String, Any> = read_headers()
        let body: String = readline()
        if str_eq(body, "") {
            let running = false
        } else {
            let method: String = json_get_string(body, "method")
            let id_val: String = json_get_raw(body, "id")
            if str_eq(id_val, "") { let id_val = "null" }
            let params_raw: String = json_get_raw(body, "params")
            let result: String = lsp_jsonrpc(method, params_raw)
            let resp: String = "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}"
            let frame: String = "Content-Length: " + int_to_str(str_len(resp)) + "\r\n\r\n" + resp
            print(frame)
        }
    }
}

// ── Entry ────────────────────────────────────────────────────────────────────
//
// When invoked as a standalone binary, default to stdio JSON-RPC so editors
// that drive the language server directly (without the HTTP wrapper) work
// out of the box. Pass a single CLI arg to short-circuit:
//   ./el-lsp probe       → emits a self-test diagnostic + exits
//   ./el-lsp version     → emits version + exits

fn lsp_main() -> Void {
    let argv: [String] = args()
    let n: Int = native_list_len(argv)
    if n > 0 {
        let cmd: String = native_list_get(argv, 0)
        if str_eq(cmd, "version") {
            println("el-lsp 0.1.0")
            return
        }
        if str_eq(cmd, "probe") {
            let sample: String = "fn main() -> Void { let x: Int = 42 }"
            println(lsp_diagnostics(sample))
            return
        }
    }
    lsp_stdio_loop()
}


// main.el — el-plugin-host
//
// Plugin lifecycle manager for el-ide. Pure-El port of the el-plugin-host
// Rust crate.
//
// Plugins are stored in process-global state via state_get / state_set.
// Each plugin lives under a key prefix `plugin/<name>/<field>`. The
// canonical list of plugin names is maintained at `plugins/index` as a
// comma-separated string (we don't have list serialisation in state yet).
//
// Public API:
//   plugin_init()                          — seed first-party plugins (idempotent)
//   plugin_list_json()       -> String     — JSON array of all plugins
//   plugin_get_json(name)    -> String     — JSON object or "null"
//   plugin_install(name)     -> String     — JSON { ok | error }
//   plugin_remove(name)      -> String
//   plugin_enable(name)      -> String
//   plugin_disable(name)     -> String

// ── State helpers (string K/V) ───────────────────────────────────────────────

fn pkey(name: String, field: String) -> String {
    "plugin/" + name + "/" + field
}

fn put_str(name: String, field: String, value: String) -> Void {
    state_set(pkey(name, field), value)
}

fn put_bool(name: String, field: String, value: Bool) -> Void {
    if value { state_set(pkey(name, field), "true") }
    if !value { state_set(pkey(name, field), "false") }
}

fn get_str(name: String, field: String) -> String {
    state_get(pkey(name, field))
}

fn get_bool(name: String, field: String) -> Bool {
    str_eq(state_get(pkey(name, field)), "true")
}

// Plugin index: comma-separated list of names at "plugins/index".
fn index_list() -> [String] {
    let raw: String = state_get("plugins/index")
    if str_eq(raw, "") { return native_list_empty() }
    str_split(raw, ",")
}

fn index_set(names: [String]) -> Void {
    state_set("plugins/index", list_join(names, ","))
}

fn index_contains(name: String) -> Bool {
    let names: [String] = index_list()
    let n: Int = native_list_len(names)
    let i: Int = 0
    while i < n {
        if str_eq(native_list_get(names, i), name) { return true }
        let i = i + 1
    }
    false
}

fn index_add(name: String) -> Void {
    if !index_contains(name) {
        let names: [String] = index_list()
        let names = native_list_append(names, name)
        index_set(names)
    }
}

// ── Initialisation ───────────────────────────────────────────────────────────

fn register_plugin(name: String, version: String, description: String,
                   installed: Bool, enabled: Bool, hooks: String,
                   first_party: Bool) -> Void {
    put_str(name, "name", name)
    put_str(name, "version", version)
    put_str(name, "description", description)
    put_bool(name, "installed", installed)
    put_bool(name, "enabled", enabled)
    put_str(name, "hooks", hooks)
    put_bool(name, "first_party", first_party)
    index_add(name)
}

fn plugin_init() -> Void {
    if str_eq(state_get("plugins/initialised"), "true") {
        return
    }
    register_plugin("el-theme-dark",  "1.0.0", "Dark theme — the default Engram IDE theme.",                                   true,  true,  "",                          true)
    register_plugin("el-theme-light", "1.0.0", "Light theme for high-ambient-light environments.",                              false, false, "",                          true)
    register_plugin("el-fmt",         "0.1.0", "Code formatter — auto-formats .el files on save.",                              true,  true,  "on_save",                   true)
    register_plugin("el-doc",         "0.1.0", "Documentation generator — produces HTML docs from type definitions.",          false, false, "custom_tool",               true)
    register_plugin("el-test",        "0.1.0", "Test runner with inline results displayed in the editor gutter.",              false, false, "on_build_complete,custom_panel", true)
    state_set("plugins/initialised", "true")
}

// ── Serialisation ────────────────────────────────────────────────────────────

fn json_escape_str(s: String) -> String {
    let chars: [String] = native_string_chars(s)
    let n: Int = native_list_len(chars)
    let out: String = ""
    let i: Int = 0
    while i < n {
        let ch: String = native_list_get(chars, i)
        if ch == "\"" { let out = out + "\\\"" }
        else { if ch == "\\" { let out = out + "\\\\" }
        else { if ch == "\n" { let out = out + "\\n" }
        else { let out = out + ch } } }
        let i = i + 1
    }
    out
}

fn quote_json(s: String) -> String {
    "\"" + json_escape_str(s) + "\""
}

fn hooks_to_json(hooks_csv: String) -> String {
    if str_eq(hooks_csv, "") { return "[]" }
    let xs: [String] = str_split(hooks_csv, ",")
    let n: Int = native_list_len(xs)
    let out: String = "["
    let i: Int = 0
    while i < n {
        if i > 0 { let out = out + "," }
        let out = out + quote_json(native_list_get(xs, i))
        let i = i + 1
    }
    out + "]"
}

fn plugin_to_json(name: String) -> String {
    let parts: [String] = native_list_empty()
    let parts = native_list_append(parts, "\"name\":" + quote_json(get_str(name, "name")))
    let parts = native_list_append(parts, "\"version\":" + quote_json(get_str(name, "version")))
    let parts = native_list_append(parts, "\"description\":" + quote_json(get_str(name, "description")))
    let installed: String = get_str(name, "installed")
    let enabled: String = get_str(name, "enabled")
    let first_party: String = get_str(name, "first_party")
    let parts = native_list_append(parts, "\"installed\":" + installed)
    let parts = native_list_append(parts, "\"enabled\":" + enabled)
    let parts = native_list_append(parts, "\"hooks\":" + hooks_to_json(get_str(name, "hooks")))
    let parts = native_list_append(parts, "\"first_party\":" + first_party)
    "{" + list_join(parts, ",") + "}"
}

fn plugin_list_json() -> String {
    plugin_init()
    let names: [String] = index_list()
    let n: Int = native_list_len(names)
    let out: String = "["
    let i: Int = 0
    while i < n {
        if i > 0 { let out = out + "," }
        let out = out + plugin_to_json(native_list_get(names, i))
        let i = i + 1
    }
    out + "]"
}

fn plugin_get_json(name: String) -> String {
    plugin_init()
    if !index_contains(name) { return "null" }
    plugin_to_json(name)
}

// ── Mutations ────────────────────────────────────────────────────────────────

fn plugin_install(name: String) -> String {
    plugin_init()
    if !index_contains(name) {
        return "{\"error\":\"plugin registry is not available (registry URL not configured)\"}"
    }
    if get_bool(name, "installed") {
        return "{\"error\":\"plugin '" + json_escape_str(name) + "' is already installed\"}"
    }
    put_bool(name, "installed", true)
    put_bool(name, "enabled", true)
    plugin_to_json(name)
}

fn plugin_remove(name: String) -> String {
    plugin_init()
    if !index_contains(name) {
        return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}"
    }
    if str_eq(name, "el-theme-dark") {
        return "{\"error\":\"plugin '" + json_escape_str(name) + "' cannot be removed: it is a required built-in\"}"
    }
    put_bool(name, "installed", false)
    put_bool(name, "enabled", false)
    "{\"ok\":true}"
}

fn plugin_enable(name: String) -> String {
    plugin_init()
    if !index_contains(name) {
        return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}"
    }
    put_bool(name, "enabled", true)
    "{\"ok\":true}"
}

fn plugin_disable(name: String) -> String {
    plugin_init()
    if !index_contains(name) {
        return "{\"error\":\"plugin '" + json_escape_str(name) + "' not found\"}"
    }
    put_bool(name, "enabled", false)
    "{\"ok\":true}"
}

// ── Standalone CLI ───────────────────────────────────────────────────────────

fn host_main() -> Void {
    let argv: [String] = args()
    let n: Int = native_list_len(argv)
    if n == 0 {
        println(plugin_list_json())
        return
    }
    let cmd: String = native_list_get(argv, 0)
    if str_eq(cmd, "list") { println(plugin_list_json()) return }
    if str_eq(cmd, "get") {
        if n >= 2 { println(plugin_get_json(native_list_get(argv, 1))) }
        return
    }
    if str_eq(cmd, "install") {
        if n >= 2 { println(plugin_install(native_list_get(argv, 1))) }
        return
    }
    if str_eq(cmd, "remove") {
        if n >= 2 { println(plugin_remove(native_list_get(argv, 1))) }
        return
    }
    if str_eq(cmd, "enable") {
        if n >= 2 { println(plugin_enable(native_list_get(argv, 1))) }
        return
    }
    if str_eq(cmd, "disable") {
        if n >= 2 { println(plugin_disable(native_list_get(argv, 1))) }
        return
    }
    println("usage: el-plugin-host [list|get|install|remove|enable|disable] <name>")
}


// main.el — el-ide-server
//
// HTTP backend for the Engram IDE. Pure-El port of the el-ide-server Rust
// crate. Wires the el-lsp and el-plugin-host vessels together and serves
// the IDE-frontend protocol used by the React-style frontend.
//
// El compilation note: `import` statements are parsed but the codegen
// concatenates nothing. The build script for this vessel concatenates
//
//     vessels/el-lsp/src/main.el       (without the trailing entry call)
//     vessels/el-plugin-host/src/main.el
//     vessels/el-ide-server/src/main.el (this file)
//
// into a single _combined.el before invoking elc. This file therefore
// assumes the lsp_* and plugin_* functions defined in those siblings are
// in scope. See README / build.sh.
//
// All routes serve JSON over HTTP. Query parameters are parsed by hand
// (the runtime gives us strings and a request body). Streaming endpoints
// (SSE) are emulated by returning the full output once — the runtime's
// http_serve handler signature is response-as-string, so true server-sent
// streaming requires a runtime extension. Flagged in the report.

// ── Tiny utility layer ───────────────────────────────────────────────────────

fn jstr(s: String) -> String {
    let chars: [String] = native_string_chars(s)
    let n: Int = native_list_len(chars)
    let out: String = "\""
    let i: Int = 0
    while i < n {
        let ch: String = native_list_get(chars, i)
        if ch == "\"" { let out = out + "\\\"" }
        else { if ch == "\\" { let out = out + "\\\\" }
        else { if ch == "\n" { let out = out + "\\n" }
        else { if ch == "\r" { let out = out + "\\r" }
        else { if ch == "\t" { let out = out + "\\t" }
        else { let out = out + ch } } } } }
        let i = i + 1
    }
    out + "\""
}

fn jerr(msg: String) -> String {
    "{\"error\":" + jstr(msg) + "}"
}

fn ok_obj() -> String {
    "{\"ok\":true}"
}

// ── URL/query parsing ────────────────────────────────────────────────────────

fn url_decode(s: String) -> String {
    let chars: [String] = native_string_chars(s)
    let n: Int = native_list_len(chars)
    let out: String = ""
    let i: Int = 0
    while i < n {
        let ch: String = native_list_get(chars, i)
        if ch == "+" {
            let out = out + " "
            let i = i + 1
        } else { if ch == "%" {
            // Read two hex digits
            if i + 2 < n {
                let hex: String = native_list_get(chars, i + 1) + native_list_get(chars, i + 2)
                let code: Int = hex_to_int(hex)
                let out = out + char_from_code(code)
                let i = i + 3
            } else {
                let out = out + ch
                let i = i + 1
            }
        } else {
            let out = out + ch
            let i = i + 1
        } }
    }
    out
}

fn hex_digit(ch: String) -> Int {
    if ch == "0" { return 0 } if ch == "1" { return 1 }
    if ch == "2" { return 2 } if ch == "3" { return 3 }
    if ch == "4" { return 4 } if ch == "5" { return 5 }
    if ch == "6" { return 6 } if ch == "7" { return 7 }
    if ch == "8" { return 8 } if ch == "9" { return 9 }
    if ch == "a" { return 10 } if ch == "A" { return 10 }
    if ch == "b" { return 11 } if ch == "B" { return 11 }
    if ch == "c" { return 12 } if ch == "C" { return 12 }
    if ch == "d" { return 13 } if ch == "D" { return 13 }
    if ch == "e" { return 14 } if ch == "E" { return 14 }
    if ch == "f" { return 15 } if ch == "F" { return 15 }
    -1
}

fn hex_to_int(hex: String) -> Int {
    let chars: [String] = native_string_chars(hex)
    let n: Int = native_list_len(chars)
    let acc: Int = 0
    let i: Int = 0
    while i < n {
        let d: Int = hex_digit(native_list_get(chars, i))
        if d < 0 { return acc }
        let acc = acc * 16 + d
        let i = i + 1
    }
    acc
}

// Map an ASCII code point back to its single-byte string. Above 127 the
// runtime would need char_from_code(>127) → multi-byte; we only need
// printable ASCII for query strings here.
fn char_from_code(code: Int) -> String {
    let table: String = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
    if code < 32 { return " " }
    if code > 126 { return "?" }
    str_slice(table, code - 32, code - 31)
}

fn split_path_query(path: String) -> Map<String, Any> {
    let q: Int = str_index_of(path, "?")
    if q < 0 { return { "path": path, "query": "" } }
    { "path": str_slice(path, 0, q), "query": str_slice(path, q + 1, str_len(path)) }
}

// query_get: pull the value of a single key out of `a=1&b=hello%20world`.
fn query_get(query: String, key: String) -> String {
    if str_eq(query, "") { return "" }
    let pairs: [String] = str_split(query, "&")
    let n: Int = native_list_len(pairs)
    let prefix: String = key + "="
    let i: Int = 0
    while i < n {
        let pair: String = native_list_get(pairs, i)
        if str_starts_with(pair, prefix) {
            return url_decode(str_slice(pair, str_len(prefix), str_len(pair)))
        }
        if str_eq(pair, key) {
            return ""
        }
        let i = i + 1
    }
    ""
}

// ── Configuration ────────────────────────────────────────────────────────────

fn cfg_port() -> Int {
    let p: String = env("EL_IDE_PORT")
    if str_eq(p, "") { return 7771 }
    str_to_int(p)
}

fn cfg_project_path() -> String {
    let p: String = env("EL_IDE_PROJECT_PATH")
    if str_eq(p, "") { return "." }
    p
}

fn cfg_engram_url() -> String {
    let u: String = env("EL_ENGRAM_URL")
    if str_eq(u, "") { return "http://localhost:8742" }
    u
}

fn cfg_el_binary() -> String {
    let b: String = env("EL_BINARY")
    if str_eq(b, "") { return "el" }
    b
}

// ── Path safety ──────────────────────────────────────────────────────────────

fn safe_join(rel: String) -> String {
    let root: String = cfg_project_path()
    // Reject obvious traversals.
    if str_contains(rel, "..") { return "" }
    if str_starts_with(rel, "/") { return "" }
    if str_eq(rel, ".") { return root }
    root + "/" + rel
}

// ── Status route ─────────────────────────────────────────────────────────────

fn route_status() -> String {
    let parts: [String] = native_list_empty()
    let parts = native_list_append(parts, "\"project_name\":" + jstr(cfg_project_path()))
    let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path()))
    let parts = native_list_append(parts, "\"version\":" + jstr("0.1.0"))
    let parts = native_list_append(parts, "\"engine\":" + jstr("el-ide"))
    "{" + list_join(parts, ",") + "}"
}

// ── Config route ─────────────────────────────────────────────────────────────

fn route_config() -> String {
    let parts: [String] = native_list_empty()
    let parts = native_list_append(parts, "\"port\":" + int_to_str(cfg_port()))
    let parts = native_list_append(parts, "\"project_path\":" + jstr(cfg_project_path()))
    let parts = native_list_append(parts, "\"engram_url\":" + jstr(cfg_engram_url()))
    let parts = native_list_append(parts, "\"el_binary\":" + jstr(cfg_el_binary()))
    "{" + list_join(parts, ",") + "}"
}

// ── File API ─────────────────────────────────────────────────────────────────

fn route_files_list(query: String) -> String {
    let rel: String = query_get(query, "path")
    if str_eq(rel, "") { let rel = "." }
    let target: String = safe_join(rel)
    if str_eq(target, "") { return jerr("path escapes project root") }
    let entries: [String] = fs_list(target)
    let n: Int = native_list_len(entries)
    let out: String = "["
    let i: Int = 0
    while i < n {
        let name: String = native_list_get(entries, i)
        // Skip hidden + noise dirs
        let skip: Bool = false
        if str_starts_with(name, ".") { let skip = true }
        if str_eq(name, "target") { let skip = true }
        if str_eq(name, "node_modules") { let skip = true }
        if str_eq(name, "dist") { let skip = true }
        if !skip {
            if i > 0 {
                if !str_eq(out, "[") { let out = out + "," }
            }
            // Approximate is_dir by checking for an extension
            let dot: Int = str_index_of(name, ".")
            let is_dir: String = "false"
            if dot < 0 { let is_dir = "true" }
            let out = out + "{\"name\":" + jstr(name) + ",\"path\":" + jstr(rel + "/" + name) + ",\"is_dir\":" + is_dir + ",\"children\":null}"
        }
        let i = i + 1
    }
    out + "]"
}

fn route_file_read(query: String) -> String {
    let rel: String = query_get(query, "path")
    if str_eq(rel, "") { return jerr("missing path parameter") }
    let target: String = safe_join(rel)
    if str_eq(target, "") { return jerr("path escapes project root") }
    let content: String = fs_read(target)
    "{\"path\":" + jstr(rel) + ",\"content\":" + jstr(content) + "}"
}

fn route_file_write(body: String) -> String {
    let rel: String = json_get_string(body, "path")
    let content: String = json_get_string(body, "content")
    if str_eq(rel, "") { return jerr("missing path") }
    let target: String = safe_join(rel)
    if str_eq(target, "") { return jerr("path escapes project root") }
    let ok: Bool = fs_write(target, content)
    if ok { return ok_obj() }
    jerr("cannot write file")
}

// ── LSP routes (delegate to inlined lsp_* functions) ─────────────────────────

fn route_lsp_complete(query: String) -> String {
    let source: String = query_get(query, "source")
    let pos: Int = str_to_int(query_get(query, "pos"))
    lsp_complete(source, pos)
}

fn route_lsp_hover(query: String) -> String {
    let source: String = query_get(query, "source")
    let pos: Int = str_to_int(query_get(query, "pos"))
    lsp_hover(source, pos)
}

fn route_lsp_errors(query: String) -> String {
    let source: String = query_get(query, "source")
    lsp_diagnostics(source)
}

fn route_lsp_jsonrpc(body: String) -> String {
    let method: String = json_get_string(body, "method")
    let params_raw: String = json_get_raw(body, "params")
    let id_val: String = json_get_raw(body, "id")
    if str_eq(id_val, "") { let id_val = "null" }
    let result: String = lsp_jsonrpc(method, params_raw)
    "{\"jsonrpc\":\"2.0\",\"id\":" + id_val + ",\"result\":" + result + "}"
}

fn route_outline(query: String) -> String {
    let source: String = query_get(query, "source")
    lsp_outline(source)
}

fn route_definition(query: String) -> String {
    let source: String = query_get(query, "source")
    let pos: Int = str_to_int(query_get(query, "pos"))
    lsp_definition(source, pos)
}

fn route_references(query: String) -> String {
    let source: String = query_get(query, "source")
    let pos: Int = str_to_int(query_get(query, "pos"))
    lsp_references(source, pos)
}

fn route_format(body: String) -> String {
    let content: String = json_get_string(body, "content")
    lsp_format(content)
}

fn route_type_graph(query: String) -> String {
    let source: String = query_get(query, "source")
    lsp_type_graph(source)
}

// ── Themes ───────────────────────────────────────────────────────────────────

fn active_theme() -> String {
    let t: String = state_get("ide/active_theme")
    if str_eq(t, "") { return "dark" }
    t
}

fn route_themes_list() -> String {
    let active: String = active_theme()
    let names: [String] = native_list_empty()
    let names = native_list_append(names, "dark")
    let names = native_list_append(names, "light")
    let names = native_list_append(names, "neuron")
    let names = native_list_append(names, "high-contrast")
    let labels: [String] = native_list_empty()
    let labels = native_list_append(labels, "Dark")
    let labels = native_list_append(labels, "Light")
    let labels = native_list_append(labels, "Neuron")
    let labels = native_list_append(labels, "High Contrast")
    let n: Int = native_list_len(names)
    let out: String = "["
    let i: Int = 0
    while i < n {
        if i > 0 { let out = out + "," }
        let nm: String = native_list_get(names, i)
        let lbl: String = native_list_get(labels, i)
        let act: String = "false"
        if str_eq(nm, active) { let act = "true" }
        let out = out + "{\"name\":" + jstr(nm) + ",\"label\":" + jstr(lbl) + ",\"active\":" + act + "}"
        let i = i + 1
    }
    out + "]"
}

fn route_themes_set(body: String) -> String {
    let name: String = json_get_string(body, "name")
    let valid: Bool = false
    if str_eq(name, "dark") { let valid = true }
    if str_eq(name, "light") { let valid = true }
    if str_eq(name, "neuron") { let valid = true }
    if str_eq(name, "high-contrast") { let valid = true }
    if !valid { return jerr("unknown theme: " + name) }
    state_set("ide/active_theme", name)
    "{\"ok\":true,\"active\":" + jstr(name) + "}"
}

// ── Plugins (delegate to plugin_* in plugin-host) ────────────────────────────

fn route_plugins_list() -> String { plugin_list_json() }

fn route_plugins_install(body: String) -> String {
    plugin_install(json_get_string(body, "name"))
}

fn route_plugins_remove(body: String) -> String {
    plugin_remove(json_get_string(body, "name"))
}

fn route_plugins_enable(body: String) -> String {
    plugin_enable(json_get_string(body, "name"))
}

fn route_plugins_disable(body: String) -> String {
    plugin_disable(json_get_string(body, "name"))
}

// ── Snippets ─────────────────────────────────────────────────────────────────

fn route_snippets() -> String {
    let snips: [String] = native_list_empty()
    let snips = native_list_append(snips, "{\"label\":\"fn\",\"description\":\"Function definition\",\"body\":\"fn ${1:name}(${2:params}) -> ${3:Ret} {\\n\\t${4:body}\\n}\"}")
    let snips = native_list_append(snips, "{\"label\":\"type\",\"description\":\"Struct\",\"body\":\"type ${1:Name} {\\n\\t${2:field}: ${3:Type},\\n}\"}")
    let snips = native_list_append(snips, "{\"label\":\"enum\",\"description\":\"Enum\",\"body\":\"enum ${1:Name} {\\n\\t${2:Variant},\\n}\"}")
    let snips = native_list_append(snips, "{\"label\":\"match\",\"description\":\"Match expression\",\"body\":\"match ${1:expr} {\\n\\t${2:pat} => ${3:body},\\n\\t_ => ${4:default},\\n}\"}")
    let snips = native_list_append(snips, "{\"label\":\"let\",\"description\":\"Let binding\",\"body\":\"let ${1:name}: ${2:Type} = ${3:value}\"}")
    let snips = native_list_append(snips, "{\"label\":\"if\",\"description\":\"If expression\",\"body\":\"if ${1:cond} {\\n\\t${2:body}\\n}\"}")
    let snips = native_list_append(snips, "{\"label\":\"for\",\"description\":\"For loop\",\"body\":\"for ${1:item} in ${2:iter} {\\n\\t${3:body}\\n}\"}")
    let snips = native_list_append(snips, "{\"label\":\"activate\",\"description\":\"Activate\",\"body\":\"activate ${1:Type}\"}")
    "[" + list_join(snips, ",") + "]"
}

// ── Settings (file-backed) ───────────────────────────────────────────────────

fn settings_path() -> String {
    let home: String = env("HOME")
    if str_eq(home, "") { let home = "/tmp" }
    home + "/.el-ide/settings.json"
}

fn default_settings_json() -> String {
    "{\"theme\":\"dark\",\"fontSize\":13,\"tabSize\":4,\"wordWrap\":false,\"formatOnSave\":false,\"vimMode\":false,\"minimap\":true,\"elBinaryPath\":\"el\",\"engramUrl\":\"http://localhost:8742\",\"stickyScroll\":true,\"lineNumbers\":true,\"bracketMatching\":true}"
}

fn route_settings_get() -> String {
    let s: String = fs_read(settings_path())
    if str_eq(s, "") { return default_settings_json() }
    s
}

fn route_settings_save(body: String) -> String {
    // Body is { "settings": {...} } — extract the inner blob.
    let blob: String = json_get_raw(body, "settings")
    if str_eq(blob, "") { return jerr("missing settings") }
    fs_write(settings_path(), blob)
    blob
}

fn route_settings_reset() -> String {
    fs_write(settings_path(), default_settings_json())
    default_settings_json()
}

// ── Build / run (synchronous; no SSE in current http_serve runtime) ──────────
//
// The Rust crate spawned `el build/run` and streamed output as SSE events.
// El's `http_serve` is request/response only — handlers return one string.
// We call the el binary, capture stdout via fs_read of a redirected temp
// file, and return JSON. Streaming is a runtime gap (see report).

fn route_build_or_run(action: String, body: String) -> String {
    let rel_file: String = json_get_string(body, "file")
    if str_eq(rel_file, "") { let rel_file = "src/main.el" }
    let target: String = safe_join(rel_file)
    if str_eq(target, "") { return jerr("path escapes project root") }
    // No process-spawn primitive in the runtime; this is a stub that
    // documents the planned behaviour. See report for runtime gap.
    "{\"event\":\"info\",\"action\":" + jstr(action) + ",\"file\":" + jstr(rel_file) + ",\"note\":\"el-ide-server: synchronous build requires a runtime exec primitive — el binary not invoked\"}"
}

fn route_build(body: String) -> String { route_build_or_run("build", body) }
fn route_run(body: String) -> String { route_build_or_run("run", body) }

// ── Engram reasoning proxy ───────────────────────────────────────────────────

fn route_reason(body: String) -> String {
    let url: String = cfg_engram_url() + "/api/reason"
    let resp: String = http_post(url, body)
    if str_eq(resp, "") { return jerr("engram unreachable") }
    resp
}

fn route_activate_preview(query: String) -> String {
    let type_name: String = query_get(query, "type_name")
    let q: String = query_get(query, "query")
    let url: String = cfg_engram_url() + "/api/activate-preview"
    let body: String = "{\"type_name\":" + jstr(type_name) + ",\"query\":" + jstr(q) + ",\"limit\":5}"
    let resp: String = http_post(url, body)
    if str_eq(resp, "") {
        return "{\"count\":0,\"nodes\":[],\"connected\":false}"
    }
    resp
}

// ── Index page (minimal placeholder; the real bundle lives in ide/index.html) ─

fn route_index() -> String {
    let path: String = cfg_project_path() + "/ide/index.html"
    let html: String = fs_read(path)
    if !str_eq(html, "") { return html }
    "<!doctype html><html><head><title>el-ide</title></head><body><h1>el-ide</h1><p>Backend running. UI bundle missing at <code>" + path + "</code>.</p></body></html>"
}

// ── Health ───────────────────────────────────────────────────────────────────

fn route_health() -> String {
    "{\"status\":\"ok\",\"engine\":\"el-ide-server\"}"
}

// ── Dispatcher ───────────────────────────────────────────────────────────────

fn handle_request(method: String, path: String, body: String) -> String {
    let parsed: Map<String, Any> = split_path_query(path)
    let route: String = parsed["path"]
    let query: String = parsed["query"]

    if str_eq(route, "/") { return route_index() }
    if str_eq(route, "/health") { return route_health() }
    if str_eq(route, "/api/status") { return route_status() }
    if str_eq(route, "/api/config") { return route_config() }

    // Files
    if str_eq(route, "/api/files") { return route_files_list(query) }
    if str_eq(route, "/api/file") {
        if str_eq(method, "GET") { return route_file_read(query) }
        if str_eq(method, "POST") { return route_file_write(body) }
    }

    // Search — minimal stub (real impl needs recursive grep)
    if str_eq(route, "/api/search") {
        let q: String = query_get(query, "q")
        return "{\"query\":" + jstr(q) + ",\"results\":[]}"
    }

    // LSP
    if str_eq(route, "/api/lsp/complete") { return route_lsp_complete(query) }
    if str_eq(route, "/api/lsp/hover") { return route_lsp_hover(query) }
    if str_eq(route, "/api/lsp/errors") { return route_lsp_errors(query) }
    if str_eq(route, "/api/lsp/activate-preview") { return route_activate_preview(query) }
    if str_eq(route, "/api/lsp/jsonrpc") { return route_lsp_jsonrpc(body) }
    if str_eq(route, "/api/type-graph") { return route_type_graph(query) }
    if str_eq(route, "/api/outline") { return route_outline(query) }
    if str_eq(route, "/api/definition") { return route_definition(query) }
    if str_eq(route, "/api/references") { return route_references(query) }
    if str_eq(route, "/api/format") { return route_format(body) }
    if str_eq(route, "/api/completions/snippet") { return route_snippets() }

    // Themes
    if str_eq(route, "/api/themes") { return route_themes_list() }
    if str_eq(route, "/api/themes/active") { return route_themes_set(body) }

    // Plugins
    if str_eq(route, "/api/plugins") { return route_plugins_list() }
    if str_eq(route, "/api/plugins/install") { return route_plugins_install(body) }
    if str_eq(route, "/api/plugins/remove") { return route_plugins_remove(body) }
    if str_eq(route, "/api/plugins/enable") { return route_plugins_enable(body) }
    if str_eq(route, "/api/plugins/disable") { return route_plugins_disable(body) }

    // Settings
    if str_eq(route, "/api/settings") {
        if str_eq(method, "GET") { return route_settings_get() }
        if str_eq(method, "POST") { return route_settings_save(body) }
        if str_eq(method, "DELETE") { return route_settings_reset() }
    }

    // Build / run
    if str_eq(route, "/api/build") { return route_build(body) }
    if str_eq(route, "/api/run") { return route_run(body) }

    // Reasoning proxy
    if str_eq(route, "/api/reason") { return route_reason(body) }

    // Git — relies on shell exec; runtime gap. Stub returns empty.
    if str_eq(route, "/api/git/status") { return "[]" }
    if str_eq(route, "/api/git/diff") { return "\"\"" }

    "{\"error\":\"not found\",\"path\":" + jstr(route) + "}"
}

// ── Entry ────────────────────────────────────────────────────────────────────

fn server_main() -> Void {
    plugin_init()
    let port: Int = cfg_port()
    println("[el-ide-server] project=" + cfg_project_path())
    println("[el-ide-server] engram=" + cfg_engram_url())
    println("[el-ide-server] listening on http://0.0.0.0:" + int_to_str(port))
    http_set_handler("handle_request")
    http_serve(port, "handle_request")
}

server_main()
