///| Git repository initialization

///|
pub(all) struct InitOptions {
  default_branch : String
  bare : Bool
  separate_git_dir : String?
  template_dir : String?
  ref_format : String
  object_format : String
  is_reinit : Bool
  git_dir : String? // explicit GIT_DIR override (no .git file created)
  work_tree : String? // explicit core.worktree to write to config
  shared : String? // --shared value for directory permissions
}

///|
pub fn InitOptions::new() -> InitOptions {
  {
    default_branch: "main",
    bare: false,
    separate_git_dir: None,
    template_dir: None,
    ref_format: "files",
    object_format: "sha1",
    is_reinit: false,
    git_dir: None,
    work_tree: None,
    shared: None,
  }
}

///|
/// Initialize a repository at `root` with a default branch.
/// If `bare` is true, creates a bare repository (no working tree).
/// If the repository already exists, this is a reinit and preserves existing HEAD.
pub fn init_repo(
  fs : &@bit.FileSystem,
  root : String,
  default_branch? : String = "main",
  bare? : Bool = false,
) -> Unit raise @bit.GitError {
  init_repo_with_options(fs, root, {
    default_branch,
    bare,
    separate_git_dir: None,
    template_dir: None,
    ref_format: "files",
    object_format: "sha1",
    is_reinit: false,
    git_dir: None,
    work_tree: None,
    shared: None,
  })
}

///|
/// Initialize a repository with full options. If `separate_git_dir` is set, this
/// version does not inspect the existing `.git` target and only updates `.git` atomically.
pub fn init_repo_with_options(
  fs : &@bit.FileSystem,
  root : String,
  opts : InitOptions,
) -> Unit raise @bit.GitError {
  let bare = opts.bare
  let git_dir = match (opts.git_dir, opts.separate_git_dir) {
    (Some(explicit_dir), _) => explicit_dir
    (None, Some(sep_dir)) => sep_dir
    (None, None) => if bare { root } else { join_path(root, ".git") }
  }
  let use_default_template_files = opts.template_dir is None
  let head_path = join_path(git_dir, "HEAD")
  if opts.separate_git_dir is Some(_) && !bare {
    fs.mkdir_p(root)
  }
  fs.mkdir_p(git_dir)
  fs.mkdir_p(join_path(git_dir, "objects"))
  fs.mkdir_p(join_path(git_dir, "objects/info"))
  fs.mkdir_p(join_path(git_dir, "objects/pack"))
  fs.mkdir_p(join_path(git_dir, "refs/heads"))
  fs.mkdir_p(join_path(git_dir, "refs/tags"))
  if use_default_template_files {
    fs.mkdir_p(join_path(git_dir, "hooks"))
    fs.mkdir_p(join_path(git_dir, "info"))
  }
  if opts.separate_git_dir is Some(_) && !bare {
    let gitfile_path = join_path(root, ".git")
    fs.remove_dir(gitfile_path) catch {
      _ => ()
    }
    fs.remove_file(gitfile_path) catch {
      _ => ()
    }
    fs.write_string(gitfile_path, "gitdir: " + git_dir + "\n")
  }
  if !opts.is_reinit {
    fs.write_string(head_path, "ref: refs/heads/" + opts.default_branch + "\n")
  }
  let bare_str = if bare { "true" } else { "false" }
  let mut repository_format_version = "0"
  if opts.ref_format == "reftable" || opts.object_format != "sha1" {
    repository_format_version = "1"
  }
  let mut config = "[core]\n\trepositoryformatversion = " +
    repository_format_version +
    "\n\tfilemode = true\n\tbare = " +
    bare_str +
    "\n"
  match opts.work_tree {
    Some(wt) => config = config + "\tworktree = " + wt + "\n"
    None => ()
  }
  if !bare {
    config = config + "\tlogallrefupdates = true\n"
  }
  if opts.ref_format == "reftable" {
    fs.mkdir_p(join_path(git_dir, "reftable"))
    fs.write_string(join_path(git_dir, "reftable/tables.list"), "")
  }
  let extension_lines : Array[String] = []
  if opts.ref_format == "reftable" {
    extension_lines.push("\trefStorage = reftable")
  }
  if opts.object_format != "sha1" {
    extension_lines.push("\tobjectFormat = " + opts.object_format)
  }
  if extension_lines.length() > 0 {
    config += "[extensions]\n" + extension_lines.join("\n") + "\n"
  }
  match opts.shared {
    Some(shared_val) =>
      config = config + "\tsharedRepository = " + shared_val + "\n"
    None => ()
  }
  fs.write_string(join_path(git_dir, "config"), config)
  if use_default_template_files {
    fs.write_string(
      join_path(git_dir, "description"),
      "Unnamed repository; edit this file 'description' to name the repository.\n",
    )
    fs.write_string(join_path(git_dir, "info/exclude"), "")
  }
}

///|
/// Initialize a repository with full options and full `.git` resolution via RepoFileSystem.
/// This is used when reinitializing with --separate-git-dir to migrate existing data.
pub fn init_repo_with_options_with_repo_fs(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  root : String,
  opts : InitOptions,
) -> Unit raise @bit.GitError {
  let bare = opts.bare
  // Determine git directory location
  let git_dir = match (opts.git_dir, opts.separate_git_dir) {
    (Some(explicit_dir), _) => explicit_dir
    (None, Some(sep_dir)) => sep_dir
    (None, None) => if bare { root } else { join_path(root, ".git") }
  }
  let use_default_template_files = opts.template_dir is None
  let head_path = join_path(git_dir, "HEAD")
  // Create work tree directory if needed (for --separate-git-dir)
  if opts.separate_git_dir is Some(_) && !bare {
    fs.mkdir_p(root)
  }
  // Create git directory structure
  fs.mkdir_p(git_dir)
  fs.mkdir_p(join_path(git_dir, "objects"))
  fs.mkdir_p(join_path(git_dir, "objects/info"))
  fs.mkdir_p(join_path(git_dir, "objects/pack"))
  fs.mkdir_p(join_path(git_dir, "refs/heads"))
  fs.mkdir_p(join_path(git_dir, "refs/tags"))
  if use_default_template_files {
    fs.mkdir_p(join_path(git_dir, "hooks"))
    fs.mkdir_p(join_path(git_dir, "info"))
  }
  // If separate-git-dir, create .git file pointing to actual git dir
  if opts.separate_git_dir is Some(_) && !bare {
    let gitfile_path = join_path(root, ".git")
    let existing_git_dir = if rfs.is_dir(gitfile_path) {
      Some(gitfile_path)
    } else if rfs.is_file(gitfile_path) {
      let existing = resolve_gitdir(rfs, gitfile_path)
      if existing == gitfile_path {
        None
      } else {
        Some(existing)
      }
    } else {
      None
    }
    match existing_git_dir {
      Some(old_git_dir) =>
        if old_git_dir != git_dir && rfs.is_dir(old_git_dir) {
          if rfs.is_dir(git_dir) || rfs.is_file(git_dir) {
            remove_tree(fs, rfs, git_dir)
          }
          move_tree(fs, rfs, old_git_dir, git_dir)
        }
      None => ()
    }
    fs.write_string(gitfile_path, "gitdir: " + git_dir + "\n")
  }
  // Write HEAD only on fresh init (not re-init)
  if !opts.is_reinit {
    fs.write_string(head_path, "ref: refs/heads/" + opts.default_branch + "\n")
  }
  // Create/update config file with appropriate bare setting
  let bare_str = if bare { "true" } else { "false" }
  let mut repository_format_version = "0"
  if opts.ref_format == "reftable" || opts.object_format != "sha1" {
    repository_format_version = "1"
  }
  let mut config = "[core]\n\trepositoryformatversion = " +
    repository_format_version +
    "\n\tfilemode = true\n\tbare = " +
    bare_str +
    "\n"
  // Write core.worktree if explicitly set
  match opts.work_tree {
    Some(wt) => config = config + "\tworktree = " + wt + "\n"
    None => ()
  }
  // Write core.logallrefupdates for non-bare repos
  if !bare {
    config = config + "\tlogallrefupdates = true\n"
  }
  if opts.ref_format == "reftable" {
    fs.mkdir_p(join_path(git_dir, "reftable"))
    let tables_list_path = join_path(git_dir, "reftable/tables.list")
    fs.write_string(tables_list_path, "")
  }
  let extension_lines : Array[String] = []
  if opts.ref_format == "reftable" {
    extension_lines.push("\trefStorage = reftable")
  }
  if opts.object_format != "sha1" {
    extension_lines.push("\tobjectFormat = " + opts.object_format)
  }
  if extension_lines.length() > 0 {
    config += "[extensions]\n" + extension_lines.join("\n") + "\n"
  }
  // Write --shared config
  match opts.shared {
    Some(shared_val) =>
      config = config + "\tsharedRepository = " + shared_val + "\n"
    None => ()
  }
  fs.write_string(join_path(git_dir, "config"), config)
  if use_default_template_files {
    // Create description file
    fs.write_string(
      join_path(git_dir, "description"),
      "Unnamed repository; edit this file 'description' to name the repository.\n",
    )
    // Create info/exclude
    fs.write_string(join_path(git_dir, "info/exclude"), "")
  }
}

///|
/// Copy template directory contents to git directory.
/// This should be called after init_repo_with_options if template_dir is specified.
pub fn apply_template(
  wfs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  template_dir : String,
) -> Unit raise @bit.GitError {
  copy_dir_recursive(wfs, rfs, template_dir, git_dir)
}

///|
fn copy_dir_recursive(
  wfs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  src : String,
  dst : String,
) -> Unit raise @bit.GitError {
  if !rfs.is_dir(src) {
    return
  }
  wfs.mkdir_p(dst)
  let entries = rfs.readdir(src)
  for entry in entries {
    if entry == "." || entry == ".." {
      continue
    }
    let src_path = join_path(src, entry)
    let dst_path = join_path(dst, entry)
    if rfs.is_dir(src_path) {
      copy_dir_recursive(wfs, rfs, src_path, dst_path)
    } else if rfs.is_file(src_path) {
      // Don't overwrite existing files
      if !rfs.is_file(dst_path) {
        let content = rfs.read_file(src_path)
        wfs.write_file(dst_path, content)
      }
    }
  }
}