///|
async fn migration_kind(path : String) -> @fs.FileKind? raise DbError {
  try @fs.kind(path, follow_symlink=false) catch {
    @os_error.OSError(_, ..) as err if err.is_ENOENT() => None
    err => raise io_error(err)
  } noraise {
    kind => Some(kind)
  }
}

///|
async fn migration_path(path : String) -> String raise DbError {
  let path = if @async.platform is Windows {
    path.replace(old="\\", new="/")
  } else {
    path
  }
  let (parent, name) = match path.rev_find("/") {
    Some(index) =>
      (
        if index == 0 {
          "/"
        } else {
          path[:index].to_owned()
        },
        path[index + 1:].to_owned(),
      )
    None => (".", path)
  }
  guard !name.is_empty() && name != "." && name != ".." else {
    raise DbError::Io("migration requires a file basename")
  }
  let parent = @fs.realpath(parent) catch { err => raise io_error(err) }
  guard migration_kind(parent) == Some(@fs.Directory) else {
    raise DbError::Io("migration parent must be an existing directory")
  }
  if parent.has_suffix("/") {
    "\{parent}\{name}"
  } else {
    "\{parent}/\{name}"
  }
}

///|
fn migration_same_path(left : String, right : String) -> Bool {
  if @async.platform is Windows {
    left.to_lower() == right.to_lower()
  } else {
    left == right
  }
}

///|
async fn migration_require_absent(path : String) -> Unit raise DbError {
  guard migration_kind(path) == None else {
    raise DbError::Io("migration destination directory entry exists: \{path}")
  }
}

///|
async fn migration_read_source(path : String) -> Bytes raise DbError {
  match migration_kind(path) {
    None => b""
    Some(@fs.Regular) => {
      let data = @fs.read_file(path) catch { err => raise io_error(err) }
      data.binary()
    }
    Some(@fs.SymLink) =>
      raise DbError::Io("migration source symlink rejected: \{path}")
    Some(_) =>
      raise DbError::Io("migration source must be a regular file: \{path}")
  }
}

///|
fn migration_each_entry(
  pages : Map[Int32, Bytes],
  visit : (BytesView, BytesView) -> Unit raise DbError,
) -> Int32 raise DbError {
  let mut count = 0
  for _, page in pages {
    if page[0] == kind_leaf {
      let mut offset = 7
      for _ in 0.. BytesView? raise DbError {
  let mut id = tree.root
  let mut remaining = tree.next_page
  while id != 0 {
    guard id > 0 && id < tree.next_page && remaining > 0 else {
      raise DbError::Corrupt
    }
    remaining -= 1
    let page = tree.pages[id]
    let index = tree.page_index(id, page)
    let branch = page[0] == kind_branch
    let low = index_position(index, key, branch~)
    if branch {
      id = read_u32(page, 3 + low * 4).reinterpret_as_int()
    } else {
      guard low < index.offsets.length() else { return None }
      let offset = index.offsets[low]
      let start = offset + 4
      let key_len = read_u16(page, offset)
      guard page[start:start + key_len] == key else { return None }
      let value_len = read_u16(page, offset + 2)
      return Some(page[start + key_len:start + key_len + value_len])
    }
  }
  None
}

///|
fn migration_verify(
  tree : Tree,
  source : Map[Int32, Bytes],
  expected : Int32,
) -> Unit raise DbError {
  tree.validate()
  let mut actual = 0
  for id, page in tree.pages {
    if id != 0 && page[0] == kind_leaf {
      actual += read_u16(page, 1)
    }
  }
  guard actual == expected else { raise DbError::Corrupt }
  let verified = migration_each_entry(source, (key, value) => {
    guard migration_value(tree, key) == Some(value) else {
      raise DbError::Corrupt
    }
  })
  guard verified == expected else { raise DbError::Corrupt }
}

///|
/// 只读旧库及 WAL,在不同路径重建并核对 v2;目标主文件/WAL 目录项必须不存在。
/// 正常返回才算完成;错误或取消后检查 staging/目标,再由调用者显式切换路径。
pub async fn migrate_v1(
  source : String,
  destination : String,
) -> Unit raise DbError {
  let source = migration_path(source)
  let destination = migration_path(destination)
  let source_wal = wal_path(source)
  let destination_wal = wal_path(destination)
  guard !migration_same_path(source, destination) &&
    !migration_same_path(source, destination_wal) &&
    !migration_same_path(source_wal, destination) &&
    !migration_same_path(source_wal, destination_wal) else {
    raise DbError::Io("migration source and destination paths overlap")
  }
  migration_require_absent(destination)
  migration_require_absent(destination_wal)
  let wal_bytes = migration_read_source(source_wal)
  let replayed = replay_wal(wal_bytes)
  let bytes = migration_read_source(source)
  let (_, source_pages) = legacy_read_image(bytes, replayed)
  let tree = Tree::new()
  let count = migration_each_entry(source_pages, (key, value) => {
    tree.put(key.to_owned(), value.to_owned())
  })
  migration_verify(tree, source_pages, count)
  tree.pages[0] = page_bytes(
    encode_header({
      root: tree.root,
      freelist: tree.freelist,
      txn_id: 1,
      page_count: tree.next_page,
    }),
  )
  let random = @random.Rand::chacha8()
  let staging = "\{destination}.vellum-migrate-\{@async.now()}-\{random.uint64()}"
  let staging_wal = wal_path(staging)
  let file = @fs.create(staging, allow_existing=false, permission=0o600) catch {
    err => raise io_error(err)
  }
  file.close()
  // Cleanup only paths created exclusively by this call, never the final destination.
  defer migration_cleanup(staging)
  let wal_file = @fs.create(staging_wal, allow_existing=false, permission=0o600) catch {
    err => raise io_error(err)
  }
  wal_file.close()
  defer migration_cleanup(staging_wal)
  write_page_batch(staging, tree.pages)
  write_synced_bytes(staging_wal, b"")
  let persisted_bytes = migration_read_source(staging)
  let (meta, pages) = read_image(persisted_bytes, Map([]))
  let persisted = Tree::new()
  persisted.root = meta.root
  persisted.freelist = meta.freelist
  persisted.next_page = meta.page_count
  for id, page in pages {
    persisted.pages[id] = page
  }
  migration_verify(persisted, source_pages, count)
  migration_require_absent(destination_wal)
  @fs.rename(staging, destination, replace=false) catch {
    err => raise io_error(err)
  }
}

///|
async fn migration_cleanup(path : String) -> Unit noraise + nocancel {
  @fs.remove(path) catch {
    _ => ()
  }
}