import { sha256 } from '@noble/hashes/sha2.js';

/**
 * Canonical bytes for an `@shade/files` RPC request signature.
 *
 *   "rpc\0addr=<sender>\0at=<signedAt>\0kind=<kind>\0id=<id>\0argsHash=<hex>\0"
 *
 * Mirror of `canonicalControlBytes` in `@shade/sdk/src/streams-bridge.ts:188`.
 * The signature binds the (sender, op kind, request id, args, signedAt)
 * tuple — defense-in-depth on top of Double Ratchet authentication.
 */
export function canonicalRpcBytes(args: {
  address: string;
  signedAt: number;
  kind: string;
  id: string;
  argsHash: Uint8Array;
}): Uint8Array {
  const enc = new TextEncoder();
  const fields = [
    `rpc\0`,
    `addr=${args.address}\0`,
    `at=${args.signedAt}\0`,
    `kind=${args.kind}\0`,
    `id=${args.id}\0`,
    `argsHash=${bytesToHex(args.argsHash)}\0`,
  ];
  return enc.encode(fields.join(''));
}

/**
 * Stable canonical-JSON serialization for hashing args. Sorts object keys
 * recursively so two equivalent inputs hash to identical bytes regardless
 * of property order.
 */
export function canonicalJsonStringify(value: unknown): string {
  if (value === null || typeof value !== 'object') return JSON.stringify(value);
  if (Array.isArray(value)) {
    return `[${value.map((v) => canonicalJsonStringify(v)).join(',')}]`;
  }
  const obj = value as Record<string, unknown>;
  const keys = Object.keys(obj).sort();
  const parts: string[] = [];
  for (const key of keys) {
    if (obj[key] === undefined) continue;
    parts.push(`${JSON.stringify(key)}:${canonicalJsonStringify(obj[key])}`);
  }
  return `{${parts.join(',')}}`;
}

/** Hash args via canonical-JSON → SHA-256 (32 bytes). */
export function hashArgs(args: unknown): Uint8Array {
  const enc = new TextEncoder();
  return sha256(enc.encode(canonicalJsonStringify(args)));
}

export function bytesToHex(b: Uint8Array): string {
  return Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
}

export function bytesToBase64(b: Uint8Array): string {
  let bin = '';
  for (let i = 0; i < b.length; i++) bin += String.fromCharCode(b[i]!);
  return btoa(bin);
}

export function base64ToBytes(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;
}
