(** Ed25519 signature verification — pure OCaml, WASM-safe.

    RFC 8032 §5.1.7 cofactorless verify over edwards25519. Includes a
    minimal arbitrary-precision unsigned bignum (no Zarith / no deps)
    and twisted-Edwards extended-coordinate point arithmetic. Verify
    is total: malformed inputs return [false], never raise. SHA-512
    is reused from {!Sx_sha2}. Reference: RFC 8032, RFC 7748. *)

(* ---- Minimal bignum: int array, little-endian, base 2^26. ---- *)

let bits = 26
let base = 1 lsl bits
let mask = base - 1

type bn = int array  (* normalized: no high zero limbs, length >= 1 *)

let norm (a : bn) : bn =
  let n = ref (Array.length a) in
  while !n > 1 && a.(!n - 1) = 0 do decr n done;
  if !n = Array.length a then a else Array.sub a 0 !n

let bzero : bn = [| 0 |]
let of_int n : bn =
  if n = 0 then bzero
  else begin
    let r = ref [] and n = ref n in
    while !n > 0 do r := (!n land mask) :: !r; n := !n lsr bits done;
    norm (Array.of_list (List.rev !r))
  end

let is_zero (a : bn) = Array.length a = 1 && a.(0) = 0

let cmp (a : bn) (b : bn) : int =
  let a = norm a and b = norm b in
  let la = Array.length a and lb = Array.length b in
  if la <> lb then compare la lb
  else begin
    let r = ref 0 and i = ref (la - 1) in
    while !r = 0 && !i >= 0 do
      if a.(!i) <> b.(!i) then r := compare a.(!i) b.(!i);
      decr i
    done; !r
  end

let add (a : bn) (b : bn) : bn =
  let la = Array.length a and lb = Array.length b in
  let n = (max la lb) + 1 in
  let r = Array.make n 0 in
  let carry = ref 0 in
  for i = 0 to n - 1 do
    let s = !carry
            + (if i < la then a.(i) else 0)
            + (if i < lb then b.(i) else 0) in
    r.(i) <- s land mask; carry := s lsr bits
  done;
  norm r

(* a - b, requires a >= b *)
let sub (a : bn) (b : bn) : bn =
  let la = Array.length a and lb = Array.length b in
  let r = Array.make la 0 in
  let borrow = ref 0 in
  for i = 0 to la - 1 do
    let s = a.(i) - !borrow - (if i < lb then b.(i) else 0) in
    if s < 0 then (r.(i) <- s + base; borrow := 1)
    else (r.(i) <- s; borrow := 0)
  done;
  norm r

let mul (a : bn) (b : bn) : bn =
  let la = Array.length a and lb = Array.length b in
  let r = Array.make (la + lb) 0 in
  for i = 0 to la - 1 do
    let carry = ref 0 in
    for j = 0 to lb - 1 do
      let s = r.(i + j) + a.(i) * b.(j) + !carry in
      r.(i + j) <- s land mask; carry := s lsr bits
    done;
    r.(i + lb) <- r.(i + lb) + !carry
  done;
  norm r

let numbits (a : bn) : int =
  let a = norm a in
  let hi = Array.length a - 1 in
  if hi = 0 && a.(0) = 0 then 0
  else begin
    let b = ref 0 and v = ref a.(hi) in
    while !v > 0 do incr b; v := !v lsr 1 done;
    hi * bits + !b
  end

let bit (a : bn) (i : int) : int =
  let limb = i / bits and off = i mod bits in
  if limb >= Array.length a then 0 else (a.(limb) lsr off) land 1

(* r = a mod m  (m > 0), binary long division. *)
let bn_mod (a : bn) (m : bn) : bn =
  if cmp a m < 0 then norm a
  else begin
    let r = ref bzero in
    for i = numbits a - 1 downto 0 do
      (* r = r*2 + bit *)
      r := add !r !r;
      if bit a i = 1 then r := add !r [| 1 |];
      if cmp !r m >= 0 then r := sub !r m
    done;
    !r
  end

let div_small (a : bn) (d : int) : bn =
  let la = Array.length a in
  let q = Array.make la 0 in
  let rem = ref 0 in
  for i = la - 1 downto 0 do
    let cur = (!rem lsl bits) lor a.(i) in
    q.(i) <- cur / d; rem := cur mod d
  done;
  norm q

let powmod (b0 : bn) (e : bn) (m : bn) : bn =
  let result = ref [| 1 |] and b = ref (bn_mod b0 m) in
  let nb = numbits e in
  for i = 0 to nb - 1 do
    if bit e i = 1 then result := bn_mod (mul !result !b) m;
    b := bn_mod (mul !b !b) m
  done;
  !result

let of_bytes_le (s : string) : bn =
  let acc = ref bzero in
  for i = String.length s - 1 downto 0 do
    acc := add (mul !acc (of_int 256)) (of_int (Char.code s.[i]))
  done;
  !acc

let to_bytes_le (a : bn) (n : int) : string =
  let b = Bytes.make n '\000' in
  let cur = ref (norm a) in
  for i = 0 to n - 1 do
    let q = div_small !cur 256 in
    let r =
      let qm = mul q (of_int 256) in
      let d = sub !cur qm in
      if is_zero d then 0 else d.(0)
    in
    Bytes.set b i (Char.chr r);
    cur := q
  done;
  Bytes.unsafe_to_string b

(* ---- Field GF(p), p = 2^255 - 19 ---- *)

let p =
  let twop255 = Array.make 11 0 in   (* 11*26 = 286 > 255 *)
  let limb = 255 / bits and off = 255 mod bits in
  twop255.(limb) <- 1 lsl off;
  sub (norm twop255) (of_int 19)

let fmod a = bn_mod a p
let fadd a b = fmod (add a b)
let fsub a b = fmod (add a (sub p (fmod b)))
let fmul a b = fmod (mul a b)
let fpow a e = powmod a e p
let finv a = fpow a (sub p (of_int 2))   (* Fermat: a^(p-2) *)

(* group order L = 2^252 + 27742317777372353535851937790883648493 *)
let ell =
  of_bytes_le
    "\xed\xd3\xf5\x5c\x1a\x63\x12\x58\xd6\x9c\xf7\xa2\xde\xf9\xde\x14\
     \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10"

(* d = -121665 / 121666 mod p *)
let dconst =
  let inv666 = finv (of_int 121666) in
  fmod (mul (fsub (of_int 0) (of_int 121665)) inv666)

(* sqrt(-1) = 2^((p-1)/4) mod p *)
let sqrtm1 = fpow (of_int 2) (div_small (sub p (of_int 1)) 4)

(* ---- edwards25519 points in extended coords (X,Y,Z,T) ---- *)

type pt = { x : bn; y : bn; z : bn; t : bn }

let identity = { x = bzero; y = of_int 1; z = of_int 1; t = bzero }

(* add-2008-hwcd-3, complete for a = -1 on ed25519 *)
let padd (p1 : pt) (p2 : pt) : pt =
  let a = fmul (fsub p1.y p1.x) (fsub p2.y p2.x) in
  let b = fmul (fadd p1.y p1.x) (fadd p2.y p2.x) in
  let c = fmul (fmul p1.t (fmul (of_int 2) dconst)) p2.t in
  let dd = fmul (fmul p1.z (of_int 2)) p2.z in
  let e = fsub b a in
  let f = fsub dd c in
  let g = fadd dd c in
  let h = fadd b a in
  { x = fmul e f; y = fmul g h; t = fmul e h; z = fmul f g }

let scalar_mul (n : bn) (q : pt) : pt =
  let r = ref identity in
  for i = numbits n - 1 downto 0 do
    r := padd !r !r;
    if bit n i = 1 then r := padd !r q
  done;
  !r

let pnegate (q : pt) : pt =
  { q with x = fsub (of_int 0) q.x; t = fsub (of_int 0) q.t }

(* Decompress a 32-byte little-endian point encoding. *)
let decompress (s : string) : pt option =
  if String.length s <> 32 then None
  else begin
    let sign = (Char.code s.[31] lsr 7) land 1 in
    let s' = Bytes.of_string s in
    Bytes.set s' 31 (Char.chr (Char.code s.[31] land 0x7f));
    let y = of_bytes_le (Bytes.unsafe_to_string s') in
    if cmp y p >= 0 then None
    else begin
      let y2 = fmul y y in
      let u = fsub y2 (of_int 1) in
      let v = fadd (fmul dconst y2) (of_int 1) in
      (* x = u v^3 (u v^7)^((p-5)/8) *)
      let v3 = fmul (fmul v v) v in
      let v7 = fmul (fmul v3 v3) v in
      let exp = div_small (sub p (of_int 5)) 8 in
      let x0 = fmul (fmul u v3) (fpow (fmul u v7) exp) in
      let vx2 = fmul v (fmul x0 x0) in
      let x =
        if cmp vx2 u = 0 then Some x0
        else if cmp vx2 (fsub (of_int 0) u) = 0 then Some (fmul x0 sqrtm1)
        else None
      in
      match x with
      | None -> None
      | Some x ->
        if is_zero x && sign = 1 then None
        else begin
          let x = if (bit x 0) <> sign then fsub (of_int 0) x else x in
          Some { x; y; z = of_int 1; t = fmul x y }
        end
    end
  end

(* Encode a point to 32-byte little-endian (y with x-parity bit). *)
let encode (q : pt) : string =
  let zi = finv q.z in
  let x = fmul q.x zi and y = fmul q.y zi in
  let b = Bytes.of_string (to_bytes_le y 32) in
  let last = Char.code (Bytes.get b 31) lor ((bit x 0) lsl 7) in
  Bytes.set b 31 (Char.chr last);
  Bytes.unsafe_to_string b

(* base point: y = 4/5 mod p, x even (sign 0). *)
let base_point =
  let by = fmul (of_int 4) (finv (of_int 5)) in
  match decompress (to_bytes_le by 32) with
  | Some pt -> pt
  | None -> failwith "ed25519: base point decompress failed"

let unhex (h : string) : string =
  let n = String.length h / 2 in
  let b = Bytes.create n in
  for i = 0 to n - 1 do
    Bytes.set b i
      (Char.chr (int_of_string ("0x" ^ String.sub h (2 * i) 2)))
  done;
  Bytes.unsafe_to_string b

let sha512_bytes s = unhex (Sx_sha2.sha512_hex s)

(* RFC 8032 §5.1.7 cofactorless: encode([S]B - [k]A) == R. *)
let verify ~pubkey ~msg ~sig_ : bool =
  if String.length pubkey <> 32 || String.length sig_ <> 64 then false
  else
    let rb = String.sub sig_ 0 32 in
    let sb = String.sub sig_ 32 32 in
    let s = of_bytes_le sb in
    if cmp s ell >= 0 then false
    else
      match decompress pubkey with
      | None -> false
      | Some a ->
        let h = sha512_bytes (rb ^ pubkey ^ msg) in
        let k = bn_mod (of_bytes_le h) ell in
        let sb_pt = scalar_mul s base_point in
        let ka = scalar_mul k a in
        let chk = padd sb_pt (pnegate ka) in
        (try encode chk = rb with _ -> false)
