///| LFS pointer file parsing and utilities

///|
pub struct LfsPointer {
  oid : String
  size : Int64
} derive(Debug, Eq)

///|
let lfs_version_prefix : String = "version https://git-lfs.github.com/spec/v1\n"

///|
let lfs_oid_prefix : String = "oid sha256:"

///|
let lfs_size_prefix : String = "size "

///|
let lfs_max_pointer_size : Int = 1024

///|
fn lfs_sha256_hex(data : Bytes) -> String {
  let raw = @bithash.sha256_bytes(data)
  let digits = "0123456789abcdef"
  let out = StringBuilder::new()
  for b in raw {
    let v = b.to_int()
    out.write_char(digits[v / 16].to_int().unsafe_to_char())
    out.write_char(digits[v % 16].to_int().unsafe_to_char())
  }
  out.to_string()
}

///|
pub fn is_lfs_pointer(content : Bytes) -> Bool {
  if content.length() == 0 || content.length() > lfs_max_pointer_size {
    return false
  }
  let text = @utf8.decode_lossy(content[:])
  text.has_prefix(lfs_version_prefix)
}

///|
pub fn parse_lfs_pointer(content : Bytes) -> LfsPointer? {
  if content.length() == 0 || content.length() > lfs_max_pointer_size {
    return None
  }
  let text = @utf8.decode_lossy(content[:])
  if !text.has_prefix(lfs_version_prefix) {
    return None
  }
  let mut oid : String? = None
  let mut size : Int64? = None
  for line_view in text.split("\n") {
    let line = line_view.to_owned()
    if line.has_prefix(lfs_oid_prefix) {
      oid = Some(
        String::unsafe_substring(
          line,
          start=lfs_oid_prefix.length(),
          end=line.length(),
        ),
      )
    } else if line.has_prefix(lfs_size_prefix) {
      let num_str = String::unsafe_substring(
        line,
        start=lfs_size_prefix.length(),
        end=line.length(),
      )
      let n = @string.parse_int64(num_str.trim()) catch { _ => -1L }
      if n >= 0L {
        size = Some(n)
      }
    }
  }
  match (oid, size) {
    (Some(o), Some(s)) =>
      if o.length() == 64 && @bithash.is_hex_string(o) {
        Some({ oid: o, size: s })
      } else {
        None
      }
    _ => None
  }
}

///|
pub fn lfs_object_path(git_dir : String, oid : String) -> String {
  let d1 = String::unsafe_substring(oid, start=0, end=2)
  let d2 = String::unsafe_substring(oid, start=2, end=4)
  "\{git_dir}/lfs/objects/\{d1}/\{d2}/\{oid}"
}

///|
fn lfs_is_session_id_char(c : Char) -> Bool {
  (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
}

///|
fn lfs_is_relay_git_session_url(remote_url : String) -> Bool {
  if !(remote_url.has_prefix("http://") || remote_url.has_prefix("https://")) {
    return false
  }
  guard remote_url.rev_find("/git/") is Some(idx) else { return false }
  let session = String::unsafe_substring(
    remote_url,
    start=idx + 5,
    end=remote_url.length(),
  )
  if session.length() < 6 || session.length() > 16 {
    return false
  }
  for c in session {
    if !lfs_is_session_id_char(c) {
      return false
    }
  }
  true
}

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

///|
fn lfs_pointer_content(oid : String, size : Int64) -> Bytes {
  @utf8.encode(
    "\{lfs_version_prefix}\{lfs_oid_prefix}\{oid}\n\{lfs_size_prefix}\{size}\n",
  )
}

///|
fn lfs_pointer_for_content(content : Bytes) -> Bytes {
  if is_lfs_pointer(content) {
    content
  } else {
    lfs_pointer_content(lfs_sha256_hex(content), content.length().to_int64())
  }
}

///|
fn lfs_clean_content(
  fs : &@bit.FileSystem,
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  content : Bytes,
) -> Bytes raise @bit.GitError {
  if is_lfs_pointer(content) {
    return content
  }
  let oid = lfs_sha256_hex(content)
  let path = lfs_object_path(git_dir, oid)
  if !rfs.is_file(path) {
    fs.mkdir_p(lfs_parent_dir(path))
    fs.write_file(path, content)
  }
  lfs_pointer_for_content(content)
}

///|
/// Resolve LFS server URL from pre-read config values.
/// Priority: lfs.url > remote..lfsurl > derive from remote URL.
pub fn resolve_lfs_url_from_config(
  lfs_url? : String = "",
  remote_lfs_url? : String = "",
  remote_url? : String = "",
) -> String {
  if lfs_url.length() > 0 {
    return lfs_url
  }
  if remote_lfs_url.length() > 0 {
    return remote_lfs_url
  }
  let base = if remote_url.has_suffix(".git") {
    remote_url
  } else if lfs_is_relay_git_session_url(remote_url) {
    remote_url
  } else {
    remote_url + ".git"
  }
  base + "/info/lfs"
}

///|
/// Read LFS URL from git config and remote config.
pub fn resolve_lfs_url(
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  remote_name : String,
) -> String {
  let config_path = git_dir + "/config"
  let lfs_url = match read_config_value(rfs, config_path, "lfs", "url") {
    Some(v) => v
    None => ""
  }
  let remote_section = "remote \"\{remote_name}\""
  let remote_lfs_url = match
    read_config_value(rfs, config_path, remote_section, "lfsurl") {
    Some(v) => v
    None => ""
  }
  let remote_url = match get_remote_url(rfs, git_dir, remote_name) {
    Some(v) => v
    None => ""
  }
  resolve_lfs_url_from_config(lfs_url~, remote_lfs_url~, remote_url~)
}

///|
/// Verify that downloaded data matches the expected LFS OID (SHA-256).
pub fn lfs_verify_oid(data : Bytes, expected_oid : String) -> Bool {
  lfs_sha256_hex(data) == expected_oid
}

///|
/// Validate that a URL has an http:// or https:// scheme.
pub fn lfs_is_valid_http_url(url : String) -> Bool {
  url.has_prefix("https://") || url.has_prefix("http://")
}

///|
/// Read an LFS object from the local cache. Returns None if not cached.
pub fn lfs_get_cached_content(
  rfs : &@bit.RepoFileSystem,
  git_dir : String,
  oid : String,
) -> Bytes? {
  let path = lfs_object_path(git_dir, oid)
  if rfs.is_file(path) {
    try rfs.read_file(path) catch {
      _ => None
    } noraise {
      data => Some(data)
    }
  } else {
    None
  }
}