///|
/// edit 执行层(jumbit 扩展,规格 docs/spec-plaintext-edit.md §1.4):
/// 三形态共享「加载 → 变换 → 校验 → save」管线,语义权威源单一。
/// 默认形态:$VISUAL/$EDITOR 编辑 db.txt 副本,读回解析,无变化静默、
/// 有变化整库替换;--rename 改路径(note 跟随);--prune 清除不存在条目
async fn run_edit(
  eopts : @cli.EditOptions,
  getenv : (String) -> String?,
  errs : Array[String],
) -> Int {
  let data_dir = match cfg(@config.data_dir(getenv), errs) {
    Some(data_dir) => data_dir
    None => return 1
  }
  let store = match @platform.load(data_dir) {
    Ok(s) => s
    Err(e) => {
      err_line(errs, "jumbit: could not open database: \{e.message()}")
      return 1
    }
  }
  let db = store.database()
  // 结构化形态先于交互形态(互斥已在解析层裁定)
  if eopts.prune {
    return run_edit_prune(db, store, errs)
  }
  match eopts.rename_old {
    Some(old) => {
      let new = match eopts.rename_new {
        Some(n) => n
        None => return 1 // 解析层保证不达
      }
      return run_edit_rename(db, store, old, new, errs)
    }
    None => ()
  }
  run_edit_interactive(data_dir, db, store, getenv, errs)
}

///|
/// --rename:精确路径匹配(remove 同款双重尝试:原串 → 规范化);
/// note 跟随迁移;new 撞库内既有条目报错不合并
async fn run_edit_rename(
  db : @core.Database,
  store : @platform.Store,
  old : String,
  new : String,
  errs : Array[String],
) -> Int {
  let cwd = match @env.current_dir() {
    Some(d) => d
    None => "/"
  }
  // 撞名预检:new 已在库中(双重尝试同款)→ 报错不合并(规格 §1.4;
  // old==new 视为撞自身,同样拒绝——重命名到自身无意义且语义含糊)
  let new_norm = @core.resolve_path(new, cwd)
  if db.find_idx(new_norm) is Some(_) {
    err_line(errs, "path already in database: \{new}")
    return 1
  }
  let renamed = try_twice(db, old, cwd, fn(db, p) { db.rename(p, new) })
  if !renamed {
    err_line(errs, "path not found in database: \{old}")
    return 1
  }
  match store.save() {
    Ok(_) => 0
    Err(e) => {
      err_line(errs, "jumbit: could not save database: \{e.message()}")
      1
    }
  }
}

///|
/// --prune:全量存在性扫描(status --check 同款),移除不存在条目并逐行
/// 输出清单(stdout);无移除静默 exit 0
async fn run_edit_prune(
  db : @core.Database,
  store : @platform.Store,
  errs : Array[String],
) -> Int {
  let removed : Array[String] = []
  for d in db.all() {
    if !(@afs.exists(d.path) catch { _ => false }) {
      removed.push(d.path)
    }
  }
  if removed.is_empty() {
    return 0
  }
  for p in removed {
    // remove 的 swap_remove 语义(懒删除同款);已确认存在才会进清单
    let _ = db.remove(p)
    println(p)
  }
  match store.save() {
    Ok(_) => 0
    Err(e) => {
      err_line(errs, "jumbit: could not save database: \{e.message()}")
      1
    }
  }
}

///|
/// 默认形态:db.txt 当前内容写临时文件 → $VISUAL/$EDITOR 继承终端编辑 →
/// 读回解析(decode_plaintext 全套行级校验)→ 与编辑前比对:无变化静默、
/// 有变化整库替换 + save。note 不进编辑面(notes.tsv 侧文件),编辑后按
/// path 匹配保留原标注(path 改名 = 丢标注,规格 §1.4 注明)
async fn run_edit_interactive(
  data_dir : String,
  db : @core.Database,
  store : @platform.Store,
  getenv : (String) -> String?,
  errs : Array[String],
) -> Int {
  // 编辑面内容 = 当前主库序列化(不含 note 列)
  let before = @core.encode_plaintext(db.all())
  let tmp = data_dir + "/db.txt.edit"
  let opened = @afs.create(tmp) catch {
    err => {
      err_line(
        errs,
        "jumbit: could not write temporary file: \{@platform.os_error_message(err)}",
      )
      return 1
    }
  }
  opened.write(before) catch {
    err => {
      ignore(err)
      let _ = @afs.remove(tmp)
      err_line(errs, "jumbit: could not write temporary file")
      return 1
    }
  }
  opened.sync()
  // 起编辑器(继承终端;退出码非 0 = 用户取消/编辑器故障,不落库)
  match @platform.run_editor(tmp, getenv) {
    Err(NotFound) => {
      err_line(errs, "no editor: set $EDITOR or $VISUAL")
      let _ = @afs.remove(tmp)
      return 1
    }
    Err(Failed(code)) => {
      err_line(errs, "editor exited with code \{code}")
      let _ = @afs.remove(tmp)
      return 1
    }
    Err(Spawn) => {
      err_line(errs, "could not launch editor")
      let _ = @afs.remove(tmp)
      return 1
    }
    Ok(_) => ()
  }
  // 读回 + 行级校验(坏行 → 不落库,报「数据文件损坏」同款前缀)
  let data = @afs.read_file(tmp) catch {
    err => {
      err_line(
        errs,
        "jumbit: could not read editor output: \{@platform.os_error_message(err)}",
      )
      let _ = @afs.remove(tmp)
      return 1
    }
  }
  let dirs = match @core.decode_plaintext(data.binary()) {
    Ok(dirs) => dirs
    Err(e) => {
      err_line(errs, "jumbit: corrupted data file: \{e.message()}")
      let _ = @afs.remove(tmp)
      return 1
    }
  }
  let _ = @afs.remove(tmp)
  // 无变化判定:字节级(编码确定 ⇒ 内容等价)
  if @core.encode_plaintext(dirs) == before {
    return 0
  }
  // note 保留:编辑前有标注的 path 在新集合中仍存在则继承
  let old_notes : Array[@core.Dir] = db.all()
  for nd in dirs {
    for od in old_notes {
      if od.path == nd.path && nd.note is None {
        nd.note = od.note
      }
    }
  }
  db.replace_all(dirs)
  match store.save() {
    Ok(_) => 0
    Err(e) => {
      err_line(errs, "jumbit: could not save database: \{e.message()}")
      1
    }
  }
}