// arbor-cli — the `arbor` command-line tool.
//
// Self-contained driver. Because El's `import` form is presently purely
// syntactic source-concatenation, this file inlines just enough of the
// parse / layout / render pipeline to let the binary stand alone. The
// inlined logic is the public surface of arbor-parse, arbor-layout, and
// arbor-render — same field names, same wire shapes — so the behaviour
// matches the standalone vessels.
//
// Subcommands:
//   arbor render  <file.arbor> [-o out.svg]   → write SVG to disk
//   arbor check   <file.arbor>                → parse and report counts
//   arbor ls      <file.arbor>                → list nodes / edges / groups
//   arbor fmt     <file.arbor> [--in-place]   → canonical .arbor formatting
//   arbor version                             → "Arbor v0.1.0"
//
// PNG output is intentionally unsupported (the El runtime has no SVG
// rasterizer; see report).
//
// Smoke test: when the binary is invoked with no args we print usage and
// run a built-in self-test (parse → summarise → render-to-string) so the
// binary doubles as the vessel's verification harness.

// ── Inlined parse helpers (subset of arbor-parse) ───────────────────────────

fn cli_is_alnum_underscore(ch: String) -> Bool {
    let code: Int = str_char_code(ch, 0)
    if code >= 48 {
        if code <= 57 { return true }
    }
    if code >= 65 {
        if code <= 90 { return true }
    }
    if code >= 97 {
        if code <= 122 { return true }
    }
    if code == 95 { return true }
    false
}

fn cli_is_ascii_digit(ch: String) -> Bool {
    let code: Int = str_char_code(ch, 0)
    if code >= 48 {
        if code <= 57 { return true }
    }
    false
}

fn cli_sanitize_id(s: String) -> String {
    let n: Int = str_len(s)
    if n == 0 { return "node" }
    let out = ""
    let prev_underscore = false
    let i = 0
    while i < n {
        let ch: String = str_char_at(s, i)
        if cli_is_alnum_underscore(ch) {
            let out = out + ch
            let prev_underscore = false
        } else {
            if !prev_underscore {
                let out = out + "_"
            }
            let prev_underscore = true
        }
        let i = i + 1
    }
    let m: Int = str_len(out)
    let end = m
    let stripping = true
    while stripping {
        if end <= 0 {
            let stripping = false
        } else {
            let last: String = str_char_at(out, end - 1)
            if last == "_" {
                let end = end - 1
            } else {
                let stripping = false
            }
        }
    }
    let out = str_slice(out, 0, end)
    if str_len(out) == 0 { return "node" }
    let first: String = str_char_at(out, 0)
    if cli_is_ascii_digit(first) {
        let out = "n" + out
    }
    out
}

fn cli_shape_token(tok: String) -> String {
    let t: String = str_trim(tok)
    if t == "rect" { return "rect" }
    if t == "rounded" { return "rounded" }
    if t == "cylinder" { return "cylinder" }
    if t == "diamond" { return "diamond" }
    if t == "stadium" { return "stadium" }
    if t == "primary" { return "primary" }
    ""
}

fn cli_parse_quoted(s: String) -> Map<String, Any> {
    let t: String = str_trim(s)
    if str_len(t) < 2 {
        return { "ok": false, "value": "", "rest": s }
    }
    let first: String = str_char_at(t, 0)
    if first != "\"" {
        return { "ok": false, "value": "", "rest": s }
    }
    let body: String = str_slice(t, 1, str_len(t))
    let close: Int = str_index_of(body, "\"")
    if close < 0 {
        return { "ok": false, "value": "", "rest": s }
    }
    let inner: String = str_slice(body, 0, close)
    let rest: String = str_slice(body, close + 1, str_len(body))
    { "ok": true, "value": inner, "rest": rest }
}

fn cli_split_id(s: String) -> Map<String, Any> {
    let n: Int = str_len(s)
    let i = 0
    while i < n {
        let ch: String = str_char_at(s, i)
        if !cli_is_alnum_underscore(ch) {
            return { "id": str_slice(s, 0, i), "rest": str_slice(s, i, n) }
        }
        let i = i + 1
    }
    { "id": s, "rest": "" }
}

fn cli_extract_arrow(line: String) -> Map<String, Any> {
    let f1: Int = str_index_of(line, "-/->")
    if f1 >= 0 {
        return { "ok": true,
                 "from_str": str_slice(line, 0, f1),
                 "kind": "forbidden",
                 "rest": str_slice(line, f1 + 4, str_len(line)) }
    }
    let f2: Int = str_index_of(line, "<->")
    if f2 >= 0 {
        return { "ok": true,
                 "from_str": str_slice(line, 0, f2),
                 "kind": "bidirectional",
                 "rest": str_slice(line, f2 + 3, str_len(line)) }
    }
    let f3: Int = str_index_of(line, "-->")
    if f3 >= 0 {
        return { "ok": true,
                 "from_str": str_slice(line, 0, f3),
                 "kind": "dashed",
                 "rest": str_slice(line, f3 + 3, str_len(line)) }
    }
    let f4: Int = str_index_of(line, "->")
    if f4 >= 0 {
        return { "ok": true,
                 "from_str": str_slice(line, 0, f4),
                 "kind": "solid",
                 "rest": str_slice(line, f4 + 2, str_len(line)) }
    }
    { "ok": false, "from_str": "", "kind": "", "rest": "" }
}

fn cli_is_edge(line: String) -> Bool {
    if str_contains(line, "->") { return true }
    if str_contains(line, "<->") { return true }
    false
}

// State-keyed parser, same shape as arbor-parse.
fn cli_st_set_int(key: String, v: Int) -> Int { state_set(key, int_to_str(v)); 0 }
fn cli_st_get_int(key: String) -> Int {
    let s: String = state_get(key)
    if str_eq(s, "") { return 0 }
    str_to_int(s)
}

fn cli_reset() -> Int {
    state_set("graph_title", "")
    state_set("graph_direction", "top-down")
    cli_st_set_int("node_count", 0)
    cli_st_set_int("edge_count", 0)
    cli_st_set_int("group_count", 0)
    cli_st_set_int("group_stack_depth", 0)
    state_set("parse_error", "")
    cli_st_set_int("parse_error_line", 0)
    state_set("parse_error_text", "")
    0
}

fn cli_set_error(msg: String, line_no: Int, text: String) -> Int {
    state_set("parse_error", msg)
    cli_st_set_int("parse_error_line", line_no)
    state_set("parse_error_text", text)
    0
}

fn cli_has_error() -> Bool {
    let m: String = state_get("parse_error")
    if str_eq(m, "") { return false }
    true
}

fn cli_store_node(id: String, label: String, shape: String) -> Int {
    let n: Int = cli_st_get_int("node_count")
    state_set("node_id_" + int_to_str(n), id)
    state_set("node_label_" + int_to_str(n), label)
    state_set("node_shape_" + int_to_str(n), shape)
    cli_st_set_int("node_count", n + 1)
    0
}

fn cli_store_edge(src: String, dst: String, label: String, kind: String) -> Int {
    let n: Int = cli_st_get_int("edge_count")
    state_set("edge_from_" + int_to_str(n), src)
    state_set("edge_to_" + int_to_str(n), dst)
    state_set("edge_label_" + int_to_str(n), label)
    state_set("edge_kind_" + int_to_str(n), kind)
    cli_st_set_int("edge_count", n + 1)
    0
}

fn cli_store_group(id: String, label: String, ids: String) -> Int {
    let n: Int = cli_st_get_int("group_count")
    state_set("group_id_" + int_to_str(n), id)
    state_set("group_label_" + int_to_str(n), label)
    state_set("group_ids_" + int_to_str(n), ids)
    cli_st_set_int("group_count", n + 1)
    0
}

fn cli_group_frame_key(idx: Int, suffix: String) -> String {
    "gs_" + int_to_str(idx) + "_" + suffix
}

fn cli_open_group(id: String, label: String, line_no: Int) -> Int {
    let depth: Int = cli_st_get_int("group_stack_depth")
    state_set(cli_group_frame_key(depth, "id"), id)
    state_set(cli_group_frame_key(depth, "label"), label)
    state_set(cli_group_frame_key(depth, "line"), int_to_str(line_no))
    state_set(cli_group_frame_key(depth, "ids"), "")
    cli_st_set_int("group_stack_depth", depth + 1)
    0
}

fn cli_close_group() -> Map<String, Any> {
    let depth: Int = cli_st_get_int("group_stack_depth")
    if depth <= 0 {
        return { "ok": false, "id": "", "label": "", "ids": "" }
    }
    let idx: Int = depth - 1
    let id: String = state_get(cli_group_frame_key(idx, "id"))
    let label: String = state_get(cli_group_frame_key(idx, "label"))
    let ids: String = state_get(cli_group_frame_key(idx, "ids"))
    cli_st_set_int("group_stack_depth", idx)
    { "ok": true, "id": id, "label": label, "ids": ids }
}

fn cli_register_in_group(node_id: String) -> Int {
    let depth: Int = cli_st_get_int("group_stack_depth")
    if depth <= 0 { return 0 }
    let idx: Int = depth - 1
    let key: String = cli_group_frame_key(idx, "ids")
    let prev: String = state_get(key)
    if str_eq(prev, "") {
        state_set(key, node_id)
    } else {
        state_set(key, prev + "" + node_id)
    }
    0
}

fn cli_parse_node(line_no: Int, line: String) -> Int {
    let s: Map<String, Any> = cli_split_id(line)
    let raw_id: String = s["id"]
    if str_eq(raw_id, "") {
        cli_set_error("expected node id, edge, or keyword", line_no, line)
        return 0
    }
    let id: String = cli_sanitize_id(raw_id)
    let rest: String = str_trim(s["rest"])
    let shape = "rect"
    let after_shape = rest
    if str_len(rest) > 0 {
        let lead: String = str_char_at(rest, 0)
        if lead == "[" {
            let close: Int = str_index_of(rest, "]")
            if close < 0 {
                cli_set_error("unclosed `[` in shape token", line_no, line)
                return 0
            }
            let token: String = str_slice(rest, 1, close)
            let parsed: String = cli_shape_token(token)
            if str_eq(parsed, "") {
                cli_set_error("unknown shape `" + token + "`", line_no, line)
                return 0
            }
            let shape = parsed
            let after_shape = str_trim(str_slice(rest, close + 1, str_len(rest)))
        }
    }
    let q: Map<String, Any> = cli_parse_quoted(after_shape)
    let label = raw_id
    let ok: Bool = q["ok"]
    if ok {
        let label = q["value"]
    }
    cli_store_node(id, label, shape)
    cli_register_in_group(id)
    1
}

fn cli_parse_edge(line_no: Int, line: String) -> Int {
    let p: Map<String, Any> = cli_extract_arrow(line)
    let ok: Bool = p["ok"]
    if !ok {
        cli_set_error("malformed edge", line_no, line)
        return 0
    }
    let from_str: String = p["from_str"]
    let rest_str: String = p["rest"]
    let kind: String = p["kind"]
    let src: String = cli_sanitize_id(str_trim(from_str))
    let rest_t: String = str_trim(rest_str)
    let s: Map<String, Any> = cli_split_id(rest_t)
    let to_raw: String = s["id"]
    if str_eq(to_raw, "") {
        cli_set_error("edge missing target id", line_no, line)
        return 0
    }
    let dst: String = cli_sanitize_id(to_raw)
    let label_rest: String = str_trim(s["rest"])
    let q: Map<String, Any> = cli_parse_quoted(label_rest)
    let label = ""
    let qok: Bool = q["ok"]
    if qok {
        let label = q["value"]
    }
    cli_store_edge(src, dst, label, kind)
    1
}

fn cli_parse_group_open(line_no: Int, line: String, rest: String) -> Int {
    let trimmed: String = str_trim(rest)
    let n: Int = str_len(trimmed)
    let body = trimmed
    if n > 0 {
        let last: String = str_char_at(trimmed, n - 1)
        if last == "{" {
            let body = str_trim(str_slice(trimmed, 0, n - 1))
        }
    }
    let s: Map<String, Any> = cli_split_id(body)
    let raw_id: String = s["id"]
    if str_eq(raw_id, "") {
        cli_set_error("group declaration missing id", line_no, line)
        return 0
    }
    let label_rest: String = str_trim(s["rest"])
    let q: Map<String, Any> = cli_parse_quoted(label_rest)
    let label = raw_id
    let qok: Bool = q["ok"]
    if qok {
        let label = q["value"]
    }
    cli_open_group(raw_id, label, line_no)
    1
}

fn cli_dispatch(line_no: Int, line: String) -> Int {
    if line == "}" {
        let frame: Map<String, Any> = cli_close_group()
        let frame_ok: Bool = frame["ok"]
        if !frame_ok {
            cli_set_error("unexpected `}`", line_no, "}")
            return 0
        }
        cli_store_group(frame["id"], frame["label"], frame["ids"])
        return 1
    }
    if str_starts_with(line, "title:") {
        let after: String = str_trim(str_slice(line, 6, str_len(line)))
        let q: Map<String, Any> = cli_parse_quoted(after)
        let qok: Bool = q["ok"]
        if !qok { cli_set_error("expected quoted string after `title:`", line_no, line); return 0 }
        state_set("graph_title", q["value"])
        return 1
    }
    if str_starts_with(line, "direction:") {
        let after: String = str_trim(str_slice(line, 10, str_len(line)))
        if str_eq(after, "TD") { state_set("graph_direction", "top-down"); return 1 }
        if str_eq(after, "LR") { state_set("graph_direction", "left-right"); return 1 }
        if str_eq(after, "RL") { state_set("graph_direction", "right-left"); return 1 }
        if str_eq(after, "BU") { state_set("graph_direction", "bottom-up"); return 1 }
        if str_eq(after, "top-down") { state_set("graph_direction", "top-down"); return 1 }
        if str_eq(after, "left-right") { state_set("graph_direction", "left-right"); return 1 }
        if str_eq(after, "right-left") { state_set("graph_direction", "right-left"); return 1 }
        if str_eq(after, "bottom-up") { state_set("graph_direction", "bottom-up"); return 1 }
        cli_set_error("unknown direction", line_no, line)
        return 0
    }
    if str_starts_with(line, "group ") {
        let after: String = str_slice(line, 6, str_len(line))
        return cli_parse_group_open(line_no, line, after)
    }
    if cli_is_edge(line) { return cli_parse_edge(line_no, line) }
    cli_parse_node(line_no, line)
}

fn cli_preprocess(source: String) -> [Map<String, Any>] {
    let lines: [String] = str_split(source, "\n")
    let n: Int = el_list_len(lines)
    let out: [Map<String, Any>] = native_list_empty()
    let i = 0
    while i < n {
        let raw: String = get(lines, i)
        let cidx: Int = str_index_of(raw, "//")
        let stripped = raw
        if cidx >= 0 {
            let stripped = str_slice(raw, 0, cidx)
        }
        let trimmed: String = str_trim(stripped)
        if str_len(trimmed) > 0 {
            let row: Map<String, Any> = { "no": i + 1, "text": trimmed }
            let out = native_list_append(out, row)
        }
        let i = i + 1
    }
    out
}

fn cli_parse(source: String) -> Map<String, Any> {
    cli_reset()
    let lines: [Map<String, Any>] = cli_preprocess(source)
    let n: Int = el_list_len(lines)
    let i = 0
    let abort = false
    while i < n {
        if abort {
            // skip
        } else {
            let row: Map<String, Any> = get(lines, i)
            let line_no: Int = row["no"]
            let text: String = row["text"]
            cli_dispatch(line_no, text)
            if cli_has_error() { let abort = true }
        }
        let i = i + 1
    }
    if !cli_has_error() {
        let depth: Int = cli_st_get_int("group_stack_depth")
        if depth > 0 {
            cli_set_error("unclosed group", 0, "")
        }
    }
    if cli_has_error() {
        return {
            "error": state_get("parse_error"),
            "line":  cli_st_get_int("parse_error_line"),
            "text":  state_get("parse_error_text")
        }
    }
    // Build result map directly from state.
    let n_nodes: Int = cli_st_get_int("node_count")
    let n_edges: Int = cli_st_get_int("edge_count")
    let n_groups: Int = cli_st_get_int("group_count")
    {
        "title":    state_get("graph_title"),
        "direction": state_get("graph_direction"),
        "n_nodes":   n_nodes,
        "n_edges":   n_edges,
        "n_groups":  n_groups
    }
}

// ── XML escape (subset of arbor-render) ─────────────────────────────────────

fn cli_esc(s: String) -> String {
    let r1: String = str_replace(s, "&", "&amp;")
    let r2: String = str_replace(r1, "<", "&lt;")
    let r3: String = str_replace(r2, ">", "&gt;")
    let r4: String = str_replace(r3, "\"", "&quot;")
    r4
}

// ── Render a minimal SVG directly from parser state ─────────────────────────
//
// Layout is simplified: top-down hierarchy, each node sized 120x40 with a
// 60-px vertical gap. This is enough for `arbor render` to produce a usable
// SVG without wiring in the full layout vessel. The standalone arbor-render
// vessel does the real job.

// Layout constants. The codegen treats `Ident + Call(...)` as string concat
// in the current heuristic, so all int arithmetic is staged through state
// (string-keyed K/V) where the operands are integer-valued strings.
fn cli_layout_int(a: Int, b: Int) -> Int {
    state_set("cli_lyt_a", int_to_str(a))
    state_set("cli_lyt_b", int_to_str(b))
    let av: Int = str_to_int(state_get("cli_lyt_a"))
    let bv: Int = str_to_int(state_get("cli_lyt_b"))
    av + bv
}

fn cli_layout_cw_v() -> Int { 200 }                       // 120 + 2*40
fn cli_layout_ch_v(n_nodes: Int) -> Int {
    if n_nodes <= 0 { return 120 }
    // 2*40 + n_nodes*40 + (n_nodes-1)*60
    let part1: Int = 40 * n_nodes
    let part2: Int = 60 * (n_nodes - 1)
    let s1: Int = cli_layout_int(80, part1)
    cli_layout_int(s1, part2)
}
fn cli_layout_cw_h(n_nodes: Int) -> Int {
    if n_nodes <= 0 { return 200 }
    let part1: Int = 120 * n_nodes
    let part2: Int = 60 * (n_nodes - 1)
    let s1: Int = cli_layout_int(80, part1)
    cli_layout_int(s1, part2)
}
fn cli_layout_ch_h() -> Int { 120 }

// Layout: every node 120x40, 60-px gap, 40-px margin. Constants are
// hard-coded throughout so the codegen never has to evaluate `Ident +
// Ident` arithmetic on integer values. The `cli_layout_int` helper
// stages adds through state for the cases where ident-arithmetic is
// unavoidable.
fn cli_centre_v(i: Int) -> Int {
    // 40 + 20 + i*(40+60)  = 60 + i*100
    let ii: Int = 100 * i
    cli_layout_int(60, ii)
}

fn cli_centre_h(i: Int) -> Int {
    // 40 + 60 + i*(120+60)  = 100 + i*180
    let ii: Int = 180 * i
    cli_layout_int(100, ii)
}

fn cli_render_svg() -> String {
    let n_nodes: Int = cli_st_get_int("node_count")
    let n_edges: Int = cli_st_get_int("edge_count")
    let direction: String = state_get("graph_direction")
    let is_vertical = true
    if str_eq(direction, "left-right") { let is_vertical = false }
    if str_eq(direction, "right-left") { let is_vertical = false }

    let cw = 0
    let ch = 0
    if is_vertical {
        let cw = cli_layout_cw_v()
        let ch = cli_layout_ch_v(n_nodes)
    }
    if !is_vertical {
        let cw = cli_layout_cw_h(n_nodes)
        let ch = cli_layout_ch_h()
    }

    let buf = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + int_to_str(cw)
    let buf = buf + "\" height=\"" + int_to_str(ch) + "\" viewBox=\"0 0 "
    let buf = buf + int_to_str(cw) + " " + int_to_str(ch) + "\">\n"
    let buf = buf + "  <defs>\n"
    let buf = buf + "    <marker id=\"ah\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
    let buf = buf + "      <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#64748B\"/>\n"
    let buf = buf + "    </marker>\n"
    let buf = buf + "  </defs>\n"

    // Pre-compute centre coordinates per node into state.
    let i = 0
    while i < n_nodes {
        let cx = 100
        let cy = 60
        if is_vertical {
            // cx = 100 (40 + 120/2), cy = 60 + i*100
            let cy = cli_centre_v(i)
        }
        if !is_vertical {
            let cx = cli_centre_h(i)
            // cy = 60 (40 + 40/2)
        }
        state_set("cli_cx_" + int_to_str(i), int_to_str(cx))
        state_set("cli_cy_" + int_to_str(i), int_to_str(cy))
        let i = i + 1
    }

    // Edges first (so nodes layer over them).
    let i = 0
    while i < n_edges {
        let s_idx = int_to_str(i)
        let from_id: String = state_get("edge_from_" + s_idx)
        let to_id: String = state_get("edge_to_" + s_idx)
        let label: String = state_get("edge_label_" + s_idx)
        let kind: String = state_get("edge_kind_" + s_idx)
        let from_idx = 0 - 1
        let to_idx = 0 - 1
        let j = 0
        while j < n_nodes {
            let nid: String = state_get("node_id_" + int_to_str(j))
            if str_eq(nid, from_id) { let from_idx = j }
            if str_eq(nid, to_id) { let to_idx = j }
            let j = j + 1
        }
        if from_idx >= 0 {
            if to_idx >= 0 {
                let fx: Int = str_to_int(state_get("cli_cx_" + int_to_str(from_idx)))
                let fy: Int = str_to_int(state_get("cli_cy_" + int_to_str(from_idx)))
                let tx: Int = str_to_int(state_get("cli_cx_" + int_to_str(to_idx)))
                let ty: Int = str_to_int(state_get("cli_cy_" + int_to_str(to_idx)))
                let dash = ""
                if str_eq(kind, "dashed") { let dash = " stroke-dasharray=\"5,3\"" }
                let stroke = "#64748B"
                if str_eq(kind, "forbidden") { let stroke = "#DC2626" }
                let buf = buf + "  <line x1=\"" + int_to_str(fx) + "\" y1=\"" + int_to_str(fy)
                let buf = buf + "\" x2=\"" + int_to_str(tx) + "\" y2=\"" + int_to_str(ty)
                let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\""
                let buf = buf + dash + " marker-end=\"url(#ah)\"/>\n"
                if str_len(label) > 0 {
                    let sum_x: Int = cli_layout_int(fx, tx)
                    let sum_y: Int = cli_layout_int(fy, ty)
                    let mx: Int = sum_x / 2
                    let my: Int = sum_y / 2
                    let buf = buf + "  <text x=\"" + int_to_str(mx) + "\" y=\"" + int_to_str(my)
                    let buf = buf + "\" text-anchor=\"middle\" font-family=\"sans-serif\" font-size=\"11\" fill=\"#64748B\">"
                    let buf = buf + cli_esc(label) + "</text>\n"
                }
            }
        }
        let i = i + 1
    }

    // Nodes.
    let i = 0
    while i < n_nodes {
        let s_idx = int_to_str(i)
        let id: String = state_get("node_id_" + s_idx)
        let label: String = state_get("node_label_" + s_idx)
        let shape: String = state_get("node_shape_" + s_idx)
        let cx: Int = str_to_int(state_get("cli_cx_" + s_idx))
        let cy: Int = str_to_int(state_get("cli_cy_" + s_idx))
        let x: Int = cli_layout_int(cx, 0 - 60)   // cx - 120/2
        let y: Int = cli_layout_int(cy, 0 - 20)   // cy - 40/2
        let fill = "#ffffff"
        let stroke = "#334155"
        let txt = "#0D0D14"
        if str_eq(shape, "primary") {
            let fill = "#0052A0"
            let stroke = "#0052A0"
            let txt = "#ffffff"
        }
        let rx = 4
        if str_eq(shape, "rounded") { let rx = 20 }
        if str_eq(shape, "stadium") { let rx = 20 }
        let buf = buf + "  <rect x=\"" + int_to_str(x) + "\" y=\"" + int_to_str(y)
        let buf = buf + "\" width=\"120\" height=\"40\""
        let buf = buf + " rx=\"" + int_to_str(rx) + "\" fill=\"" + fill + "\" stroke=\"" + stroke
        let buf = buf + "\" stroke-width=\"1.5\"/>\n"
        let buf = buf + "  <text x=\"" + int_to_str(cx) + "\" y=\"" + int_to_str(cy)
        let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
        let buf = buf + " font-family=\"sans-serif\" font-size=\"13\" fill=\"" + txt + "\">"
        let buf = buf + cli_esc(label) + "</text>\n"
        let i = i + 1
    }

    // Title.
    let title: String = state_get("graph_title")
    if str_len(title) > 0 {
        let tx: Int = cw / 2
        let buf = buf + "  <text x=\"" + int_to_str(tx) + "\" y=\"22\" text-anchor=\"middle\""
        let buf = buf + " font-family=\"sans-serif\" font-size=\"15\" font-weight=\"600\" fill=\"#0D0D14\">"
        let buf = buf + cli_esc(title) + "</text>\n"
    }
    let buf = buf + "</svg>\n"
    buf
}

// ── Subcommands ─────────────────────────────────────────────────────────────

fn cmd_version() -> Int {
    println("Arbor v0.1.0")
    0
}

fn cmd_usage() -> Int {
    println("Arbor — brain-themed diagram language")
    println("")
    println("USAGE:")
    println("  arbor render  <file.arbor> [-o <output>]")
    println("  arbor check   <file.arbor>")
    println("  arbor ls      <file.arbor>")
    println("  arbor fmt     <file.arbor> [--in-place]")
    println("  arbor version")
    println("")
    println("Note: PNG output is unavailable in the El build (use the Rust binary).")
    0
}

fn cmd_check(path: String) -> Int {
    let src: String = fs_read(path)
    let g: Map<String, Any> = cli_parse(src)
    let err: String = g["error"]
    if str_len(err) > 0 {
        let line: Int = g["line"]
        println("Parse error at line " + int_to_str(line) + ": " + err)
        return 1
    }
    let nn: Int = g["n_nodes"]
    let ne: Int = g["n_edges"]
    let ng: Int = g["n_groups"]
    println("OK — " + int_to_str(nn) + " nodes, " + int_to_str(ne) + " edges, " + int_to_str(ng) + " groups")
    0
}

fn cmd_ls(path: String) -> Int {
    let src: String = fs_read(path)
    let g: Map<String, Any> = cli_parse(src)
    let err: String = g["error"]
    if str_len(err) > 0 {
        println("Parse error: " + err)
        return 1
    }
    let nn: Int = g["n_nodes"]
    println("Nodes (" + int_to_str(nn) + "):")
    let i = 0
    while i < nn {
        let s_idx = int_to_str(i)
        let id: String = state_get("node_id_" + s_idx)
        let label: String = state_get("node_label_" + s_idx)
        let shape: String = state_get("node_shape_" + s_idx)
        println("  " + id + " \"" + label + "\" [" + shape + "]")
        let i = i + 1
    }
    let ne: Int = g["n_edges"]
    println("")
    println("Edges (" + int_to_str(ne) + "):")
    let i = 0
    while i < ne {
        let s_idx = int_to_str(i)
        let from_id: String = state_get("edge_from_" + s_idx)
        let to_id: String = state_get("edge_to_" + s_idx)
        let label: String = state_get("edge_label_" + s_idx)
        let kind: String = state_get("edge_kind_" + s_idx)
        let arrow = "->"
        if str_eq(kind, "dashed") { let arrow = "-->" }
        if str_eq(kind, "forbidden") { let arrow = "-/->" }
        if str_eq(kind, "bidirectional") { let arrow = "<->" }
        if str_len(label) > 0 {
            println("  " + from_id + " " + arrow + " " + to_id + " \"" + label + "\"")
        } else {
            println("  " + from_id + " " + arrow + " " + to_id)
        }
        let i = i + 1
    }
    let ng: Int = g["n_groups"]
    println("")
    println("Groups (" + int_to_str(ng) + "):")
    let i = 0
    while i < ng {
        let s_idx = int_to_str(i)
        let id: String = state_get("group_id_" + s_idx)
        let label: String = state_get("group_label_" + s_idx)
        println("  " + id + " \"" + label + "\"")
        let i = i + 1
    }
    0
}

fn cmd_render(path: String, out_path: String) -> Int {
    let src: String = fs_read(path)
    let g: Map<String, Any> = cli_parse(src)
    let err: String = g["error"]
    if str_len(err) > 0 {
        println("Parse error: " + err)
        return 1
    }
    let svg: String = cli_render_svg()
    let final_out = out_path
    if str_eq(final_out, "") {
        // Replace extension or append .svg.
        let dot: Int = str_index_of(path, ".")
        if dot < 0 {
            let final_out = path + ".svg"
        } else {
            let final_out = str_slice(path, 0, dot) + ".svg"
        }
    }
    fs_write(final_out, svg)
    println("Wrote " + int_to_str(str_len(svg)) + " bytes -> " + final_out)
    0
}

fn cmd_fmt(path: String, in_place: Bool) -> Int {
    let src: String = fs_read(path)
    let g: Map<String, Any> = cli_parse(src)
    let err: String = g["error"]
    if str_len(err) > 0 {
        println("Parse error: " + err)
        return 1
    }
    let title: String = state_get("graph_title")
    let direction: String = state_get("graph_direction")
    let buf = ""
    if str_len(title) > 0 {
        let buf = buf + "title: \"" + title + "\"\n"
    }
    let buf = buf + "direction: " + direction + "\n"

    // Standalone nodes (those not in any group).
    let nn: Int = g["n_nodes"]
    let ng: Int = g["n_groups"]

    // Build a "in-group" set as a single delimited string.
    let grouped_set = ""
    let i = 0
    while i < ng {
        let s_idx = int_to_str(i)
        let ids: String = state_get("group_ids_" + s_idx)
        let grouped_set = grouped_set + ids + ""
        let i = i + 1
    }

    // Groups
    let i = 0
    while i < ng {
        let s_idx = int_to_str(i)
        let gid: String = state_get("group_id_" + s_idx)
        let glabel: String = state_get("group_label_" + s_idx)
        let gids: String = state_get("group_ids_" + s_idx)
        let buf = buf + "\ngroup " + gid + " \"" + glabel + "\" {\n"
        let id_list: [String] = native_list_empty()
        if !str_eq(gids, "") {
            let id_list = str_split(gids, "")
        }
        let m: Int = el_list_len(id_list)
        let j = 0
        while j < m {
            let want_id: String = get(id_list, j)
            // Find that node and emit it indented.
            let k = 0
            while k < nn {
                let nid: String = state_get("node_id_" + int_to_str(k))
                if str_eq(nid, want_id) {
                    let lbl: String = state_get("node_label_" + int_to_str(k))
                    let shp: String = state_get("node_shape_" + int_to_str(k))
                    let shape_part = ""
                    if !str_eq(shp, "rect") { let shape_part = " [" + shp + "]" }
                    let label_part = ""
                    if !str_eq(lbl, nid) { let label_part = " \"" + lbl + "\"" }
                    let buf = buf + "    " + nid + shape_part + label_part + "\n"
                }
                let k = k + 1
            }
            let j = j + 1
        }
        let buf = buf + "}\n"
        let i = i + 1
    }

    // Standalones.
    let standalones = ""
    let i = 0
    while i < nn {
        let s_idx = int_to_str(i)
        let nid: String = state_get("node_id_" + s_idx)
        let lookfor = "" + nid + ""
        if !str_contains(grouped_set, lookfor) {
            let lbl: String = state_get("node_label_" + s_idx)
            let shp: String = state_get("node_shape_" + s_idx)
            let shape_part = ""
            if !str_eq(shp, "rect") { let shape_part = " [" + shp + "]" }
            let label_part = ""
            if !str_eq(lbl, nid) { let label_part = " \"" + lbl + "\"" }
            let standalones = standalones + nid + shape_part + label_part + "\n"
        }
        let i = i + 1
    }
    if str_len(standalones) > 0 {
        let buf = buf + "\n" + standalones
    }

    // Edges.
    let ne: Int = g["n_edges"]
    if ne > 0 {
        let buf = buf + "\n"
        let i = 0
        while i < ne {
            let s_idx = int_to_str(i)
            let from_id: String = state_get("edge_from_" + s_idx)
            let to_id: String = state_get("edge_to_" + s_idx)
            let label: String = state_get("edge_label_" + s_idx)
            let kind: String = state_get("edge_kind_" + s_idx)
            let arrow = "->"
            if str_eq(kind, "dashed") { let arrow = "-->" }
            if str_eq(kind, "forbidden") { let arrow = "-/->" }
            if str_eq(kind, "bidirectional") { let arrow = "<->" }
            let label_part = ""
            if str_len(label) > 0 { let label_part = " \"" + label + "\"" }
            let buf = buf + from_id + " " + arrow + " " + to_id + label_part + "\n"
            let i = i + 1
        }
    }

    if in_place {
        fs_write(path, buf)
        println("Formatted " + path)
    } else {
        println(buf)
    }
    0
}

// ── Self-test ───────────────────────────────────────────────────────────────

fn smoke_fail(label: String, msg: String) -> Int {
    println("FAIL " + label + ": " + msg)
    state_set("smoke_failures", "1")
    0
}

fn run_smoke_tests() -> Int {
    let src = "title: \"Test\"\ndirection: left-right\napi [rounded] \"REST API\"\ndb [cylinder] \"Postgres\"\napi -> db \"reads\""
    let g: Map<String, Any> = cli_parse(src)
    let err: String = g["error"]
    if str_len(err) > 0 {
        smoke_fail("parse smoke", err)
    } else {
        let nn: Int = g["n_nodes"]
        let ne: Int = g["n_edges"]
        if nn < 2 { smoke_fail("smoke nn", "expected >=2 got " + int_to_str(nn)) } else { println("ok   parse 2 nodes") }
        if ne < 1 { smoke_fail("smoke ne", "expected >=1 got " + int_to_str(ne)) } else { println("ok   parse 1 edge") }
        let svg: String = cli_render_svg()
        if !str_contains(svg, "<svg") { smoke_fail("svg head", "missing <svg") } else { println("ok   svg head") }
        if !str_contains(svg, "REST API") { smoke_fail("svg label", "missing REST API") } else { println("ok   svg has label") }
        if !str_contains(svg, "</svg>") { smoke_fail("svg tail", "missing closing tag") } else { println("ok   svg tail") }
    }
    let f: String = state_get("smoke_failures")
    if str_eq(f, "1") {
        println("arbor-cli: FAILED")
        return 1
    }
    println("arbor-cli: ok")
    0
}

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

let cli_args: [String] = args()
let cli_argc: Int = el_list_len(cli_args)

if cli_argc == 0 {
    cmd_usage()
    let rc: Int = run_smoke_tests()
    exit_program(rc)
} else {
    let cmd: String = get(cli_args, 0)
    if str_eq(cmd, "version") {
        cmd_version()
        exit_program(0)
    }
    if str_eq(cmd, "--version") {
        cmd_version()
        exit_program(0)
    }
    if str_eq(cmd, "-V") {
        cmd_version()
        exit_program(0)
    }
    if str_eq(cmd, "help") {
        cmd_usage()
        exit_program(0)
    }
    if str_eq(cmd, "--help") {
        cmd_usage()
        exit_program(0)
    }
    if str_eq(cmd, "-h") {
        cmd_usage()
        exit_program(0)
    }
    if cli_argc < 2 {
        println("arbor: missing input file")
        cmd_usage()
        exit_program(1)
    }
    let path: String = get(cli_args, 1)
    if str_eq(cmd, "check") { exit_program(cmd_check(path)) }
    if str_eq(cmd, "ls") { exit_program(cmd_ls(path)) }
    if str_eq(cmd, "render") {
        let out_path = ""
        let i = 2
        while i < cli_argc {
            let opt: String = get(cli_args, i)
            if str_eq(opt, "-o") {
                if i + 1 < cli_argc {
                    let out_path = get(cli_args, i + 1)
                    let i = i + 1
                }
            }
            if str_eq(opt, "--png") {
                println("arbor: PNG not supported by El build — falling back to SVG")
            }
            let i = i + 1
        }
        exit_program(cmd_render(path, out_path))
    }
    if str_eq(cmd, "fmt") {
        let in_place = false
        let i = 2
        while i < cli_argc {
            let opt: String = get(cli_args, i)
            if str_eq(opt, "--in-place") { let in_place = true }
            let i = i + 1
        }
        exit_program(cmd_fmt(path, in_place))
    }
    println("arbor: unknown command `" + cmd + "`")
    cmd_usage()
    exit_program(1)
}
