// Crypto wrapper around tweetnacl.
//
// The byte-for-byte references for everything here are:
//   /Users/sn0/Desktop/rustdesk/src/common.rs:2005-2031   (decode_id_pk, create_symmetric_key_msg)
//   /Users/sn0/Desktop/rustdesk-server/libs/hbb_common/src/tcp.rs:296-344  (Encrypt + nonce derivation)
//
// tweetnacl exposes the same NaCl primitives the desktop client's
// `sodiumoxide` uses:
//   - Ed25519: nacl.sign.open  (verifies + unwraps a signed message)
//   - Curve25519 box: nacl.box  (asymmetric one-shot — we use the all-zero
//     24-byte nonce per the desktop client's create_symmetric_key_msg)
//   - Secretbox: nacl.secretbox / nacl.secretbox.open  (XSalsa20-Poly1305,
//     symmetric per-message — nonce derivation is per-direction 8-byte LE
//     sequence counter || 16 zero bytes)
//
// Pure JS, ~50 KB minified, no WASM, no module-resolution drama.

import nacl from "tweetnacl";

// Resolved immediately — kept as a Promise so the call site doesn't change
// when we swap implementations later.
export const sodiumReady: Promise<void> = Promise.resolve();

export const SIGN_PUBLICKEYBYTES = nacl.sign.publicKeyLength;     // 32
export const BOX_PUBLICKEYBYTES = nacl.box.publicKeyLength;       // 32
export const BOX_SECRETKEYBYTES = nacl.box.secretKeyLength;       // 32
export const BOX_NONCEBYTES = nacl.box.nonceLength;               // 24
export const SECRETBOX_KEYBYTES = nacl.secretbox.keyLength;       // 32
export const SECRETBOX_NONCEBYTES = nacl.secretbox.nonceLength;   // 24

/** Verify and unwrap an Ed25519-signed message. Throws on auth failure. */
export function signOpen(signed: Uint8Array, publicKey: Uint8Array): Uint8Array {
  if (publicKey.length !== SIGN_PUBLICKEYBYTES) {
    throw new Error(`signOpen: bad pk length ${publicKey.length}`);
  }
  const out = nacl.sign.open(signed, publicKey);
  if (!out) throw new Error("signOpen: signature verification failed");
  return out;
}

/** Generate an ephemeral Curve25519 keypair for the box handshake. */
export function genBoxKeypair(): { publicKey: Uint8Array; secretKey: Uint8Array } {
  const kp = nacl.box.keyPair();
  return { publicKey: kp.publicKey, secretKey: kp.secretKey };
}

/** Generate a fresh symmetric key for the per-session secretbox stream. */
export function genSecretboxKey(): Uint8Array {
  return nacl.randomBytes(SECRETBOX_KEYBYTES);
}

/**
 * Seal `msg` under the peer's Curve25519 public key with our secret key.
 * All-zero 24-byte nonce — matches `sodiumoxide::crypto::box_::seal` in the
 * desktop client's create_symmetric_key_msg.
 */
export function boxSeal(
  msg: Uint8Array,
  peerPublicKey: Uint8Array,
  ourSecretKey: Uint8Array,
): Uint8Array {
  const nonce = new Uint8Array(BOX_NONCEBYTES);   // all zeros
  return nacl.box(msg, nonce, peerPublicKey, ourSecretKey);
}

/**
 * Encrypt with secretbox using the per-direction nonce derivation:
 *   nonce[0..8]  = sequence_counter as little-endian u64
 *   nonce[8..24] = zeros
 * Returns ciphertext (which includes the 16-byte Poly1305 tag).
 */
export function secretboxSeal(
  msg: Uint8Array,
  sequence: bigint,
  key: Uint8Array,
): Uint8Array {
  return nacl.secretbox(msg, makeNonce(sequence), key);
}

/** Inverse of secretboxSeal — throws on auth failure. */
export function secretboxOpen(
  cipher: Uint8Array,
  sequence: bigint,
  key: Uint8Array,
): Uint8Array {
  const out = nacl.secretbox.open(cipher, makeNonce(sequence), key);
  if (!out) throw new Error("secretboxOpen: authentication failed");
  return out;
}

/** Build a 24-byte nonce: little-endian u64 in [0..8], zeros in [8..24]. */
function makeNonce(sequence: bigint): Uint8Array {
  const n = new Uint8Array(SECRETBOX_NONCEBYTES);
  const view = new DataView(n.buffer);
  view.setBigUint64(0, sequence, /*littleEndian=*/ true);
  return n;
}

// Pure-JS SHA-256 from @noble/hashes — works in non-secure contexts
// (browsers gate `crypto.subtle` to HTTPS / localhost only). Audited,
// minimal, no WASM. The dashboard often runs on plain http for self-hosted
// deployments, which would break SubtleCrypto.
import { sha256 as nobleSha256 } from "@noble/hashes/sha2.js";

/** SHA-256 of `data`. Used for the Hash/LoginRequest password challenge. */
export function sha256(data: Uint8Array): Uint8Array {
  return nobleSha256(data);
}

/** Concatenate two byte arrays. */
export function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
  const out = new Uint8Array(a.length + b.length);
  out.set(a, 0);
  out.set(b, a.length);
  return out;
}

/** Standard base64 (with padding) → bytes. Used to decode the server pk. */
export function base64Decode(s: string): Uint8Array {
  const bin = atob(s);
  const out = new Uint8Array(bin.length);
  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
  return out;
}

/** Bytes → standard base64 (with padding). */
export function base64Encode(bytes: Uint8Array): string {
  let s = "";
  for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
  return btoa(s);
}
