// server.el — Engram HTTP server.
//
// Engram is the in-process graph store. The runtime owns the data; this
// file is the thin HTTP face. Every route maps to one or two engram_*
// builtins. There is no SQL, no db layer, no SQLite — the runtime IS the
// database.
//
// Built and linked with:
//   elc src/server.el > server.c
//   cc -std=c11 -O2 -lcurl -lpthread -o engram server.c el_runtime.c
//   ./engram
//
// Configuration via environment:
//   ENGRAM_BIND       — host:port (default :8742)
//   ENGRAM_API_KEY    — bearer auth (optional)
//   ENGRAM_DATA_DIR   — snapshot location (default ~/.neuron/engram)

// ── Helpers ───────────────────────────────────────────────────────────────────

fn parse_port(bind: String) -> Int {
    // ":8742" → 8742; "0.0.0.0:8742" → 8742; bare "8742" → 8742
    let colon: Int = str_index_of(bind, ":")
    if colon < 0 {
        return str_to_int(bind)
    }
    let after: String = str_slice(bind, colon + 1, str_len(bind))
    return str_to_int(after)
}

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

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

fn strip_query(path: String) -> String {
    let q: Int = str_index_of(path, "?")
    if q < 0 { return path }
    str_slice(path, 0, q)
}

fn query_param(path: String, key: String) -> String {
    let q: Int = str_index_of(path, "?")
    if q < 0 { return "" }
    let qs: String = str_slice(path, q + 1, str_len(path))
    let needle: String = key + "="
    let pos: Int = str_index_of(qs, needle)
    if pos < 0 { return "" }
    let after: String = str_slice(qs, pos + str_len(needle), str_len(qs))
    let amp: Int = str_index_of(after, "&")
    if amp < 0 { return after }
    str_slice(after, 0, amp)
}

fn query_int(path: String, key: String, default_val: Int) -> Int {
    let v: String = query_param(path, key)
    if str_eq(v, "") { return default_val }
    str_to_int(v)
}

// Extract last path segment after a known prefix: extract_id("/api/nodes/abc-123", "/api/nodes/") → "abc-123"
fn extract_id(path: String, prefix: String) -> String {
    let clean: String = strip_query(path)
    if !str_starts_with(clean, prefix) { return "" }
    let after: String = str_slice(clean, str_len(prefix), str_len(clean))
    let slash: Int = str_index_of(after, "/")
    if slash < 0 { return after }
    str_slice(after, 0, slash)
}

// ── Routes ────────────────────────────────────────────────────────────────────

fn route_stats(method: String, path: String, body: String) -> String {
    engram_stats_json()
}

fn route_create_node(method: String, path: String, body: String) -> String {
    let content: String = json_get_string(body, "content")
    let node_type: String = json_get_string(body, "node_type")
    if str_eq(node_type, "") { let node_type = "Memory" }
    let salience: Float = json_get_float(body, "salience")
    if salience == 0.0 { let salience = 0.5 }
    let id: String = engram_node(content, node_type, salience)
    "{\"id\":\"" + id + "\",\"content\":\"" + content + "\",\"node_type\":\"" + node_type + "\"}"
}

fn route_get_node(method: String, path: String, body: String) -> String {
    let id: String = extract_id(path, "/api/nodes/")
    if str_eq(id, "") { return err_json("missing id") }
    return engram_get_node_json(id)
}

fn route_scan_nodes(method: String, path: String, body: String) -> String {
    let limit:  Int    = query_int(path, "limit", 50)
    let offset: Int    = query_int(path, "offset", 0)
    let nt:     String = query_param(path, "node_type")
    if str_eq(nt, "") {
        return engram_scan_nodes_json(limit, offset)
    }
    return engram_scan_nodes_by_type_json(nt, limit, offset)
}

// route_scan_edges — bulk export of all edges as a JSON array. Implemented
// via engram_save → fs_read of the canonical on-disk snapshot, which the
// runtime keeps in lockstep with the in-memory graph. Live against the
// running graph, not a stale export.
fn route_scan_edges(method: String, path: String, body: String) -> String {
    let dir: String = env("ENGRAM_DATA_DIR")
    if str_eq(dir, "") { let dir = "/tmp/engram" }
    let snap_path: String = dir + "/snapshot.json"
    engram_save(snap_path)
    let snap: String = fs_read(snap_path)
    if str_eq(snap, "") { return "[]" }
    // json_get truncates at the first delimiter (no bracket depth tracking),
    // so for the edges ARRAY value we need json_get_raw, which honors
    // brackets and returns the full sub-JSON.
    let edges: String = json_get_raw(snap, "edges")
    if str_eq(edges, "") { return "[]" }
    return edges
}

fn route_search(method: String, path: String, body: String) -> String {
    let q: String = ""
    if str_eq(method, "GET") {
        let q = query_param(path, "q")
    } else {
        let q = json_get_string(body, "query")
    }
    let limit: Int = query_int(path, "limit", 20)
    if limit == 0 { let limit = json_get_int(body, "limit") }
    if limit == 0 { let limit = 20 }
    return engram_search_json(q, limit)
}

fn route_activate(method: String, path: String, body: String) -> String {
    let q: String = ""
    let depth: Int = 3
    if str_eq(method, "GET") {
        let q = query_param(path, "q")
        let depth = query_int(path, "depth", 3)
    } else {
        let q = json_get_string(body, "query")
        let bd: Int = json_get_int(body, "depth")
        if bd > 0 { let depth = bd }
    }
    return "{\"results\":" + engram_activate_json(q, depth) + "}"
}

fn route_create_edge(method: String, path: String, body: String) -> String {
    let from_id: String = json_get_string(body, "from_id")
    let to_id: String = json_get_string(body, "to_id")
    let relation: String = json_get_string(body, "relation")
    if str_eq(relation, "") { let relation = "associates" }
    let weight: Float = json_get_float(body, "weight")
    if weight == 0.0 { let weight = 0.5 }
    engram_connect(from_id, to_id, weight, relation)
    "{\"ok\":true,\"from_id\":\"" + from_id + "\",\"to_id\":\"" + to_id + "\",\"relation\":\"" + relation + "\"}"
}

fn route_neighbors(method: String, path: String, body: String) -> String {
    let id: String = extract_id(path, "/api/neighbors/")
    if str_eq(id, "") { return err_json("missing id") }
    let depth: Int = query_int(path, "depth", 1)
    return engram_neighbors_json(id, depth, "both")
}

fn route_strengthen(method: String, path: String, body: String) -> String {
    let id: String = json_get_string(body, "node_id")
    if str_eq(id, "") { return err_json("missing node_id") }
    engram_strengthen(id)
    ok_json()
}

fn route_forget(method: String, path: String, body: String) -> String {
    let id: String = extract_id(path, "/api/nodes/")
    if str_eq(id, "") { return err_json("missing id") }
    engram_forget(id)
    ok_json()
}

fn route_save(method: String, path: String, body: String) -> String {
    let p: String = json_get_string(body, "path")
    if str_eq(p, "") {
        let dir: String = env("ENGRAM_DATA_DIR")
        if str_eq(dir, "") { let dir = "/tmp/engram" }
        let p = dir + "/snapshot.json"
    }
    engram_save(p)
    "{\"ok\":true,\"path\":\"" + p + "\"}"
}

fn route_load(method: String, path: String, body: String) -> String {
    let p: String = json_get_string(body, "path")
    if str_eq(p, "") {
        let dir: String = env("ENGRAM_DATA_DIR")
        if str_eq(dir, "") { let dir = "/tmp/engram" }
        let p = dir + "/snapshot.json"
    }
    engram_load(p)
    ok_json()
}

fn route_health(method: String, path: String, body: String) -> String {
    "{\"status\":\"ok\",\"engine\":\"engram-runtime-native\"}"
}

// route_sync — return a snapshot of non-ISE/non-Working nodes for the soul daemon
// to merge into its in-process graph via engram_load_merge.
//
// The soul calls GET /api/sync every SOUL_REFRESH_MS (default 10 min) to pull
// new Knowledge/Memory/BacklogItem nodes from the authoritative HTTP Engram into
// its in-process working store. Previously this returned 404 "not found", causing
// the soul to write the error JSON to a temp file and attempt an empty merge.
//
// Strategy: save the current snapshot to disk, read it back, return the full
// snapshot JSON. The soul's engram_load_merge handles large files gracefully
// (it skips nodes already present by ID). Auth-exempt: same-host internal call.
// (2026-06-27 self-review: added this route to fix silent 10-min sync failures)
fn route_sync(method: String, path: String, body: String) -> String {
    let dir: String = env("ENGRAM_DATA_DIR")
    if str_eq(dir, "") { let dir = "/tmp/engram" }
    let snap_path: String = dir + "/snapshot.json"
    engram_save(snap_path)
    let snap: String = fs_read(snap_path)
    if str_eq(snap, "") { return "{\"nodes\":[],\"edges\":[]}" }
    return snap
}

// route_emit_ise — write an InternalStateEvent node from the soul daemon.
//
// Endpoint: POST /api/neuron/state-events
// Body:     {"content": "<json-string>"}
//
// Auth: exempt (internal endpoint, soul daemon on same host, no _auth needed).
// The soul's ise_post() sends {"content":"..."} without _auth; enforcing auth
// here would silently drop all heartbeat/curiosity ISEs. Unauthenticated POST
// to this endpoint is acceptable: ISE writes are observability-only, append-only,
// and come from a trusted process on localhost.
//
// Salience/importance set to match engram_node_full ISE defaults used by the
// in-process fallback path in awareness.el (salience=0.3, importance=0.3,
// confidence=0.8, tier=Episodic). High temporal_decay_rate (1.617) — ISEs
// are inherently transient; they should decay faster than structural knowledge.
// (2026-06-26 self-review: added this route after discovering ise_post was
// silently failing — the soul posts here but the endpoint didn't exist.)
fn route_emit_ise(method: String, path: String, body: String) -> String {
    let content: String = json_get_string(body, "content")
    if str_eq(content, "") { return err_json("missing content") }
    let sal: Float = 0.3
    let imp: Float = 0.3
    let conf: Float = 0.8
    let id: String = engram_node_full(
        content, "InternalStateEvent", "state-event",
        sal, imp, conf,
        "Episodic", "[\"internal-state\",\"InternalStateEvent\"]"
    )
    "{\"ok\":true,\"id\":\"" + id + "\"}"
}

// ── Auth ──────────────────────────────────────────────────────────────────────

fn check_auth_ok(method: String, body: String) -> Bool {
    let key: String = env("ENGRAM_API_KEY")
    if str_eq(key, "") { return true }
    // Read-only methods don't require auth. Until http_serve surfaces
    // request headers we can't accept a Bearer token cleanly; mutating
    // requests must include "_auth": "<key>" in the JSON body.
    if str_eq(method, "GET") { return true }
    let provided: String = json_get_string(body, "_auth")
    if str_eq(provided, key) { return true }
    return false
}

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

fn handle_request(method: String, path: String, body: String) -> String {
    let clean: String = strip_query(path)

    // Health is always reachable
    if str_eq(method, "GET") {
        if str_eq(clean, "/health") || str_eq(clean, "/") {
            return route_health(method, path, body)
        }
    }

    // ISE posting is auth-exempt (internal soul daemon, same host, no _auth key)
    if str_eq(method, "POST") && str_eq(clean, "/api/neuron/state-events") {
        return route_emit_ise(method, path, body)
    }

    // Auth (when ENGRAM_API_KEY is set)
    if !check_auth_ok(method, body) {
        return err_json("unauthorized")
    }

    // Stats
    if str_eq(method, "GET") && (str_eq(clean, "/api/stats") || str_eq(clean, "/stats")) {
        return route_stats(method, path, body)
    }

    // Nodes
    if str_eq(method, "POST") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes")) {
        return route_create_node(method, path, body)
    }
    if str_eq(method, "GET") && (str_eq(clean, "/api/nodes") || str_eq(clean, "/nodes") || str_eq(clean, "/nodes/list") || str_eq(clean, "/api/nodes/list")) {
        return route_scan_nodes(method, path, body)
    }
    if str_eq(method, "GET") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
        return route_scan_edges(method, path, body)
    }
    if str_eq(method, "GET") && str_starts_with(clean, "/api/nodes/") {
        return route_get_node(method, path, body)
    }
    if str_eq(method, "DELETE") && str_starts_with(clean, "/api/nodes/") {
        return route_forget(method, path, body)
    }

    // Edges
    if str_eq(method, "POST") && (str_eq(clean, "/api/edges") || str_eq(clean, "/edges")) {
        return route_create_edge(method, path, body)
    }
    if str_eq(method, "GET") && str_starts_with(clean, "/api/neighbors/") {
        return route_neighbors(method, path, body)
    }

    // Activation + Search
    if str_eq(method, "POST") && (str_eq(clean, "/api/activate") || str_eq(clean, "/activate")) {
        return route_activate(method, path, body)
    }
    if str_eq(method, "GET") && str_starts_with(clean, "/api/activate") {
        return route_activate(method, path, body)
    }
    if str_eq(method, "POST") && (str_eq(clean, "/api/search") || str_eq(clean, "/search")) {
        return route_search(method, path, body)
    }
    if str_eq(method, "GET") && str_starts_with(clean, "/api/search") {
        return route_search(method, path, body)
    }

    // Strengthen
    if str_eq(method, "POST") && (str_eq(clean, "/api/strengthen") || str_eq(clean, "/strengthen")) {
        return route_strengthen(method, path, body)
    }

    // Persistence
    if str_eq(method, "POST") && (str_eq(clean, "/api/save") || str_eq(clean, "/save")) {
        return route_save(method, path, body)
    }
    if str_eq(method, "POST") && (str_eq(clean, "/api/load") || str_eq(clean, "/load")) {
        return route_load(method, path, body)
    }

    // Sync — soul daemon periodic pull of non-ISE knowledge into in-process graph
    if str_eq(method, "GET") && str_eq(clean, "/api/sync") {
        return route_sync(method, path, body)
    }

    "{\"error\":\"not found\",\"path\":\"" + clean + "\"}"
}

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

let bind_str: String = env("ENGRAM_BIND")
if str_eq(bind_str, "") { let bind_str = ":8742" }
let port: Int = parse_port(bind_str)

// On startup, try to load any existing snapshot (best effort).
let data_dir: String = env("ENGRAM_DATA_DIR")
if str_eq(data_dir, "") { let data_dir = "/tmp/engram" }
let snapshot_path: String = data_dir + "/snapshot.json"
engram_load(snapshot_path)

println("[engram] runtime-native graph engine")
println("[engram] data_dir=" + data_dir)
println("[engram] node_count=" + int_to_str(engram_node_count()))
println("[engram] edge_count=" + int_to_str(engram_edge_count()))
println("[engram] listening on " + int_to_str(port))

http_set_handler("handle_request")
http_serve(port, "handle_request")
