// examples/npz_write -- the N7 emit harness for the np.savez write direction.
// It reads 1..N .npy files given on the command line, decodes each with the
// Reader, packs them into an uncompressed NPZ archive with @npz.encode_npz,
// and writes the bytes to disk so an external NumPy process
// (interoperability/verify_npz_output.py) can np.load the result and compare
// every member against its source file -- the "MoonBit Generates NPZ" hard
// acceptance of N7. It is the write counterpart of examples/roundtrip: the
// same decode -> re-serialize -> emit-to-disk shape, one container up.
//
// Member keys: an argument of the form "=" uses the explicit key
// (split at the FIRST '='); a bare path derives the key from the file stem
// (tests/fixtures/f4_2x3_c_le_v1.npy -> "f4_2x3_c_le_v1"), mirroring how
// np.savez names keyword members. Members keep argument order.
//
// Usage:  moon run examples/npz_write --target native --  [=] [[=] ...]
// e.g.    moon run examples/npz_write --target native -- out.npz weights=a.npy
// Per AGENTS.md §6, @env.args()[0] is the exe path; user args start at [1].
//
// Diagnostics stay generic on purpose: the harness only proves emit-to-disk
// works, while the authoritative verdict (np.load readability, per-member
// dtype/shape/array_equal, CRC + stored-method facts) comes from the NumPy
// Oracle. Fatal paths use abort (validated in tests/), which exits non-zero
// so the driver can detect and report the failing input.

///|
// The archive key derived from a bare path: the last path component with its
// ".npy" suffix removed (tests/fixtures/foo.npy -> "foo"). Byte-level on
// purpose; every sliced range lands on a UTF-8 boundary because '/' and '\'
// are ASCII.
fn stem_of(bytes : Bytes) -> String {
  let mut start = 0
  for i = 0; i < bytes.length(); i = i + 1 {
    if bytes[i] == b'/' || bytes[i] == b'\\' {
      start = i + 1
    }
  }
  let mut end = bytes.length()
  if end - start >= 4 {
    if bytes[end - 4] == b'.' &&
      bytes[end - 3] == b'n' &&
      bytes[end - 2] == b'p' &&
      bytes[end - 1] == b'y' {
      end = end - 4
    }
  }
  @utf8.decode(bytes.view(start~, end~)) catch {
    _ => abort("file name is not UTF-8")
  }
}

///|
// Split a CLI argument into (key, path). An argument containing '=' splits at
// the FIRST '='; otherwise the key is the file stem of the path. Slicing at
// the ASCII '=' offset keeps non-ASCII keys intact.
fn key_and_path(arg : String) -> (String, String) {
  let bytes = @utf8.encode(arg)
  let mut eq = -1
  for i = 0; i < bytes.length(); i = i + 1 {
    if eq < 0 && bytes[i] == b'=' {
      eq = i
    }
  }
  if eq < 0 {
    (stem_of(bytes), arg)
  } else if eq == 0 {
    abort("empty key in argument: \{arg}")
  } else {
    let key = @utf8.decode(bytes.view(end=eq)) catch {
      _ => abort("key is not UTF-8 in argument: \{arg}")
    }
    let path = @utf8.decode(bytes.view(start=eq + 1)) catch {
      _ => abort("path is not UTF-8 in argument: \{arg}")
    }
    (key, path)
  }
}

///|
fn main {
  let args = @env.args()
  if args.length() < 3 {
    abort(
      "usage: npz_write  [=] [[=] ...]",
    )
  }
  let out_path = args[1]
  let members : Array[@npz.NpzMember] = []
  for i = 2; i < args.length(); i = i + 1 {
    let (key, in_path) = key_and_path(args[i])
    // 1. Read the source .npy. @fs raises IOError; bridge to Result
    //    (AGENTS.md §2) and abort with the path on failure.
    let read_res = try @fs.read_file_to_bytes(in_path) |> Ok catch {
      e => Err(e)
    }
    let input : Bytes = match read_res {
      Ok(b) => b
      Err(_) => abort("cannot read \{in_path}")
    }
    // 2. Decode -> validated NpyArray (Reader, §10.1). One bad member aborts
    //    the whole archive: encode_npz operates on decoded arrays only.
    let array = match @reader.decode(input) {
      Ok(a) => a
      Err(_) => abort("decode failed for \{in_path}")
    }
    members.push(@npz.NpzMember::{ name: key, array, })
  }
  // 3. Encode -> stored, np.load-readable NPZ bytes (the N7 "clean" profile);
  //    the reader-mirrored refusals (unsafe/duplicate key, zip64 surface)
  //    surface as Err here.
  let output = match @npz.encode_npz(members) {
    Ok(b) => b
    Err(_) =>
      abort("encode_npz refused the member list (unsafe or duplicate key?)")
  }
  // 4. Write to disk so NumPy can np.load it.
  let write_res = try @fs.write_bytes_to_file(out_path, output) |> Ok catch {
    e => Err(e)
  }
  match write_res {
    Ok(_) => ()
    Err(_) => abort("cannot write \{out_path}")
  }
  // 5. One-line summary; the NumPy Oracle does the authoritative checking.
  println(
    "npz_write OK: \{out_path} | \{members.length()} members | \{output.length()} bytes",
  )
}