/**
 * High-level quicproquo client.
 *
 * Combines the WASM crypto layer with a WebSocket transport to provide
 * a type-safe API for interacting with a quicproquo server.
 *
 * The crypto helpers work standalone (no server connection required).
 * Server RPC methods require a WebSocket bridge proxy since the native
 * server speaks Cap'n Proto over QUIC/TCP + Noise_XX.
 */

import type {
  ConnectOptions,
  Message,
  ChannelResult,
  Identity,
  HybridKeypair,
  TransportEvent,
} from "./types.js";
import type { Transport } from "./transport.js";
import { WebSocketTransport } from "./transport.js";
import * as crypto from "./crypto.js";

export class QpqClient {
  private transport: Transport | null;
  private token: Uint8Array | null = null;

  private constructor(transport: Transport | null) {
    this.transport = transport;
  }

  /**
   * Connect to a quicproquo server via WebSocket bridge.
   *
   * NOTE: The server must expose a WebSocket-to-capnp bridge endpoint.
   * See the project documentation for bridge proxy setup.
   */
  static async connect(opts: ConnectOptions): Promise<QpqClient> {
    await crypto.initCrypto();
    const transport = await WebSocketTransport.connect(opts);
    return new QpqClient(transport);
  }

  /**
   * Create a client for offline/crypto-only usage (no server connection).
   * All crypto helper methods will work; RPC methods will throw.
   */
  static async offline(): Promise<QpqClient> {
    await crypto.initCrypto();
    return new QpqClient(null);
  }

  /** Register a handler for transport-level events. */
  on(handler: (event: TransportEvent) => void): void {
    if (this.transport) {
      this.transport.on(handler);
    }
  }

  /** True when the transport is connected. */
  get connected(): boolean {
    return this.transport?.connected ?? false;
  }

  /** Set a session token for authenticated RPC calls. */
  setSessionToken(token: Uint8Array): void {
    this.token = token;
  }

  // -----------------------------------------------------------------------
  // RPC methods (require server connection via WebSocket bridge)
  // -----------------------------------------------------------------------

  /** Health check. */
  async health(): Promise<string> {
    const resp = await this.rpc("health", {});
    return resp.result as string;
  }

  /** Resolve a username to its public key. */
  async resolveUser(username: string): Promise<Uint8Array> {
    const resp = await this.rpc("resolveUser", { username });
    return base64ToBytes(resp.result as string);
  }

  /** Create or join a channel with a peer. */
  async createChannel(peerKey: Uint8Array): Promise<ChannelResult> {
    const resp = await this.rpc("createChannel", {
      peerKey: bytesToBase64(peerKey),
    });
    const r = resp.result as { channelId: string; wasNew: boolean };
    return {
      channelId: base64ToBytes(r.channelId),
      wasNew: r.wasNew,
    };
  }

  /** Send a payload to a recipient. Returns the message sequence number. */
  async send(recipientKey: Uint8Array, payload: Uint8Array): Promise<number> {
    const resp = await this.rpc("send", {
      recipientKey: bytesToBase64(recipientKey),
      payload: bytesToBase64(payload),
    });
    return resp.result as number;
  }

  /** Send a payload with a time-to-live in seconds. */
  async sendWithTTL(
    recipientKey: Uint8Array,
    payload: Uint8Array,
    ttlSecs: number,
  ): Promise<number> {
    const resp = await this.rpc("send", {
      recipientKey: bytesToBase64(recipientKey),
      payload: bytesToBase64(payload),
      ttlSecs,
    });
    return resp.result as number;
  }

  /** Receive pending messages for a recipient key. */
  async receive(recipientKey: Uint8Array): Promise<Message[]> {
    const resp = await this.rpc("receive", {
      recipientKey: bytesToBase64(recipientKey),
    });
    const raw = resp.result as Array<{ seq: number; data: string }>;
    return raw.map((m) => ({
      seq: m.seq,
      data: base64ToBytes(m.data),
    }));
  }

  /** Delete the authenticated account. */
  async deleteAccount(): Promise<void> {
    await this.rpc("deleteAccount", {});
  }

  // -----------------------------------------------------------------------
  // Crypto helpers (work offline, delegate to WASM)
  // -----------------------------------------------------------------------

  /** Generate a fresh Ed25519 identity. */
  generateIdentity(): Identity {
    return crypto.generateIdentity();
  }

  /** Derive public key from an Ed25519 seed. */
  identityPublicKey(seed: Uint8Array): Uint8Array {
    return crypto.identityPublicKey(seed);
  }

  /** Sign a message with an Ed25519 seed. */
  sign(seed: Uint8Array, message: Uint8Array): Uint8Array {
    return crypto.sign(seed, message);
  }

  /** Verify an Ed25519 signature. */
  verify(
    publicKey: Uint8Array,
    message: Uint8Array,
    signature: Uint8Array,
  ): boolean {
    return crypto.verify(publicKey, message, signature);
  }

  /** Generate a hybrid (X25519 + ML-KEM-768) keypair. */
  hybridGenerateKeypair(): HybridKeypair {
    return crypto.hybridGenerateKeypair();
  }

  /** Encrypt to a hybrid public key. */
  hybridEncrypt(publicKey: Uint8Array, plaintext: Uint8Array): Uint8Array {
    return crypto.hybridEncrypt(publicKey, plaintext);
  }

  /** Decrypt a hybrid envelope. */
  hybridDecrypt(keypairBlob: Uint8Array, envelope: Uint8Array): Uint8Array {
    return crypto.hybridDecrypt(keypairBlob, envelope);
  }

  /** Compute a 60-digit safety number from two public keys. */
  computeSafetyNumber(keyA: Uint8Array, keyB: Uint8Array): string {
    return crypto.computeSafetyNumber(keyA, keyB);
  }

  /** Seal a payload in a sealed sender envelope. */
  seal(seed: Uint8Array, payload: Uint8Array): Uint8Array {
    return crypto.seal(seed, payload);
  }

  /** Unseal a sealed sender envelope. */
  unseal(envelope: Uint8Array): Uint8Array {
    return crypto.unseal(envelope);
  }

  /** Pad a message to a fixed bucket size. */
  padMessage(data: Uint8Array): Uint8Array {
    return crypto.padMessage(data);
  }

  /** Remove padding from a message. */
  unpadMessage(data: Uint8Array): Uint8Array {
    return crypto.unpadMessage(data);
  }

  /** Gracefully close the connection. */
  close(): void {
    if (this.transport) {
      this.transport.close();
      this.transport = null;
    }
  }

  // -----------------------------------------------------------------------
  // Internal
  // -----------------------------------------------------------------------

  private async rpc(
    method: string,
    params: Record<string, unknown>,
  ): Promise<{ ok: true; result: unknown }> {
    if (!this.transport) {
      throw new Error(
        `Cannot call RPC method "${method}": no server connection. ` +
          "Use QpqClient.connect() instead of QpqClient.offline().",
      );
    }
    if (this.token) {
      params.token = bytesToBase64(this.token);
    }
    const resp = await this.transport.call(method, params);
    if (!resp.ok) {
      throw new Error(`RPC error in ${method}: ${resp.error ?? "unknown"}`);
    }
    return resp as { ok: true; result: unknown };
  }
}

// ---------------------------------------------------------------------------
// Base64 helpers (browser-compatible, no Node.js Buffer dependency)
// ---------------------------------------------------------------------------

function bytesToBase64(bytes: Uint8Array): string {
  let binary = "";
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

function base64ToBytes(b64: string): Uint8Array {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}
