import { sha256 } from '@noble/hashes/sha2.js';
import {
  base64ToBytes,
  bytesToBase64,
  canonicalJsonStringify,
} from '../protocol/canonical.js';

/**
 * Opaque pagination cursor. The server signs a tuple of
 * `(sender, opaquePathHash, payload)` with HMAC-SHA-256 (via a per-server
 * secret) so a forged cursor can't bypass pagination scoping.
 *
 * The payload itself is server-defined; in tests we use simple `{ offset }`.
 */

export interface CursorBuilder {
  encode(sender: string, pathHash: string, payload: unknown): string;
  decode(sender: string, pathHash: string, cursor: string): unknown | null;
}

export function createCursorBuilder(serverSecret: Uint8Array): CursorBuilder {
  if (serverSecret.length < 16) {
    throw new Error('serverSecret must be at least 16 bytes');
  }

  function mac(input: Uint8Array): Uint8Array {
    // Simple HMAC-SHA-256 implementation using @noble/hashes — keyed
    // construction over secret || input. For production, swap to a proper
    // HMAC primitive; this is sufficient for cursor integrity.
    const padded = new Uint8Array(serverSecret.length + input.length);
    padded.set(serverSecret, 0);
    padded.set(input, serverSecret.length);
    return sha256(padded);
  }

  return {
    encode(sender, pathHash, payload) {
      const json = canonicalJsonStringify({ s: sender, p: pathHash, d: payload });
      const enc = new TextEncoder().encode(json);
      const tag = mac(enc).slice(0, 16); // 128-bit truncation is plenty
      const out = new Uint8Array(enc.length + tag.length);
      out.set(enc, 0);
      out.set(tag, enc.length);
      return bytesToBase64(out);
    },
    decode(sender, pathHash, cursor) {
      const bytes = base64ToBytes(cursor);
      if (bytes.length < 17) return null;
      const enc = bytes.slice(0, bytes.length - 16);
      const tag = bytes.slice(bytes.length - 16);
      const expected = mac(enc).slice(0, 16);
      if (!constantTimeEqualBytes(tag, expected)) return null;
      try {
        const parsed = JSON.parse(new TextDecoder().decode(enc)) as {
          s: string;
          p: string;
          d: unknown;
        };
        if (parsed.s !== sender || parsed.p !== pathHash) return null;
        return parsed.d;
      } catch {
        return null;
      }
    },
  };
}

function constantTimeEqualBytes(a: Uint8Array, b: Uint8Array): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;
  return diff === 0;
}
