export type RealtimeMessageType = string;

export interface RealtimeMessage<T = unknown> {
  id: string;
  type: RealtimeMessageType;
  room?: string;
  senderId?: string;
  senderName?: string;
  sentAt: string;
  data: T;
  meta?: Record<string, string | number | boolean | null>;
}

export interface CreateRealtimeMessageOptions<T> {
  id?: string;
  type: string;
  room?: string;
  senderId?: string;
  senderName?: string;
  sentAt?: string | Date;
  data: T;
  meta?: Record<string, string | number | boolean | null>;
}

export interface ParseRealtimeMessageOptions {
  maxBytes?: number;
  maxDepth?: number;
  allowedTypes?: readonly string[];
  room?: string;
}

export interface RealtimePresence {
  userId: string;
  name?: string;
  avatar?: string;
  status?: "online" | "away" | "busy" | "offline";
  joinedAt?: string;
  lastSeenAt?: string;
  meta?: Record<string, string | number | boolean | null>;
}

export interface RealtimeRoomMeta {
  id: string;
  name: string;
  description?: string;
  memberCount?: number;
  onlineCount?: number;
  createdAt?: string;
  updatedAt?: string;
  private?: boolean;
  tags?: string[];
}

const MESSAGE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
const MESSAGE_TYPE = /^[A-Za-z][A-Za-z0-9._:-]{0,63}$/;
const ROOM_NAME = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
const UNSAFE_KEYS = new Set(["__proto__", "prototype", "constructor"]);

function randomMessageId(): string {
  const webCrypto = globalThis.crypto;
  if (!webCrypto) {
    throw new Error(
      "WRN-REALTIME-CRYPTO-UNAVAILABLE: provide an explicit message id in this runtime.",
    );
  }
  if (typeof webCrypto.randomUUID === "function") return webCrypto.randomUUID();
  const bytes = new Uint8Array(16);
  webCrypto.getRandomValues(bytes);
  return [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
}

function assertString(value: string | undefined, name: string, maxLength: number): void {
  if (value !== undefined && (value.length > maxLength || value.includes("\0"))) {
    throw new TypeError(`Invalid realtime ${name}.`);
  }
}

function assertSafeValue(
  value: unknown,
  maxDepth: number,
  depth = 0,
  seen = new WeakSet<object>(),
): void {
  if (depth > maxDepth) throw new TypeError("Realtime payload exceeds the maximum nesting depth.");
  if (value === null || ["string", "number", "boolean", "undefined"].includes(typeof value)) return;
  if (typeof value !== "object")
    throw new TypeError("Realtime payload contains an unsupported value.");
  if (seen.has(value)) throw new TypeError("Realtime payload contains a circular reference.");
  seen.add(value);
  if (Array.isArray(value)) {
    if (value.length > 10_000) throw new TypeError("Realtime payload array is too large.");
    for (const child of value) assertSafeValue(child, maxDepth, depth + 1, seen);
    return;
  }
  for (const [key, child] of Object.entries(value)) {
    if (UNSAFE_KEYS.has(key)) throw new TypeError(`Unsafe realtime payload key: ${key}`);
    assertSafeValue(child, maxDepth, depth + 1, seen);
  }
}

export function assertRealtimeRoomName(name: string): string {
  const value = name.trim();
  if (!ROOM_NAME.test(value)) throw new TypeError(`Invalid realtime room name: ${name}`);
  return value;
}

export function createRealtimeMessage<T>(
  options: CreateRealtimeMessageOptions<T>,
): RealtimeMessage<T> {
  const type = options.type.trim();
  if (!MESSAGE_TYPE.test(type)) throw new TypeError("Invalid realtime message type.");
  const id = options.id ?? randomMessageId();
  if (!MESSAGE_ID.test(id)) throw new TypeError("Invalid realtime message id.");
  const room = options.room === undefined ? undefined : assertRealtimeRoomName(options.room);
  assertString(options.senderId, "sender id", 256);
  assertString(options.senderName, "sender name", 256);
  assertSafeValue(options.data, 16);
  if (options.meta) assertSafeValue(options.meta, 4);
  const sentAt =
    options.sentAt instanceof Date
      ? options.sentAt.toISOString()
      : (options.sentAt ?? new Date().toISOString());
  if (!Number.isFinite(Date.parse(sentAt))) throw new TypeError("Invalid realtime sentAt value.");
  return {
    id,
    type,
    ...(room ? { room } : {}),
    ...(options.senderId ? { senderId: options.senderId } : {}),
    ...(options.senderName ? { senderName: options.senderName } : {}),
    sentAt,
    data: options.data,
    ...(options.meta ? { meta: { ...options.meta } } : {}),
  };
}

export function isRealtimeMessage(value: unknown): value is RealtimeMessage {
  if (!value || typeof value !== "object") return false;
  const message = value as Partial<RealtimeMessage>;
  return (
    typeof message.id === "string" &&
    MESSAGE_ID.test(message.id) &&
    typeof message.type === "string" &&
    MESSAGE_TYPE.test(message.type) &&
    typeof message.sentAt === "string" &&
    Number.isFinite(Date.parse(message.sentAt)) &&
    (message.room === undefined ||
      (typeof message.room === "string" && ROOM_NAME.test(message.room))) &&
    "data" in message
  );
}

export function parseRealtimeMessage<T = unknown>(
  value: string | unknown,
  options: ParseRealtimeMessageOptions = {},
): RealtimeMessage<T> {
  const maxBytes = options.maxBytes ?? 65_536;
  if (!Number.isFinite(maxBytes) || maxBytes < 1) {
    throw new TypeError("Realtime maxBytes must be a positive finite number.");
  }
  if (typeof value === "string" && new TextEncoder().encode(value).byteLength > maxBytes) {
    throw new RangeError("Realtime message exceeds the maximum size.");
  }
  const parsed = typeof value === "string" ? JSON.parse(value) : value;
  if (!isRealtimeMessage(parsed)) throw new TypeError("Invalid realtime message envelope.");
  if (options.allowedTypes && !options.allowedTypes.includes(parsed.type)) {
    throw new TypeError(`Realtime message type is not allowed: ${parsed.type}`);
  }
  if (options.room && parsed.room !== assertRealtimeRoomName(options.room)) {
    throw new TypeError("Realtime message room does not match the active room.");
  }
  assertSafeValue(parsed.data, Math.max(1, options.maxDepth ?? 16));
  if (parsed.meta) assertSafeValue(parsed.meta, 4);
  return parsed as RealtimeMessage<T>;
}

export function createPresenceEvent(
  action: "join" | "leave" | "update",
  presence: RealtimePresence,
  room?: string,
): RealtimeMessage<{ action: "join" | "leave" | "update"; presence: RealtimePresence }> {
  return createRealtimeMessage({ type: "presence", room, data: { action, presence } });
}

export function createTypingEvent(
  userId: string,
  typing: boolean,
  options: { room?: string; name?: string } = {},
): RealtimeMessage<{ userId: string; name?: string; typing: boolean }> {
  return createRealtimeMessage({
    type: "typing",
    room: options.room,
    senderId: userId,
    senderName: options.name,
    data: { userId, ...(options.name ? { name: options.name } : {}), typing },
  });
}

export function roomMemberSummary(members: readonly RealtimePresence[]): {
  total: number;
  online: number;
  away: number;
  busy: number;
} {
  return members.reduce(
    (summary, member) => {
      summary.total += 1;
      if (member.status === "away") summary.away += 1;
      else if (member.status === "busy") summary.busy += 1;
      else if (member.status !== "offline") summary.online += 1;
      return summary;
    },
    { total: 0, online: 0, away: 0, busy: 0 },
  );
}
