///|
/// The shrc store: the manifest of managed entries, and the code that writes
/// them into every detected startup file.

///|
/// Read the manifest. A missing or unreadable manifest is an empty store, not
/// an error: the first `shrc set` creates it.
pub fn load_entries() -> Array[Entry] {
  let path = manifest_path()
  if !@fs.path_exists(path) {
    return []
  }
  let text = @fs.read_file_to_string(path) catch { _ => return [] }
  parse_entries(text)
}

///|
/// Persist the manifest, creating `SHRC_HOME` when needed.
pub fn save_entries(entries : Array[Entry]) -> Result[Unit, String] {
  match ensure_dir(shrc_home()) {
    Ok(_) => ()
    Err(msg) => return Err(msg)
  }
  @fs.write_string_to_file(manifest_path(), serialize_entries(entries)) catch {
    _ => return Err("cannot write '\{manifest_path()}'")
  }
  Ok(())
}

///|
/// Write `entries` into every startup file in `shells`.
///
/// Returns one human-readable message per file that could not be written; an
/// empty array means everything succeeded. Imported entries (adopted from the
/// startup files) are left in place and are not rendered into the managed
/// block, so only entries added by `shrc set` live there.
pub fn apply_to_shells(
  entries : Array[Entry],
  shells : Array[Shell],
) -> Array[String] {
  let managed = []
  for entry in entries {
    if !entry.imported {
      managed.push(entry)
    }
  }
  let errors = []
  for shell in shells {
    let exists = @fs.path_exists(shell.rc)
    let existing = if exists {
      @fs.read_file_to_string(shell.rc) catch {
        _ => ""
      }
    } else {
      ""
    }
    let updated = sync_block(existing, shell.family, managed)
    // Removing the last entry leaves an empty block, and an empty block in a
    // file that never existed should not create one.
    if exists || updated.length() > 0 {
      match ensure_dir(path_dirname(shell.rc)) {
        Ok(_) => ()
        Err(msg) => {
          errors.push(msg)
          continue
        }
      }
      @fs.write_string_to_file(shell.rc, updated) catch {
        _ => errors.push("cannot write '\{shell.rc}'")
      }
    }
  }
  errors
}

///|
/// One line per entry, in the order they are stored. A search-path entry is
/// listed as one row per directory, indexed in search order, instead of one
/// long line — so `shrc ls PATH` shows what the shell will actually search,
/// in order.
pub fn format_ls(entries : Array[Entry]) -> String {
  let out = StringBuilder()
  for entry in entries {
    out.write_string(kind_label(entry.kind))
    out.write_string("\t")
    if is_search_path_entry(entry) {
      out.write_string(entry.name)
      out.write_char('\n')
      let parts = split_path(entry.value)
      if parts.length() == 0 {
        out.write_string("\t(empty)\n")
      }
      let mut i = 0
      for part in parts {
        out.write_string("\t\{i}\t\{part}\n")
        i = i + 1
      }
    } else {
      out.write_string(entry.name)
      out.write_string("=")
      out.write_string(entry.value)
      out.write_char('\n')
    }
  }
  out.to_string()
}

///|
/// The complete picture: the manifest, then every shell shrc would manage and
/// the block it would receive.
pub fn format_all(entries : Array[Entry], shells : Array[Shell]) -> String {
  let out = StringBuilder()
  out.write_string("# shrc — managed shell configuration\n")
  out.write_string("# home: \{shrc_home()}\n")
  out.write_string("\n# entries (\{entries.length()})\n")
  if entries.length() == 0 {
    out.write_string("#   (none)\n")
  } else {
    for entry in entries {
      if is_search_path_entry(entry) {
        out.write_string("#   \{kind_label(entry.kind)}  \{entry.name}\n")
        let mut i = 0
        for part in split_path(entry.value) {
          out.write_string("#     \{i}  \{part}\n")
          i = i + 1
        }
      } else {
        out.write_string(
          "#   \{kind_label(entry.kind)}  \{entry.name}=\{entry.value}\n",
        )
      }
    }
  }
  out.write_string("\n# startup files\n")
  for shell in shells {
    out.write_string("\n# \{shell.name}  \{shell.rc}\n")
    if entries.length() == 0 {
      out.write_string("#   (nothing to write)\n")
    } else {
      for entry in entries {
        out.write_string(render_entry(shell.family, entry))
        out.write_char('\n')
      }
    }
  }
  out.to_string()
}