import { z } from "zod";
import { Blocks, Call, CalendarEvent, Channel, ChannelMember, Message, NotificationPreferences, Reaction, Task, ThreadSubscriptionStatus, Ulid } from "./entities";
import { CallSignalKind } from "./calls-http";

/**
 * Realtime event catalogue (§8.1 spec required before Phase 2).
 *
 * Protocol version is negotiated at connect time via the `auth.protocolVersion`
 * field on the Socket.IO handshake. The gateway rejects a connection whose major
 * version it does not support, so an event's payload shape may only change
 * within a major version by adding optional fields.
 */
export const PROTOCOL_VERSION = "1.0" as const;

// ---- Rooms -----------------------------------------------------------------
// ws:{workspaceId}   — workspace-wide events (presence, membership)
// ch:{channelId}     — per-channel events, joined only while a channel is open
// u:{userId}         — personal events (DMs opened elsewhere, notifications)

export const ConnectAuth = z.object({
  ticket: z.string().min(1),
  protocolVersion: z.string(),
});
export type ConnectAuth = z.infer<typeof ConnectAuth>;

// ---- Client -> Server --------------------------------------------------------

export const ClientJoinChannel = z.object({
  channelId: Ulid,
  cursorSeq: z.number().int().nonnegative(), // last_read_seq the client caught up from
});

export const ClientSendMessage = z.object({
  channelId: Ulid,
  clientMsgId: z.string().uuid(),
  // Empty text is valid for an attachment-only message. The write service
  // rejects a message only when both text and verified attachments are empty.
  text: z.string().max(40_000),
  blocks: z.unknown().optional(),
  parentId: Ulid.nullable().optional(),
  isBroadcast: z.boolean().optional(),
});

export const ClientAckRead = z.object({
  channelId: Ulid,
  seq: z.number().int().nonnegative(),
});

export const ClientAckThreadRead = z.object({
  rootMessageId: Ulid,
  seq: z.number().int().nonnegative(),
});

export const ClientTyping = z.object({
  workspaceId: Ulid,
  channelId: Ulid,
});

export const ClientPresenceHeartbeat = z.object({ active: z.boolean().optional() });

/** WebRTC signalling, relayed peer-to-peer through the gateway. This is the
 * one client event whose payload the server forwards rather than acts on —
 * see CallSignalRequest in calls-http.ts. */
export const ClientCallSignal = z.object({
  workspaceId: Ulid,
  callId: Ulid,
  toUserId: Ulid,
  signal: CallSignalKind,
  data: z.unknown(),
});

export const ClientEventEnvelope = z.discriminatedUnion("type", [
  z.object({ type: z.literal("channel:join"), payload: ClientJoinChannel }),
  z.object({ type: z.literal("message:send"), payload: ClientSendMessage }),
  z.object({ type: z.literal("read:ack"), payload: ClientAckRead }),
  z.object({ type: z.literal("thread:ack"), payload: ClientAckThreadRead }),
  z.object({ type: z.literal("typing:start"), payload: ClientTyping }),
  z.object({ type: z.literal("presence:heartbeat"), payload: ClientPresenceHeartbeat }),
  z.object({ type: z.literal("call:signal"), payload: ClientCallSignal }),
]);
export type ClientEventEnvelope = z.infer<typeof ClientEventEnvelope>;

// ---- Server -> Client --------------------------------------------------------

export const ServerMessageCreated = z.object({
  message: Message,
  /** Present on personal-room fanout so clients can route without joining ch: */
  workspaceId: Ulid.optional(),
  channelId: Ulid.optional(),
});
export const ServerMessageEdited = z.object({ message: Message });
export const ServerMessageDeleted = z.object({
  channelId: Ulid,
  messageId: Ulid,
  seq: z.number().int().positive(),
});
export const ServerReactionChanged = z.object({
  reaction: Reaction,
  op: z.enum(["add", "remove"]),
});
export const ServerPinChanged = z.object({
  channelId: Ulid,
  messageId: Ulid,
  op: z.enum(["add", "remove"]),
});
export const ServerChannelUpdated = z.object({ channel: Channel });
export const ServerReadStateUpdated = z.object({ channelMember: ChannelMember });
export const ServerTypingChanged = z.object({
  channelId: Ulid,
  userId: Ulid,
  isTyping: z.boolean(),
});
export const ServerPresenceChanged = z.object({
  workspaceId: Ulid,
  userId: Ulid,
  status: z.enum(["active", "away", "offline"]),
});
export const ServerMemberStatusChanged = z.object({
  workspaceId: Ulid,
  userId: Ulid,
});
export const ServerSavedItemUpdated = z.object({
  workspaceId: Ulid,
  messageId: Ulid,
  saved: z.boolean(),
});
export const ServerDraftUpdated = z.object({
  workspaceId: Ulid,
  channelId: Ulid,
  threadRootMessageId: Ulid.nullable(),
  text: z.string().max(40_000),
  blocks: Blocks.nullable(),
  deleted: z.boolean(),
});
export const ServerNotificationPreferencesUpdated = z.object({
  preferences: NotificationPreferences,
});
export const ServerActivityReadUpdated = z.object({
  workspaceId: Ulid,
  messageIds: z.array(Ulid),
  read: z.boolean(),
});
export const ServerThreadSubscriptionUpdated = z.object({ status: ThreadSubscriptionStatus });

// One event for every mutation kind (create/assign/status/edit) — the
// payload always carries the full current task, same simplification
// channel:updated already makes for updateChannel/archiveChannel, so the
// client never needs a per-mutation-kind switch.
export const ServerTaskCreated = z.object({ task: Task });
export const ServerTaskUpdated = z.object({ task: Task });
export const ServerTaskDeleted = z.object({
  workspaceId: Ulid,
  taskId: Ulid,
  title: z.string(),
  createdBy: Ulid,
  deletedByUserId: Ulid,
});

// Calendar mutations carry the stored series row, not an expanded occurrence:
// a client can't merge one occurrence into a window it may not be showing, so
// it refetches its current window on any of these. `event:updated` therefore
// covers series edits, occurrence overrides, invites and RSVPs alike.
export const ServerCalendarEventCreated = z.object({ event: CalendarEvent });
export const ServerCalendarEventUpdated = z.object({ event: CalendarEvent });
export const ServerCalendarEventDeleted = z.object({ workspaceId: Ulid, eventId: Ulid });

// Calls carry the whole row on every transition, for the same reason tasks do:
// a client showing history, an active-calls strip and a ringing dialog all
// need different slices of the same change, and one full-row event serves all
// three without a per-transition switch. `call:ended` additionally exists as
// its own type so a client can tear down media without diffing statuses.
export const ServerCallStarted = z.object({ call: Call });
export const ServerCallUpdated = z.object({ call: Call });
export const ServerCallEnded = z.object({ call: Call });

/**
 * A relayed WebRTC offer/answer/ICE candidate. Unlike every other server
 * event this is addressed to exactly one user's personal room and is not a
 * broadcast of state — `fromUserId` is stamped by the gateway from the
 * authenticated socket, never taken from the sender's payload.
 */
export const ServerCallSignal = z.object({
  callId: Ulid,
  fromUserId: Ulid,
  signal: CallSignalKind,
  data: z.unknown(),
});

export const ServerEventEnvelope = z.discriminatedUnion("type", [
  z.object({ type: z.literal("message:created"), payload: ServerMessageCreated }),
  z.object({ type: z.literal("message:edited"), payload: ServerMessageEdited }),
  z.object({ type: z.literal("message:deleted"), payload: ServerMessageDeleted }),
  z.object({ type: z.literal("reaction:changed"), payload: ServerReactionChanged }),
  z.object({ type: z.literal("pin:changed"), payload: ServerPinChanged }),
  z.object({ type: z.literal("channel:updated"), payload: ServerChannelUpdated }),
  z.object({ type: z.literal("read:updated"), payload: ServerReadStateUpdated }),
  z.object({ type: z.literal("typing:changed"), payload: ServerTypingChanged }),
  z.object({ type: z.literal("presence:changed"), payload: ServerPresenceChanged }),
  z.object({ type: z.literal("member:status_changed"), payload: ServerMemberStatusChanged }),
  z.object({ type: z.literal("saved:updated"), payload: ServerSavedItemUpdated }),
  z.object({ type: z.literal("draft:updated"), payload: ServerDraftUpdated }),
  z.object({ type: z.literal("notification-preferences:updated"), payload: ServerNotificationPreferencesUpdated }),
  z.object({ type: z.literal("activity:read-updated"), payload: ServerActivityReadUpdated }),
  z.object({ type: z.literal("thread:subscription-updated"), payload: ServerThreadSubscriptionUpdated }),
  z.object({ type: z.literal("task:created"), payload: ServerTaskCreated }),
  z.object({ type: z.literal("task:updated"), payload: ServerTaskUpdated }),
  z.object({ type: z.literal("task:deleted"), payload: ServerTaskDeleted }),
  z.object({ type: z.literal("event:created"), payload: ServerCalendarEventCreated }),
  z.object({ type: z.literal("event:updated"), payload: ServerCalendarEventUpdated }),
  z.object({ type: z.literal("event:deleted"), payload: ServerCalendarEventDeleted }),
  z.object({ type: z.literal("call:started"), payload: ServerCallStarted }),
  z.object({ type: z.literal("call:updated"), payload: ServerCallUpdated }),
  z.object({ type: z.literal("call:ended"), payload: ServerCallEnded }),
  z.object({ type: z.literal("call:signal"), payload: ServerCallSignal }),
]);
export type ServerEventEnvelope = z.infer<typeof ServerEventEnvelope>;
