// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// A recipient type that lives outside the library, to show what adding one takes:
/// a name for the stanza, a way to wrap the file key, and a way to unwrap it.
///
/// This one is a shared secret — the kind of thing a plugin for a team's own key
/// service would be. A new type is a new package: nothing in this library has to
/// know it exists, because the header carries stanzas it does not understand and
/// an identity only answers for the types it implements.
///
///     moon run src/examples/recipient

///|
let kind : String = "example.com/shared-secret"

///|
let context : Bytes = b"example.com/shared-secret/v1"

///|
let sha256 : () -> &@cryptspec.Hash = fn() -> &@cryptspec.Hash {
  @sha2.Hasher::new()
}

///|
/// The key the file key is wrapped under: the shared secret, bound to the salt
/// that identifies this stanza.
fn wrap_key(secret : BytesView, salt : BytesView) -> Bytes {
  @hkdf.key(secret, sha256, salt~, info=context, len=32)
}

///|
struct Recipient {
  secret : Bytes
}

///|
impl @spec.Recipient for Recipient with fn wrap(self, file_key~, random~) {
  let salt = @spec.draw(random, 16)
  let key = wrap_key(self.secret, salt)
  let aead = @chachapoly.ChaChaPoly::new(key) catch { _ => abort("no") }
  @spec.Stanza::new(
    kind~,
    args=[hex_of(salt)],
    body=aead.seal(nonce=Bytes::make(12, 0), plain=file_key, aad=b""),
  )
}

///|
struct Identity {
  secret : Bytes
}

///|
impl @spec.Identity for Identity with fn unwrap(self, stanza~) {
  if stanza.kind != kind {
    return None
  }
  let salt = hex_decode(stanza.args[0])
  let key = wrap_key(self.secret, salt)
  let aead = @chachapoly.ChaChaPoly::new(key) catch { _ => abort("no") }
  return Some(
    aead.open(nonce=Bytes::make(12, 0), cipher=stanza.body[:], aad=b"") catch {
      _ => return None
    },
  )
}

///|
/// The salt as text, since an argument is printable ASCII and nothing else.
fn hex_of(bytes : BytesView) -> String {
  let out = StringBuilder(size_hint=bytes.length() * 2)
  for byte in bytes {
    let value = byte.to_int()
    out.write_char(digit(value >> 4))
    out.write_char(digit(value & 0x0F))
  }
  out.to_string()
}

///|
fn digit(value : Int32) -> Char {
  (if value < 10 { 48 + value } else { 87 + value }).unsafe_to_char()
}

///|
fn hex_decode(text : StringView) -> Bytes {
  let units = text.code_units()
  let out = Array(capacity=units.length() / 2)
  let mut i = 0
  while i + 1 < units.length() {
    out.push(((nibble(units[i]) << 4) | nibble(units[i + 1])).to_byte())
    i = i + 2
  }
  Bytes::from_array(out)
}

///|
fn nibble(unit : UInt16) -> Int32 {
  if unit >= 48 && unit <= 57 {
    (unit - 48).to_int()
  } else if unit >= 97 && unit <= 102 {
    (unit - 87).to_int()
  } else {
    0
  }
}

///|
#coverage.skip
fn main {
  run() catch {
    error => println("refused: \{error}")
  }
}

///|
#coverage.skip
fn run() -> Unit raise @spec.Refused {
  let secret = b"the shared secret the team keeps"[:]
  let file = @moonseal.seal(
    [Recipient::{ secret: secret.to_owned(), }],
    b"the quick brown fox\n",
    random=@system.source,
  )
  println("sealed: \{file.length()} bytes")
  println(
    "opened: " +
    @utf8.decode_lossy(
      @moonseal.open([Identity::{ secret: secret.to_owned(), }], file),
    ),
  )
  // A different secret is a different file: the stanza opens to nothing.
  match
    (Some(
      @moonseal.open(
        [Identity::{ secret: b"another secret"[:].to_owned(), }],
        file,
      ),
    ) catch {
      error => {
        println("the wrong secret gets: \{error}")
        None
      }
    }) {
    Some(_) => println("the wrong secret opened it, which cannot happen")
    None => ()
  }
}