///| JS-exported Hub (issue) operations.

///|
fn[T] hub_wrap_error(f : () -> T raise @bit.GitError) -> Result[T, String] {
  Ok(f()) catch {
    @bit.GitError::IoError(e) => Err(e)
    @bit.GitError::InvalidObject(e) => Err(e)
    @bit.GitError::HashMismatch(expected, actual) =>
      Err("hash mismatch: \{expected} vs \{actual}")
    @bit.GitError::PackfileError(e) => Err("packfile error: \{e}")
    @bit.GitError::ProtocolError(e) => Err("protocol error: \{e}")
  }
}

///|
priv struct HubJsObjectStore {
  fs : HubJsHostFs
  git_dir : String
  // Cached lazily-loaded ObjectDb. Reused across get/has calls so pack
  // indexes are parsed at most once per store instance.
  mut cached_db : @lib.ObjectDb?
}

///|
/// Return a cached ObjectDb, building it lazily on first use.
///
/// Rebuilding it on every `get`/`has` re-parsed each pack `.idx` from
/// scratch (hex-encoding every object id it contains), so once `bit issue`
/// blobs were packed by gc, every read re-hashed the whole pack index.
/// Caching lets `LazyPackIndex` keep its parsed index between reads.
fn HubJsObjectStore::object_db(
  self : HubJsObjectStore,
) -> @lib.ObjectDb raise @bit.GitError {
  match self.cached_db {
    Some(db) => db
    None => {
      let db = @lib.ObjectDb::load_lazy(self.fs, self.git_dir)
      self.cached_db = Some(db)
      db
    }
  }
}

///|
impl @lib.ObjectStore for HubJsObjectStore with fn get(self, id) {
  let db = self.object_db()
  db.get(self.fs, id)
}

///|
impl @lib.ObjectStore for HubJsObjectStore with fn put(self, obj_type, content) {
  let (id, compressed) = @bit.create_object(obj_type, content)
  let hex = id.to_hex()
  let dir = self.git_dir +
    "/objects/" +
    String::unsafe_substring(hex, start=0, end=2)
  let path = dir + "/" + String::unsafe_substring(hex, start=2, end=40)
  (self.fs as &@bit.FileSystem).mkdir_p(dir)
  (self.fs as &@bit.FileSystem).write_file(path, compressed)
  id
}

///|
impl @lib.ObjectStore for HubJsObjectStore with fn has(self, id) {
  let db = self.object_db()
  let obj = db.get(self.fs, id)
  obj is Some(_)
}

///|
priv struct HubJsRefStore {
  fs : HubJsHostFs
  git_dir : String
}

///|
impl @lib.RefStore for HubJsRefStore with fn resolve(self, ref_name) {
  @lib.resolve_ref(self.fs, self.git_dir, ref_name)
}

///|
impl @lib.RefStore for HubJsRefStore with fn update(self, ref_name, id) {
  match id {
    Some(commit_id) => {
      let ref_path = self.git_dir + "/" + ref_name
      let dir = hub_parent_path(ref_path)
      (self.fs as &@bit.FileSystem).mkdir_p(dir)
      (self.fs as &@bit.FileSystem).write_string(
        ref_path,
        commit_id.to_hex() + "\n",
      )
    }
    None => ()
  }
}

///|
impl @lib.RefStore for HubJsRefStore with fn list(self, prefix) {
  let result : Array[String] = []
  let refs_dir = self.git_dir + "/refs"
  hub_collect_refs(self.fs, refs_dir, "refs", prefix, result)
  result
}

///|
fn hub_collect_refs(
  fs : HubJsHostFs,
  dir : String,
  prefix : String,
  filter : String,
  result : Array[String],
) -> Unit {
  let entries = (fs as &@bit.RepoFileSystem).readdir(dir) catch { _ => return }
  for entry in entries {
    let full = dir + "/" + entry
    let ref_name = prefix + "/" + entry
    if (fs as &@bit.RepoFileSystem).is_dir(full) {
      hub_collect_refs(fs, full, ref_name, filter, result)
    } else if ref_name.has_prefix(filter) {
      result.push(ref_name)
    }
  }
}

///|
priv struct HubJsClock {
  timestamp : Int64
}

///|
impl @lib.Clock for HubJsClock with fn now(self) {
  self.timestamp
}

///|
fn hub_parent_path(path : String) -> String {
  match path.rev_find("/") {
    None => "."
    Some(0) => "/"
    Some(idx) => String::unsafe_substring(path, start=0, end=idx)
  }
}

///|
fn hub_resolve_git_dir(
  fs : HubJsHostFs,
  root : String,
) -> String raise @bit.GitError {
  let git_path = root + "/.git"
  if (fs as &@bit.RepoFileSystem).is_file(git_path) {
    @lib.resolve_gitdir(fs, git_path)
  } else {
    git_path
  }
}

///|
extern "js" fn hub_js_date_now() -> Double =
  #| () => Date.now()

///|
fn hub_make_stores(
  host_id : Int,
  root : String,
) -> (HubJsObjectStore, HubJsRefStore, HubJsClock) raise @bit.GitError {
  let fs = hub_make_host_fs(host_id)
  let git_dir = hub_resolve_git_dir(fs, root)
  let objects = HubJsObjectStore::{ fs, git_dir, cached_db: None }
  let refs = HubJsRefStore::{ fs, git_dir }
  let now = hub_js_date_now().to_int64() / 1000L
  let clock = HubJsClock::{ timestamp: now }
  (objects, refs, clock)
}

// --- Issue API exports ---

///|
pub fn js_hub_issue_init(host_id : Int, root : String) -> Result[Unit, String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, _) = hub_make_stores(host_id, root)
    let _ = Hub::init(objects, refs)
  })
}

///|
pub fn js_hub_issue_list(
  host_id : Int,
  root : String,
  state : String,
) -> Result[Array[Issue], String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, _) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    let filter : IssueState? = match state {
      "open" => Some(IssueState::Open)
      "closed" => Some(IssueState::Closed)
      _ => None
    }
    hub.list_issues(objects, state=filter)
  })
}

///|
pub fn js_hub_issue_get(
  host_id : Int,
  root : String,
  issue_id : String,
) -> Result[Issue?, String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, _) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    hub.get_issue(objects, issue_id)
  })
}

///|
pub fn js_hub_issue_create(
  host_id : Int,
  root : String,
  title : String,
  body : String,
  author : String,
  labels : Array[String],
  parent_id : String,
) -> Result[Issue, String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, clock) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    let pid : String? = if parent_id.length() > 0 {
      Some(parent_id)
    } else {
      None
    }
    hub.create_issue(
      objects,
      refs,
      clock,
      title,
      body,
      author,
      labels~,
      parent_id=pid,
    )
  })
}

///|
pub fn js_hub_issue_update(
  host_id : Int,
  root : String,
  issue_id : String,
  title : String,
  body : String,
  labels : Array[String],
) -> Result[Issue, String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, clock) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    let t : String? = if title.length() > 0 { Some(title) } else { None }
    let b : String? = if body.length() > 0 { Some(body) } else { None }
    let l : Array[String]? = if labels.length() > 0 {
      Some(labels)
    } else {
      None
    }
    hub.update_issue(objects, refs, clock, issue_id, title=t, body=b, labels=l)
  })
}

///|
pub fn js_hub_issue_close(
  host_id : Int,
  root : String,
  issue_id : String,
) -> Result[Unit, String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, clock) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    hub.close_issue(objects, refs, clock, issue_id)
  })
}

///|
pub fn js_hub_issue_reopen(
  host_id : Int,
  root : String,
  issue_id : String,
) -> Result[Unit, String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, clock) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    hub.reopen_issue(objects, refs, clock, issue_id)
  })
}

///|
pub fn js_hub_issue_comment_list(
  host_id : Int,
  root : String,
  issue_id : String,
) -> Result[Array[IssueComment], String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, _) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    hub.list_issue_comments(objects, issue_id)
  })
}

///|
pub fn js_hub_issue_comment_add(
  host_id : Int,
  root : String,
  issue_id : String,
  author : String,
  body : String,
) -> Result[IssueComment, String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, clock) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    hub.add_issue_comment(objects, refs, clock, issue_id, author, body)
  })
}

///|
pub fn js_hub_issue_search(
  host_id : Int,
  root : String,
  query : String,
  state : String,
) -> Result[Array[Issue], String] {
  hub_wrap_error(fn() -> _ raise _ {
    let (objects, refs, _) = hub_make_stores(host_id, root)
    let hub = Hub::load(objects, refs)
    let filter : IssueState? = match state {
      "open" => Some(IssueState::Open)
      "closed" => Some(IssueState::Closed)
      _ => None
    }
    let all = hub.list_issues(objects, state=filter)
    let q = query.to_lower()
    let result : Array[Issue] = []
    for issue in all {
      if issue.title.to_lower().contains(q) || issue.body.to_lower().contains(q) {
        result.push(issue)
      }
    }
    result
  })
}