///|
// mnemo_api.mbt — expose 12 mnemo tools via globalThis.__mnemo_api
// Each tool accepts a JSON string and returns a JSON string.

// ---------------------------------------------------------------------------
// JS FFI: JSON helpers
// ---------------------------------------------------------------------------

extern "js" fn _api_json_parse(s : String) -> Json =
  #| (s) => JSON.parse(s)

extern "js" fn _api_json_stringify(v : Json) -> String =
  #| (v) => JSON.stringify(v)

// ---------------------------------------------------------------------------
// JS FFI: env / path helpers
// ---------------------------------------------------------------------------

extern "js" fn _api_get_mnemo_home() -> String =
  #| () => {
  #|   const os = require("node:os");
  #|   const path = require("node:path");
  #|   return process.env.MNEMO_HOME || path.join(os.homedir(), ".mnemo");
  #| }

extern "js" fn _api_get_db_path() -> String =
  #| () => {
  #|   const os = require("node:os");
  #|   const path = require("node:path");
  #|   const home = process.env.MNEMO_HOME || path.join(os.homedir(), ".mnemo");
  #|   return path.join(home, "sessions.db");
  #| }

// ---------------------------------------------------------------------------
// JS FFI: string field extraction helpers
// ---------------------------------------------------------------------------

extern "js" fn _api_field_str(obj : Json, key : String) -> String =
  #| (obj, key) => (obj && typeof obj[key] === "string") ? obj[key] : ""

extern "js" fn _api_field_str_or_null(obj : Json, key : String) -> String =
  #| (obj, key) => (obj && obj[key] != null && obj[key] !== undefined) ? String(obj[key]) : "\u0000"

extern "js" fn _api_field_int(obj : Json, key : String, def : Int) -> Int =
  #| (obj, key, def) => (obj && typeof obj[key] === "number") ? (obj[key] | 0) : def

extern "js" fn _api_field_int64(obj : Json, key : String, def : Int64) -> Int64 =
  #| (obj, key, def) => {
  #|   if (!obj || obj[key] == null) return def;
  #|   const v = obj[key];
  #|   if (typeof v === "bigint") return v;
  #|   if (typeof v === "number") return BigInt(Math.trunc(v));
  #|   return def;
  #| }

extern "js" fn _api_field_bool(obj : Json, key : String, def : Bool) -> Bool =
  #| (obj, key, def) => (obj && typeof obj[key] === "boolean") ? obj[key] : def

extern "js" fn _api_field_array_str(obj : Json, key : String) -> Array[String] =
  #| (obj, key) => (obj && Array.isArray(obj[key])) ? obj[key].map(String) : []

// ---------------------------------------------------------------------------
// JS FFI: JSON result constructors (avoid MoonBit Json literals)
// ---------------------------------------------------------------------------

extern "js" fn _api_ok_json(fields_json : String) -> String =
  #| (fieldsJson) => JSON.stringify(Object.assign({ ok: true }, JSON.parse(fieldsJson)))

extern "js" fn _api_err_json(error : String, error_code : String) -> String =
  #| (error, error_code) => JSON.stringify({ ok: false, error, error_code })

// ---------------------------------------------------------------------------
// Helper: parse null-sentinel for optional strings
// ---------------------------------------------------------------------------

fn is_null_sentinel(s : String) -> Bool {
  s == "\u0000"
}

fn sentinel_to_option(s : String) -> String? {
  if is_null_sentinel(s) { None } else { Some(s) }
}

// ---------------------------------------------------------------------------
// tool_memory_add
// ---------------------------------------------------------------------------

pub async fn tool_memory_add(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let target_str = _api_field_str(obj, "target")
  let content = _api_field_str(obj, "content")
  let target : Target = if target_str == "user" { User } else { Memory }
  let mnemo_home = _api_get_mnemo_home()
  let store = MemoryStore::load(mnemo_home)
  match store.add(target, content) {
    AddOk(used, max) =>
      _api_ok_json(
        "{\"usage\":{\"used_chars\":" + used.to_string() + ",\"max_chars\":" + max.to_string() + "}}",
      )
    AddErr(msg, code) => _api_err_json(msg, code)
  }
}

// ---------------------------------------------------------------------------
// tool_memory_replace
// ---------------------------------------------------------------------------

pub async fn tool_memory_replace(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let target_str = _api_field_str(obj, "target")
  let old_substring = _api_field_str(obj, "old_substring")
  let new_content = _api_field_str(obj, "new_content")
  let target : Target = if target_str == "user" { User } else { Memory }
  let mnemo_home = _api_get_mnemo_home()
  let store = MemoryStore::load(mnemo_home)
  match store.replace(target, old_substring, new_content) {
    OpOk(used, max) =>
      _api_ok_json(
        "{\"usage\":{\"used_chars\":" + used.to_string() + ",\"max_chars\":" + max.to_string() + "}}",
      )
    OpErr(msg, code) => _api_err_json(msg, code)
  }
}

// ---------------------------------------------------------------------------
// tool_memory_remove
// ---------------------------------------------------------------------------

pub async fn tool_memory_remove(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let target_str = _api_field_str(obj, "target")
  let old_substring = _api_field_str(obj, "old_substring")
  let target : Target = if target_str == "user" { User } else { Memory }
  let mnemo_home = _api_get_mnemo_home()
  let store = MemoryStore::load(mnemo_home)
  match store.remove(target, old_substring) {
    OpOk(used, max) =>
      _api_ok_json(
        "{\"usage\":{\"used_chars\":" + used.to_string() + ",\"max_chars\":" + max.to_string() + "}}",
      )
    OpErr(msg, code) => _api_err_json(msg, code)
  }
}

// ---------------------------------------------------------------------------
// tool_memory_read
// ---------------------------------------------------------------------------

pub fn tool_memory_read(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let target_str = _api_field_str(obj, "target")
  let target : Target = if target_str == "user" { User } else { Memory }
  let mnemo_home = _api_get_mnemo_home()
  let store = MemoryStore::load(mnemo_home)
  let content = store.snapshot(target)
  let max = match target {
    Memory => store.max_memory
    User => store.max_user
  }
  let used = content.length()
  _api_ok_json(
    "{\"content\":" +
    _api_str_to_json_str(content) +
    ",\"usage\":{\"used_chars\":" +
    used.to_string() +
    ",\"max_chars\":" +
    max.to_string() +
    "}}",
  )
}

// ---------------------------------------------------------------------------
// JS FFI: JSON.stringify a string value
// ---------------------------------------------------------------------------

extern "js" fn _api_str_to_json_str(s : String) -> String =
  #| (s) => JSON.stringify(s)

// ---------------------------------------------------------------------------
// tool_skills_list
// ---------------------------------------------------------------------------

extern "js" fn _api_skills_list_js(
  mnemo_home : String,
  available_tools_json : String,
  platform_str : String
) -> String =
  #| (mnemoHome, toolsJson, platformStr) => {
  #|   // This function returns JSON-encoded result
  #|   // It's called by tool_skills_list via direct extern
  #|   return null; // placeholder — actual impl in tool_skills_list below
  #| }

pub fn tool_skills_list(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let mnemo_home = _api_get_mnemo_home()
  let available_tools = _api_field_array_str(obj, "available_tools")
  let available_toolsets = _api_field_array_str(obj, "available_toolsets")
  let disabled = _api_field_array_str(obj, "disabled")
  let platform_str = _api_field_str(obj, "platform")
  let category_filter = _api_field_str(obj, "category")
  // Load skills from mnemo_home/skills
  let skills_dir = mnemo_home + "/skills"
  let store = skills_store_load(skills_dir, [])
  let platform_opt : String? = if platform_str.is_empty() { None } else { Some(platform_str) }
  // Match proto: when both arrays are empty, treat as absent (None) → no tool/toolset filtering.
  // When either is non-empty, pass both as Some([...]) to enable filtering.
  let has_filter = available_tools.length() > 0 || available_toolsets.length() > 0
  let tools_opt : Array[String]? = if has_filter { Some(available_tools) } else { None }
  let toolsets_opt : Array[String]? = if has_filter { Some(available_toolsets) } else { None }
  let filtered = skills_store_list(
    store, tools_opt, platform_opt,
    available_toolsets=toolsets_opt, disabled=disabled,
  )
  // Apply category filter
  let results = if category_filter.is_empty() {
    filtered
  } else {
    filtered.filter(fn(s) { s.category == category_filter })
  }
  // Build JSON output
  let parts : Array[String] = []
  for s in results {
    parts.push(
      "{\"name\":" +
      _api_str_to_json_str(s.name) +
      ",\"description\":" +
      _api_str_to_json_str(s.description) +
      ",\"category\":" +
      _api_str_to_json_str(s.category) +
      "}",
    )
  }
  let skills_json = "[" + parts.join(",") + "]"
  _api_ok_json("{\"skills\":" + skills_json + ",\"count\":" + results.length().to_string() + "}")
}

// ---------------------------------------------------------------------------
// tool_skill_view
// ---------------------------------------------------------------------------

pub fn tool_skill_view(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let name = _api_field_str(obj, "name")
  let mnemo_home = _api_get_mnemo_home()
  let skills_dir = mnemo_home + "/skills"
  let store = skills_store_load(skills_dir, [])
  match skills_store_view(store, name) {
    ViewOk(skill_name, content) =>
      _api_ok_json(
        "{\"name\":" +
        _api_str_to_json_str(skill_name) +
        ",\"content\":" +
        _api_str_to_json_str(content) +
        "}",
      )
    ViewErr(msg) => _api_err_json(msg, "not_found")
  }
}

// ---------------------------------------------------------------------------
// Session tools — shared db open helper
// ---------------------------------------------------------------------------

fn open_api_session_db() -> SessionDb {
  let db_path = _api_get_db_path()
  open_session_db(db_path)
}

// ---------------------------------------------------------------------------
// tool_session_create
// ---------------------------------------------------------------------------

pub async fn tool_session_create(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let source = _api_field_str(obj, "source")
  let system_prompt = _api_field_str(obj, "system_prompt")
  let model_s = _api_field_str_or_null(obj, "model")
  let user_id_s = _api_field_str_or_null(obj, "user_id")
  let parent_s = _api_field_str_or_null(obj, "parent_session_id")
  let sdb = open_api_session_db()
  let sid = create_session(
    sdb,
    source,
    system_prompt,
    model=sentinel_to_option(model_s),
    user_id=sentinel_to_option(user_id_s),
    parent_session_id=sentinel_to_option(parent_s),
  )
  _api_ok_json("{\"session_id\":" + _api_str_to_json_str(sid) + "}")
}

// ---------------------------------------------------------------------------
// tool_session_append
// ---------------------------------------------------------------------------

pub async fn tool_session_append(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let session_id = _api_field_str(obj, "session_id")
  let role = _api_field_str(obj, "role")
  let timestamp = _api_field_int64(obj, "timestamp", 0L)
  let content_s = _api_field_str_or_null(obj, "content")
  let tool_name_s = _api_field_str_or_null(obj, "tool_name")
  let token_count_raw = _api_field_int(obj, "token_count", -1)
  let token_count_opt : Int? = if token_count_raw < 0 { None } else { Some(token_count_raw) }
  let sdb = open_api_session_db()
  let mid = append_message(
    sdb,
    session_id,
    role,
    timestamp,
    content=sentinel_to_option(content_s),
    tool_name=sentinel_to_option(tool_name_s),
    token_count=token_count_opt,
  )
  _api_ok_json("{\"message_id\":" + mid.to_string() + "}")
}

// ---------------------------------------------------------------------------
// Session row serializers (pure MoonBit)
// ---------------------------------------------------------------------------

fn _opt_str_json(v : String?, j : (String) -> String) -> String {
  match v {
    None => "null"
    Some(s) => j(s)
  }
}

fn _session_row_to_json(s : SessionRow) -> String {
  let j = _api_str_to_json_str
  let user_id_j = _opt_str_json(s.user_id, j)
  let model_j = _opt_str_json(s.model, j)
  let parent_j = _opt_str_json(s.parent_session_id, j)
  let ended_at_j = match s.ended_at {
    None => "null"
    Some(v) => v.to_string()
  }
  let end_reason_j = _opt_str_json(s.end_reason, j)
  let title_j = _opt_str_json(s.title, j)
  "{\"id\":" + j(s.id) +
  ",\"source\":" + j(s.source) +
  ",\"user_id\":" + user_id_j +
  ",\"model\":" + model_j +
  ",\"system_prompt\":" + j(s.system_prompt) +
  ",\"parent_session_id\":" + parent_j +
  ",\"started_at\":" + s.started_at.to_string() +
  ",\"ended_at\":" + ended_at_j +
  ",\"end_reason\":" + end_reason_j +
  ",\"message_count\":" + s.message_count.to_string() +
  ",\"input_tokens\":" + s.input_tokens.to_string() +
  ",\"output_tokens\":" + s.output_tokens.to_string() +
  ",\"cache_read_tokens\":" + s.cache_read_tokens.to_string() +
  ",\"cache_write_tokens\":" + s.cache_write_tokens.to_string() +
  ",\"title\":" + title_j +
  "}"
}

// ---------------------------------------------------------------------------
// tool_session_get — pure MoonBit implementation
// ---------------------------------------------------------------------------

pub fn tool_session_get(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let session_id = _api_field_str(obj, "session_id")
  let sdb = open_api_session_db()
  match get_session(sdb, session_id) {
    None => _api_err_json("session not found", "not_found")
    Some(sess) => {
      // Fetch messages: id, role, content, tool_name, timestamp, token_count
      let stmt = prepare(sdb.db,
        "SELECT id, role, content, tool_name, timestamp, token_count FROM messages WHERE session_id = ? ORDER BY id ASC"
      )
      _stmt_bind_text(stmt, 1, session_id)
      let rows = _stmt_all(stmt)
      let n = _rows_length(rows)
      let msg_parts : Array[String] = []
      let mut i = 0
      while i < n {
        let row = _row_at(rows, i)
        let j = _api_str_to_json_str
        let content_j = if _row_is_null(row, "content") {
          "null"
        } else {
          j(_row_get_text(row, "content"))
        }
        let tool_j = if _row_is_null(row, "tool_name") {
          "null"
        } else {
          j(_row_get_text(row, "tool_name"))
        }
        let tc_j = if _row_is_null(row, "token_count") {
          "null"
        } else {
          _row_get_int(row, "token_count").to_string()
        }
        msg_parts.push(
          "{\"id\":" + _row_get_int(row, "id").to_string() +
          ",\"role\":" + j(_row_get_text(row, "role")) +
          ",\"content\":" + content_j +
          ",\"tool_name\":" + tool_j +
          ",\"timestamp\":" + _row_get_int(row, "timestamp").to_string() +
          ",\"token_count\":" + tc_j +
          "}",
        )
        i = i + 1
      }
      let msgs_json = "[" + msg_parts.join(",") + "]"
      _api_ok_json(
        "{\"session\":" + _session_row_to_json(sess) + ",\"messages\":" + msgs_json + "}",
      )
    }
  }
}

// ---------------------------------------------------------------------------
// tool_session_list — pure MoonBit implementation
// ---------------------------------------------------------------------------

pub fn tool_session_list(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let limit = _api_field_int(obj, "limit", 20)
  let offset = _api_field_int(obj, "offset", 0)
  let source = _api_field_str(obj, "source")
  let sdb = open_api_session_db()
  let (sessions, total) = list_sessions(sdb, limit=limit, offset=offset, source=source)
  let parts : Array[String] = []
  for s in sessions {
    parts.push(_session_row_to_json(s))
  }
  let sessions_json = "[" + parts.join(",") + "]"
  _api_ok_json("{\"sessions\":" + sessions_json + ",\"total\":" + total.to_string() + "}")
}

// ---------------------------------------------------------------------------
// tool_session_search
// ---------------------------------------------------------------------------

extern "js" fn _api_search_results_to_json(results_json : String) -> String =
  #| (s) => s

pub async fn tool_session_search(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let query = _api_field_str(obj, "query")
  let limit = _api_field_int(obj, "limit", 3)
  let role_filter_s = _api_field_str_or_null(obj, "role_filter")
  let sdb = open_api_session_db()
  let resp = session_search(
    sdb,
    query,
    limit=limit,
    role_filter=sentinel_to_option(role_filter_s),
  )
  // Build JSON manually
  let result_parts : Array[String] = []
  for r in resp.results {
    result_parts.push(
      "{\"session_id\":" +
      _api_str_to_json_str(r.session_id) +
      ",\"when\":" +
      _api_str_to_json_str(r.when) +
      ",\"source\":" +
      _api_str_to_json_str(r.source) +
      ",\"model\":" +
      _api_str_to_json_str(r.model) +
      ",\"summary\":" +
      _api_str_to_json_str(r.summary) +
      "}",
    )
  }
  let results_json = "[" + result_parts.join(",") + "]"
  "{\"ok\":true,\"query\":" +
  _api_str_to_json_str(resp.query) +
  ",\"results\":" +
  results_json +
  ",\"count\":" +
  resp.count.to_string() +
  ",\"sessions_searched\":" +
  resp.sessions_searched.to_string() +
  "}"
}

// ---------------------------------------------------------------------------
// tool_session_resolve
// ---------------------------------------------------------------------------

pub fn tool_session_resolve(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let prefix = _api_field_str(obj, "prefix")
  if prefix == "" {
    return _api_err_json("prefix must be non-empty", "invalid_input")
  }
  let sdb = open_api_session_db()
  match resolve_session_id(sdb, prefix) {
    Ok(sid) => _api_ok_json("{\"session_id\":" + _api_str_to_json_str(sid) + "}")
    Err(e) => {
      let code = if e.contains("session_not_found") {
        "not_found"
      } else if e.contains("session_ambiguous") {
        "ambiguous"
      } else {
        "internal_error"
      }
      _api_err_json(e, code)
    }
  }
}

// ---------------------------------------------------------------------------
// tool_prompt_build
// ---------------------------------------------------------------------------

pub fn tool_prompt_build(json_args : String) -> String {
  let obj = _api_json_parse(json_args)
  let cwd = _api_field_str(obj, "cwd")
  let platform = _api_field_str(obj, "platform")
  let model = _api_field_str(obj, "model")
  let available_tools = _api_field_array_str(obj, "available_tools")
  let disabled_skills = _api_field_array_str(obj, "disabled_skills")
  let mnemo_home_override = _api_field_str(obj, "hermes_home")
  let mnemo_home = if mnemo_home_override.is_empty() {
    _api_get_mnemo_home()
  } else {
    mnemo_home_override
  }
  let opts : BuildPromptOptions = {
    hermes_home: mnemo_home,
    cwd,
    platform,
    model,
    available_tools,
    disabled_skills,
  }
  let prompt = build_system_prompt(opts)
  _api_ok_json("{\"prompt\":" + _api_str_to_json_str(prompt) + "}")
}

// ---------------------------------------------------------------------------
// register_api — sets globalThis.__mnemo_api
// ---------------------------------------------------------------------------

extern "js" fn _register_api_js(
  mem_add : (String) -> @ffi.Promise[String],
  mem_replace : (String) -> @ffi.Promise[String],
  mem_remove : (String) -> @ffi.Promise[String],
  mem_read : (String) -> String,
  sk_list : (String) -> String,
  sk_view : (String) -> String,
  sess_create : (String) -> @ffi.Promise[String],
  sess_append : (String) -> @ffi.Promise[String],
  sess_get : (String) -> String,
  sess_list : (String) -> String,
  sess_search : (String) -> @ffi.Promise[String],
  sess_resolve : (String) -> String,
  prompt_build : (String) -> String
) -> Unit =
  #| (mem_add, mem_replace, mem_remove, mem_read,
  #|  sk_list, sk_view,
  #|  sess_create, sess_append, sess_get, sess_list, sess_search, sess_resolve,
  #|  prompt_build) => {
  #|   const wrap = (fn_async) => (s) => Promise.resolve(fn_async(s)).catch(e => JSON.stringify({ok: false, error: String(e), error_code: "internal_error"}));
  #|   const wrapSync = (fn_sync) => (s) => { try { return fn_sync(s); } catch(e) { return JSON.stringify({ok: false, error: String(e), error_code: "internal_error"}); } };
  #|   globalThis.__mnemo_api = {
  #|     memory_add:      wrap(mem_add),
  #|     memory_replace:  wrap(mem_replace),
  #|     memory_remove:   wrap(mem_remove),
  #|     memory_read:     wrapSync(mem_read),
  #|     skills_list:     wrapSync(sk_list),
  #|     skill_view:      wrapSync(sk_view),
  #|     session_create:  wrap(sess_create),
  #|     session_append:  wrap(sess_append),
  #|     session_get:     wrapSync(sess_get),
  #|     session_list:    wrapSync(sess_list),
  #|     session_search:  wrap(sess_search),
  #|     session_resolve: wrapSync(sess_resolve),
  #|     prompt_build:    wrapSync(prompt_build),
  #|   };
  #| }

extern "js" fn _resolved_promise(s : String) -> @ffi.Promise[String] =
  #| (s) => Promise.resolve(s)

extern "js" fn _make_async_promise(f : () -> Unit) -> @ffi.Promise[String] =
  #| (f) => new Promise((resolve) => { f(); resolve("__pending__"); })

fn async_tool(f : async () -> String) -> @ffi.Promise[String] {
  _make_deferred_promise(fn(resolve, reject) {
    @ffi.run_async(async fn() noraise {
      let result = try {
        f()
      } catch {
        e => {
          reject(@ffi.identity(e))
          return
        }
      }
      resolve(result)
    })
  })
}

extern "js" fn _make_deferred_promise(
  executor : ((String) -> Unit, (@ffi.JsValue) -> Unit) -> Unit
) -> @ffi.Promise[String] =
  #| (executor) => new Promise((resolve, reject) => executor(resolve, reject))

pub fn register_api() -> Unit {
  _register_api_js(
    fn(s) { async_tool(async fn() { tool_memory_add(s) }) },
    fn(s) { async_tool(async fn() { tool_memory_replace(s) }) },
    fn(s) { async_tool(async fn() { tool_memory_remove(s) }) },
    fn(s) { tool_memory_read(s) },
    fn(s) { tool_skills_list(s) },
    fn(s) { tool_skill_view(s) },
    fn(s) { async_tool(async fn() { tool_session_create(s) }) },
    fn(s) { async_tool(async fn() { tool_session_append(s) }) },
    fn(s) { tool_session_get(s) },
    fn(s) { tool_session_list(s) },
    fn(s) { async_tool(async fn() { tool_session_search(s) }) },
    fn(s) { tool_session_resolve(s) },
    fn(s) { tool_prompt_build(s) },
  )
}