/**
 * Server → client messages. Every message is an envelope `{type, seq, serverNow, public?, me?, host?,
 * snapshot?, data?}` (ARCHITECTURE §4 "Messaging"): public messages to `room:<roomId>` increment `seq`,
 * personal ones to `player:<playerSessionId>` carry the current `seq`. A client that sees a gap calls
 * `state.request`.
 */
import { z } from 'zod';
import { EventModeSchema, SeqSchema, TimestampSchema, UuidSchema } from './common';
import { AckErrorSchema } from './acks';
import { RoomSnapshotSchema } from './snapshot';
import { SessionTokenDataSchema } from './tokens';
import {
  CompletedRoundSummarySchema,
  HostStateSchema,
  PrivateStateSchema,
  PublicStateSchema,
  StandingSchema,
} from './views';

export const SERVER_EVENTS = [
  'room.state',
  'round.start',
  'question.pending',
  'question.open',
  'question.lock',
  'question.reveal',
  'answer.result',
  'board.updated',
  'score.updated',
  'leaderboard.updated',
  'round.complete',
  'event.complete',
  'event.state',
  'host.state',
  'session.token',
  'player.kicked',
  'error',
] as const;
export type ServerEventName = (typeof SERVER_EVENTS)[number];
export const ServerEventNameSchema = z.enum(SERVER_EVENTS);

const serverEventNames: ReadonlySet<string> = new Set(SERVER_EVENTS);
export function isServerEventName(name: string): name is ServerEventName {
  return serverEventNames.has(name);
}

// ───────────────────────────── Event-specific data ─────────────────────────────

/** `round.complete`: the finished round's podium plus session standings after it. */
export const RoundCompleteDataSchema = z.strictObject({
  summary: CompletedRoundSummarySchema,
  standings: z.array(StandingSchema),
});

/** `event.complete`: final session standings. */
export const EventCompleteDataSchema = z.strictObject({
  standings: z.array(StandingSchema),
});

/**
 * `event.state`: the room's active event changed (championship lobby opened, always-on resumed, ...).
 * Same fields as the snapshot; phones compare `activeSessionId` with their own session.
 */
export const EventStateDataSchema = z.strictObject({
  activeSessionId: UuidSchema.nullable(),
  activeEventKind: EventModeSchema.nullable(),
  activeEventName: z.string().nullable(),
});

/** `player.kicked`: sent to the kicked player's channel just before the server drops the socket. */
export const PlayerKickedDataSchema = z.strictObject({
  playerSessionId: UuidSchema,
});

// ───────────────────────────── Envelope ─────────────────────────────

/** Generic envelope: validates the frame, leaves `data` unchecked. Prefer `ServerMessageSchema`. */
export const ServerEnvelopeSchema = z.strictObject({
  type: ServerEventNameSchema,
  seq: SeqSchema,
  serverNow: TimestampSchema,
  public: PublicStateSchema.nullable().optional(),
  me: PrivateStateSchema.nullable().optional(),
  host: HostStateSchema.optional(),
  snapshot: RoomSnapshotSchema.optional(),
  data: z.unknown().optional(),
});
export type AnyServerEnvelope = z.output<typeof ServerEnvelopeSchema>;

/** One envelope variant: `data` absent unless the event defines it; `required` makes parts mandatory. */
function message<const T extends ServerEventName, S extends z.core.$ZodLooseShape>(type: T, required: S) {
  return ServerEnvelopeSchema.extend({ type: z.literal(type), data: z.never().optional() }).extend(required);
}

/** Exact per-event envelopes: wrong or missing parts for a given `type` fail validation. */
export const ServerMessageSchema = z.discriminatedUnion('type', [
  message('room.state', { snapshot: RoomSnapshotSchema }),
  message('round.start', {}),
  message('question.pending', {}),
  message('question.open', {}),
  message('question.lock', {}),
  message('question.reveal', {}),
  message('answer.result', {}),
  message('board.updated', {}),
  message('score.updated', {}),
  message('leaderboard.updated', {}),
  message('round.complete', { data: RoundCompleteDataSchema }),
  message('event.complete', { data: EventCompleteDataSchema }),
  message('event.state', { data: EventStateDataSchema }),
  message('host.state', { host: HostStateSchema }),
  message('session.token', { data: SessionTokenDataSchema }),
  message('player.kicked', { data: PlayerKickedDataSchema }),
  message('error', { data: AckErrorSchema }),
]);

export type ServerMessage = z.output<typeof ServerMessageSchema>;
/** The envelope for one event type (or the union for several). */
export type ServerEnvelope<E extends ServerEventName = ServerEventName> = Extract<ServerMessage, { type: E }>;
export type ServerEventData<E extends ServerEventName> = ServerEnvelope<E>['data'];

export type RoundCompleteData = z.output<typeof RoundCompleteDataSchema>;
export type EventCompleteData = z.output<typeof EventCompleteDataSchema>;
export type EventStateData = z.output<typeof EventStateDataSchema>;
export type PlayerKickedData = z.output<typeof PlayerKickedDataSchema>;

// ───────────────────────────── Helpers ─────────────────────────────

type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;

export interface EnvelopeMeta {
  seq: number;
  serverNow: number;
}
/** Everything in an envelope except the frame fields. */
export type EnvelopeBody<E extends ServerEventName> = DistributiveOmit<ServerEnvelope<E>, 'type' | 'seq' | 'serverNow'>;

/** Build a typed envelope; the compiler enforces the parts each event requires. */
export function envelope<E extends ServerEventName>(type: E, meta: EnvelopeMeta, body: EnvelopeBody<E>): ServerEnvelope<E> {
  // TS cannot prove that re-attaching `type` to a distributive Omit of Extract<…, {type: E}> yields the
  // same member for a generic E; the parameter types already guarantee it.
  return { ...body, type, seq: meta.seq, serverNow: meta.serverNow } as unknown as ServerEnvelope<E>;
}

/** Validate an incoming message (client side), including per-event required parts and data. */
export function parseServerMessage(input: unknown) {
  return ServerMessageSchema.safeParse(input);
}
