///|
/// FIST-Mbt store: 命名空间多库持久化管理器。
/// 按 ns 路由到独立 SQLite 文件 {data_dir}/{ns}.db,惰性打开(首次访问才建库)。
/// 每个 ns 拥有独立的物理库文件,实现多租户/多项目隔离;路径完全由参数传入,
/// 不做任何绝对路径硬编码。
///
/// 可变性模式与 AuditLog 一致:通过 self.stores.set / self.stores.remove 原地修改,
/// 无需在调用方重新赋值。这样可被模块级不可变绑定持有,供 MCP 工具闭包安全调用。
pub struct MultiStore {
  stores : Map[String, SqliteStore]
  data_dir : String
}

///|
/// 构造:仅记录根目录,不打开任何库(惰性)。
pub fn MultiStore::new(data_dir? : String = ".") -> MultiStore {
  { stores: Map([]), data_dir, }
}

///|
/// 命名空间消毒:仅允许 `[A-Za-z0-9_-]`,拒绝空串及任何含(斜杠/反斜杠/点/
/// 控制字符/空白)的输入,杜绝路径遍历(`../../etc/x`)与注入。
/// 合法返回浅拷贝后的 ns;非法返回 None。
pub fn MultiStore::safe_ns(ns : String) -> String? {
  if ns == "" {
    return None
  }
  let chars = ns.to_array()
  for c in chars {
    let u = c.to_int()
    let ok = (u >= 48 && u <= 57) ||
      (u >= 65 && u <= 90) ||
      (u >= 97 && u <= 122) ||
      u == 45 ||
      u == 95
    if not(ok) {
      return None
    }
  }
  Some(ns)
}

///|
/// 库文件根目录校验(BUG-78):只接受**相对 server 进程 cwd** 的相对路径。
/// 拒空串 / `..` 上跳 / 前导分隔符 / 盘符(`C:`)——过去这里只对 ns 走 safe_ns,
/// data_dir 原样拼进 `\{dir}/\{safe}.db` 就交给 SqliteStore::open(open 会建文件),
/// 于是"命名空间物理隔离"实际是"调用方指到哪就往哪建库"。
/// 口径与 src/server/bugreport.mbt 的 bug_project_dir_ok 保持一致:
/// 同族守卫不该有两套规则(project_dir 拒穿越、data_dir 放行,就是本次缺陷的形状)。
pub fn MultiStore::data_dir_ok(dir : String) -> Bool {
  if dir.is_empty() {
    return false
  }
  if dir.contains("..") {
    return false
  }
  let c0 = dir[0]
  if c0 == '/' || c0 == '\\' {
    return false
  }
  // 拒绝盘符(如 C: 或 D:),顺带挡掉 Windows 绝对形态
  if dir.length() >= 2 && dir[1] == ':' {
    return false
  }
  true
}

///|
/// 打开(或复用)某个命名空间对应的库文件 {data_dir}/{safe_ns}.db。
/// ns 会先经 safe_ns 消毒,非法命名空间直接失败返回 false。
/// data_dir~ 为空时回退到 self.data_dir(与 get 保持一致,避免状态分裂);
/// **两者取用后的最终目录仍要过 data_dir_ok**(BUG-78:消毒范围覆盖实际落点,不只覆盖入参形态)。
pub fn MultiStore::open(
  self : MultiStore,
  ns~ : String,
  data_dir~ : String,
) -> Bool {
  match MultiStore::safe_ns(ns) {
    None => false
    Some(safe) => {
      // 已打开则直接复用(幂等)
      if self.stores.contains(safe) {
        return true
      }
      let dir = if data_dir == "" { self.data_dir } else { data_dir }
      if not(MultiStore::data_dir_ok(dir)) {
        return false
      }
      let path = "\{dir}/\{safe}.db"
      match SqliteStore::open(path) {
        None => false
        Some(s) => {
          self.stores.set(safe, s)
          true
        }
      }
    }
  }
}

///|
/// 获取某 ns 的存储;惰性打开:尚未打开时按 self.data_dir 首次创建/打开,
/// 并在打开成功后缓存进 self.stores,保证后续 get/ns_stats 复用同一句柄。
/// ns 非法(未过 safe_ns)或打开失败返回 None。
pub fn MultiStore::get(self : MultiStore, ns : String) -> SqliteStore? {
  match MultiStore::safe_ns(ns) {
    None => None
    Some(safe) =>
      match self.stores.get(safe) {
        Some(s) => Some(s)
        None =>
          if not(MultiStore::data_dir_ok(self.data_dir)) {
            // 构造期就把根目录写歪(如绝对路径)也不建库:与 open 同口径,别留第二条免检入口
            None
          } else {
            let path = "\{self.data_dir}/\{safe}.db"
            match SqliteStore::open(path) {
              Some(s) => {
                self.stores.set(safe, s)
                Some(s)
              }
              None => None
            }
          }
      }
  }
}

///|
/// 关闭并移除某个命名空间(不删除物理文件,可重新 open 恢复)。
pub fn MultiStore::close(self : MultiStore, ns : String) -> Unit {
  if self.stores.contains(ns) {
    self.stores.remove(ns)
  }
}

///|
/// 关闭全部命名空间。
pub fn MultiStore::close_all(self : MultiStore) -> Unit {
  let keys = self.ns_list()
  for k in keys {
    self.stores.remove(k)
  }
}

///|
/// 列出已打开的命名空间(Map 遍历顺序,不保证稳定)。
pub fn MultiStore::ns_list(self : MultiStore) -> Array[String] {
  let out : Array[String] = []
  self.stores.each(fn(k, _) { out.push(k) })
  out
}

///|
/// 每个已打开 ns 的任务数统计(直接用该 ns 库的 tasks 表 COUNT)。
/// 返回 Json 数组,元素形如 {"ns": "...", "total": N}。
pub fn MultiStore::ns_stats(self : MultiStore) -> Json {
  let m : Map[String, Json] = Map([])
  fn count_tasks(store : SqliteStore) -> Int {
    let mut n = 0
    match store.db {
      None => n = 0
      Some(db) =>
        match db.prepare("SELECT COUNT(*) FROM tasks") {
          None => n = 0
          Some(stmt) => {
            if stmt.step() {
              n = stmt.column_int(0)
            }
            stmt.finalize()
          }
        }
    }
    n
  }

  self.stores.each(fn(ns, store) {
    let item : Map[String, Json] = Map([])
    item.set("ns", Json::string(ns))
    item.set("total", Json::number(count_tasks(store).to_double()))
    ignore(m.set(ns, Json::object(item)))
  })
  Json::object(m)
}