///|
let id_prefix = "mb-"

///|
let legacy_prefix = "bd-"

///|
pub struct Store {
  beads_dir : String
  issues_path : String
  backup_path : String
  tmp_path : String
  lock_path : String
}

///|
pub fn Store::new(beads_dir? : String = ".beads") -> Store {
  let issues_path = join_path(beads_dir, "issues.jsonl")
  let backup_path = join_path(beads_dir, "issues.jsonl.bak")
  let tmp_path = join_path(beads_dir, "issues.jsonl.tmp")
  let lock_path = join_path(beads_dir, "lock")
  { beads_dir, issues_path, backup_path, tmp_path, lock_path }
}

///|
pub fn Store::lock_path(self : Store) -> String {
  self.lock_path
}

///|
pub fn Store::issues_path(self : Store) -> String {
  self.issues_path
}

///|
pub fn Store::backup_path(self : Store) -> String {
  self.backup_path
}

///|
pub fn Store::tmp_path(self : Store) -> String {
  self.tmp_path
}

///|
pub async fn Store::ensure_workspace(self : Store) -> Unit {
  try @fs.mkdir(self.beads_dir, permission=0o755, recursive=true) catch {
    @os_error.OSError(_) as err if err.is_EEXIST() => ()
    err => raise err
  } noraise {
    _ => ()
  }
}

///|
pub async fn Store::load_issues(self : Store) -> Array[Issue] {
  try load_issues_from_path(self.issues_path) catch {
    _ =>
      if @fs.exists(self.backup_path) {
        try load_issues_from_path(self.backup_path) catch {
          _ => []
        } noraise {
          issues => issues
        }
      } else {
        []
      }
  } noraise {
    issues => issues
  }
}

///|
pub async fn Store::save_issues(self : Store, issues : Array[Issue]) -> Unit {
  let lines = issues.map(issue => issue.to_json().stringify())
  let body = if lines.length() == 0 { "" } else { lines.join("\n") + "\n" }
  @fs.write_file(self.tmp_path, body, create=0o644, truncate=true)
  if @fs.exists(self.issues_path) {
    let current = @fs.read_file(self.issues_path)
    @fs.write_file(
      self.backup_path,
      current.binary(),
      create=0o644,
      truncate=true,
    )
  }
  @fs.write_file(self.issues_path, body, create=0o644, truncate=true)
  try @fs.remove(self.tmp_path) catch {
    _ => ()
  } noraise {
    _ => ()
  }
}

///|
async fn load_issues_from_path(path : String) -> Array[Issue] {
  if !@fs.exists(path) {
    return []
  }
  let data = @fs.read_file(path)
  let content = data.text()
  let issues = []
  for line in content.split("\n") {
    let trimmed = line.trim()
    if trimmed.is_empty() {
      continue
    }
    let json = @json.parse(trimmed)
    let issue : Issue = @json.from_json(json)
    issues.push(issue)
  }
  issues
}

///|
pub fn next_issue_id(issues : Array[Issue]) -> String {
  let mut max_id = 0
  for issue in issues {
    match parse_issue_number(issue.id) {
      Some(num) => if num > max_id { max_id = num }
      None => ()
    }
  }
  "\{id_prefix}\{max_id + 1}"
}

///|
fn parse_issue_number(id : String) -> Int? {
  match id.strip_prefix(id_prefix) {
    Some(rest) =>
      try @strconv.parse_int(rest, base=10) catch {
        _ => None
      } noraise {
        num => Some(num)
      }
    None =>
      match id.strip_prefix(legacy_prefix) {
        Some(rest) =>
          try @strconv.parse_int(rest, base=10) catch {
            _ => None
          } noraise {
            num => Some(num)
          }
        None => None
      }
  }
}

///|
pub fn find_issue_index(issues : Array[Issue], id : String) -> Int? {
  for i, issue in issues {
    if issue.id == id {
      return Some(i)
    }
  }
  None
}

///|
pub fn build_status_map(issues : Array[Issue]) -> Map[String, String] {
  let map : Map[String, String] = {}
  for issue in issues {
    map[issue.id] = issue.status
  }
  map
}