///|
priv struct AssetEntry {
  virtual_path : String
  offset : Int
  size : Int
}

///|
priv struct CliConfig {
  assets_dir : String
  pack_out : String?
  index_out : String?
  path_prefix : String
  blob_name : String
  lookup_fn : String
}

///|
fn usage_text() -> String {
  (
    #| Usage:
    #|   selene-embed-assets --assets-dir  [--pack-out ] [--index-out ]
    #|                       [--path-prefix ] [--blob-name ] [--lookup-fn ]
    #|
    #| Options:
    #|   --assets-dir   Source asset directory to scan recursively (required)
    #|   --pack-out     Output pack file path (optional)
    #|   --index-out    Output MoonBit index file path (optional)
    #|   --path-prefix  Runtime asset prefix, default: assets
    #|   --blob-name    Embedded blob symbol name, default: embedded_assets_blob
    #|   --lookup-fn    Generated lookup function name, default: get_embedded_asset
    #|   -h, --help     Show this help
  )
}

///|
fn join_path(base : String, child : String) -> String {
  if base.is_empty() {
    child
  } else if base.has_suffix("/") || base.has_suffix("\\") {
    base + child
  } else {
    base + "/" + child
  }
}

///|
fn normalize_slashes(path : String) -> String {
  path.replace_all(old="\\", new="/")
}

///|
fn normalize_prefix(prefix : String) -> String {
  let mut normalized = normalize_slashes(prefix).trim().to_string()
  while normalized.length() > 0 && normalized.has_prefix("/") {
    normalized = normalized.unsafe_substring(start=1, end=normalized.length())
  }
  while normalized.length() > 0 && normalized.has_suffix("/") {
    normalized = normalized.unsafe_substring(
      start=0,
      end=normalized.length() - 1,
    )
  }
  normalized
}

///|
fn with_prefix(prefix : String, rel_path : String) -> String {
  if prefix.is_empty() {
    rel_path
  } else {
    prefix + "/" + rel_path
  }
}

///|
fn escape_moon_string(text : String) -> String {
  text.replace_all(old="\\", new="\\\\").replace_all(old="\"", new="\\\"")
}

///|
fn read_dir_safe(path : String) -> Result[Array[String], String] {
  try @fs.read_dir(path) catch {
    err => Err("Failed to read directory `\{path}`: \{err.to_string()}")
  } noraise {
    entries => Ok(entries)
  }
}

///|
fn is_dir_safe(path : String) -> Result[Bool, String] {
  try @fs.is_dir(path) catch {
    err => Err("Failed to inspect path `\{path}`: \{err.to_string()}")
  } noraise {
    v => Ok(v)
  }
}

///|
fn is_file_safe(path : String) -> Result[Bool, String] {
  try @fs.is_file(path) catch {
    err => Err("Failed to inspect path `\{path}`: \{err.to_string()}")
  } noraise {
    v => Ok(v)
  }
}

///|
fn read_file_to_bytes_safe(path : String) -> Result[Bytes, String] {
  try @fs.read_file_to_bytes(path) catch {
    err => Err("Failed to read file `\{path}`: \{err.to_string()}")
  } noraise {
    bytes => Ok(bytes)
  }
}

///|
fn write_bytes_safe(path : String, bytes : Bytes) -> Result[Unit, String] {
  try @fs.write_bytes_to_file(path, bytes) catch {
    err => Err("Failed to write pack file `\{path}`: \{err.to_string()}")
  } noraise {
    _ => Ok(())
  }
}

///|
fn write_string_safe(path : String, content : String) -> Result[Unit, String] {
  try @fs.write_string_to_file(path, content) catch {
    err => Err("Failed to write index file `\{path}`: \{err.to_string()}")
  } noraise {
    _ => Ok(())
  }
}

///|
fn walk_assets(
  root : String,
  rel_dir : String,
  out : Array[String],
) -> Result[Unit, String] {
  let full_dir = if rel_dir.is_empty() {
    root
  } else {
    join_path(root, rel_dir)
  }
  let entries = match read_dir_safe(full_dir) {
    Ok(entries) => entries
    Err(msg) => return Err(msg)
  }
  entries.sort()

  for name in entries {
    let rel_path = if rel_dir.is_empty() { name } else { rel_dir + "/" + name }
    let full_path = join_path(root, rel_path)
    let is_dir = match is_dir_safe(full_path) {
      Ok(v) => v
      Err(msg) => return Err(msg)
    }
    if is_dir {
      match walk_assets(root, rel_path, out) {
        Ok(_) => ()
        Err(msg) => return Err(msg)
      }
    } else {
      let is_file = match is_file_safe(full_path) {
        Ok(v) => v
        Err(msg) => return Err(msg)
      }
      if is_file {
        out.push(normalize_slashes(rel_path))
      }
    }
  }
  Ok(())
}

///|
fn collect_relative_files(assets_dir : String) -> Result[Array[String], String] {
  if !@fs.path_exists(assets_dir) {
    return Err("Assets directory not found: \{assets_dir}")
  }
  let is_dir = match is_dir_safe(assets_dir) {
    Ok(v) => v
    Err(msg) => return Err(msg)
  }
  if !is_dir {
    return Err("Assets path is not a directory: \{assets_dir}")
  }

  let files : Array[String] = []
  match walk_assets(assets_dir, "", files) {
    Ok(_) => {
      files.sort()
      Ok(files)
    }
    Err(msg) => Err(msg)
  }
}

///|
fn build_entries(
  cfg : CliConfig,
  relative_files : Array[String],
) -> Result[(Array[AssetEntry], Bytes), String] {
  let should_pack = cfg.pack_out is Some(_)
  let entries : Array[AssetEntry] = []
  let packed : Array[Byte] = []
  let mut offset = 0

  for rel_path in relative_files {
    let full_path = join_path(cfg.assets_dir, rel_path)
    let bytes = match read_file_to_bytes_safe(full_path) {
      Ok(bytes) => bytes
      Err(msg) => return Err(msg)
    }
    let size = bytes.length()
    entries.push({
      virtual_path: with_prefix(cfg.path_prefix, rel_path),
      offset,
      size,
    })
    if should_pack {
      packed.push_iter(bytes.iter())
    }
    offset += size
  }

  let packed_bytes = if should_pack {
    Bytes::from_array(packed)
  } else {
    Bytes::default()
  }
  Ok((entries, packed_bytes))
}

///|
fn build_index_source(cfg : CliConfig, entries : Array[AssetEntry]) -> String {
  let lines : Array[String] = []
  lines.push("// Generated by selene-embed-assets, DO NOT EDIT.")
  lines.push("")
  lines.push("///|")
  lines.push("fn slice_embedded_asset(start : Int, size : Int) -> Bytes {")
  lines.push(
    "  \{cfg.blob_name}.view(start=start, end=start + size).to_bytes()",
  )
  lines.push("}")
  lines.push("")
  lines.push("///|")
  lines.push("pub fn \{cfg.lookup_fn}(path : String) -> Bytes? {")
  lines.push("  match path {")
  for entry in entries {
    lines.push(
      "    \"\{escape_moon_string(entry.virtual_path)}\" => Some(slice_embedded_asset(\{entry.offset.to_string()}, \{entry.size.to_string()}))",
    )
  }
  lines.push("    _ => None")
  lines.push("  }")
  lines.push("}")
  lines.push("")
  lines.join("\n")
}

///|
fn parse_args(raw_args : Array[String]) -> Result[CliConfig?, String] {
  let mut assets_dir : String? = None
  let mut pack_out : String? = None
  let mut index_out : String? = None
  let mut path_prefix = "assets"
  let mut blob_name = "embedded_assets_blob"
  let mut lookup_fn = "get_embedded_asset"

  let mut i = 1
  while i < raw_args.length() {
    let arg = raw_args[i]
    if arg == "-h" || arg == "--help" {
      return Ok(None)
    }
    if i + 1 >= raw_args.length() {
      return Err("Missing value after \{arg}")
    }
    let value = raw_args[i + 1]
    match arg {
      "--assets-dir" => assets_dir = Some(value)
      "--pack-out" => pack_out = Some(value)
      "--index-out" => index_out = Some(value)
      "--path-prefix" => path_prefix = value
      "--blob-name" => blob_name = value
      "--lookup-fn" => lookup_fn = value
      _ => return Err("Unknown argument: \{arg}")
    }
    i += 2
  }

  let assets_dir = match assets_dir {
    Some(v) => v
    None => return Err("Missing required argument: --assets-dir")
  }
  if pack_out is None && index_out is None {
    return Err("At least one of --pack-out or --index-out must be provided")
  }
  Ok(
    Some({
      assets_dir,
      pack_out,
      index_out,
      path_prefix: normalize_prefix(path_prefix),
      blob_name,
      lookup_fn,
    }),
  )
}

///|
pub fn run_cli(raw_args : Array[String]) -> Int {
  let cfg = match parse_args(raw_args) {
    Ok(None) => {
      println(usage_text())
      return 0
    }
    Ok(Some(cfg)) => cfg
    Err(msg) => {
      println("Error: \{msg}")
      println("")
      println(usage_text())
      return 2
    }
  }

  let relative_files = match collect_relative_files(cfg.assets_dir) {
    Ok(files) => files
    Err(msg) => {
      println("Error: \{msg}")
      return 1
    }
  }

  let (entries, packed_bytes) = match build_entries(cfg, relative_files) {
    Ok(v) => v
    Err(msg) => {
      println("Error: \{msg}")
      return 1
    }
  }

  if cfg.pack_out is Some(pack_out) {
    match write_bytes_safe(pack_out, packed_bytes) {
      Ok(_) => ()
      Err(msg) => {
        println("Error: \{msg}")
        return 1
      }
    }
  }

  if cfg.index_out is Some(index_out) {
    let index_src = build_index_source(cfg, entries)
    match write_string_safe(index_out, index_src) {
      Ok(_) => ()
      Err(msg) => {
        println("Error: \{msg}")
        return 1
      }
    }
  }

  let outputs : Array[String] = []
  if cfg.pack_out is Some(pack_out) {
    outputs.push(pack_out)
  }
  if cfg.index_out is Some(index_out) {
    outputs.push(index_out)
  }
  let outputs_text = outputs.join(", ")
  println(
    "Bundled \{entries.length().to_string()} assets from \{cfg.assets_dir} -> \{outputs_text}",
  )
  0
}