/**
 * Socket.IO acknowledgements: every inbound event is answered `{ok: true, data?}` or
 * `{ok: false, error: {code, message}}`. A duplicate idempotency key gets the same success reply
 * as the original (no `duplicate` flag), as the spec requires.
 */
import { z } from 'zod';
import type { ErrorCode } from '@pintrush/engine';
import { IdSchema, OptionIdSchema, PlayerStatusSchema, SeqSchema, TimestampSchema, UuidSchema } from './common';
import type { ClientEventName, HostEventName } from './client';
import { RoomSnapshotSchema } from './snapshot';
import { SessionTokenDataSchema } from './tokens';
import { HostStateSchema } from './views';

// ───────────────────────────── Error codes ─────────────────────────────

/** Transport/server-level failures (validation, auth, tenancy, rate limits). */
export const PROTOCOL_ERROR_CODES = [
  'BAD_REQUEST',
  'UNAUTHORIZED',
  'FORBIDDEN',
  'RATE_LIMITED',
  'NOT_FOUND',
  'ROOM_CLOSED',
  'NO_ACTIVE_SESSION',
  'NICKNAME_INVALID',
  'NICKNAME_PROFANE',
  'TEAM_NAME_INVALID',
  'INTERNAL',
] as const;
export type ProtocolErrorCode = (typeof PROTOCOL_ERROR_CODES)[number];

/**
 * Engine reply codes, forwarded verbatim. Listed at runtime here because the engine exports `ErrorCode`
 * only as a type; `alignment.ts` fails typecheck if this list and the engine union diverge.
 */
export const ENGINE_ERROR_CODES = [
  'SESSION_NOT_RUNNING',
  'SESSION_COMPLETED',
  'ALREADY_STARTED',
  'NOT_PAUSED',
  'NO_ROUND',
  'ROUND_NOT_EXPECTED',
  'INVALID_ROUND',
  'UNKNOWN_PLAYER',
  'PLAYER_KICKED',
  'NICKNAME_TAKEN',
  'NOT_PARTICIPANT',
  'WRONG_PHASE',
  'WRONG_QUESTION',
  'TOO_LATE',
  'ALREADY_ANSWERED',
  'INVALID_OPTION',
  'INVALID_TILE',
  'POWERUPS_DISABLED',
  'NO_TOKEN',
  'POWERUP_ALREADY_USED',
  'POWERUP_NOT_APPLICABLE',
  'INSUFFICIENT_BALANCE',
  'NOT_FINALIST_ROUND',
  'CANNOT_PREDICT',
] as const satisfies readonly ErrorCode[];

export const ACK_ERROR_CODES = [...PROTOCOL_ERROR_CODES, ...ENGINE_ERROR_CODES] as const;
export type AckErrorCode = ProtocolErrorCode | ErrorCode;

export const AckErrorCodeSchema = z.enum(ACK_ERROR_CODES);
export const AckErrorSchema = z.strictObject({
  code: AckErrorCodeSchema,
  message: z.string(),
});
export type AckError = z.output<typeof AckErrorSchema>;

const ackErrorCodes: ReadonlySet<string> = new Set(ACK_ERROR_CODES);
export function isAckErrorCode(code: string): code is AckErrorCode {
  return ackErrorCodes.has(code);
}

// ───────────────────────────── Ack type + helpers ─────────────────────────────

export type AckOk<T> = { ok: true; data?: T };
export type AckFail = { ok: false; error: AckError };
/** `T = never` for events whose success carries no data. */
export type Ack<T = never> = AckOk<T> | AckFail;

/** Success reply; omit `data` for events that return nothing. */
export function ok<T = never>(data?: T): AckOk<T> {
  return data === undefined ? { ok: true } : { ok: true, data };
}

/** Failure reply. Pass engine `Reply.error` fields straight through for game-rule rejections. */
export function fail(code: AckErrorCode, message: string): AckFail {
  return { ok: false, error: { code, message } };
}

/** Runtime schema for an ack whose success data matches `data` (clients can validate replies). */
export function ackSchema<T extends z.ZodType>(data: T) {
  return z.discriminatedUnion('ok', [
    z.strictObject({ ok: z.literal(true), data: data.optional() }),
    z.strictObject({ ok: z.literal(false), error: AckErrorSchema }),
  ]);
}

// ───────────────────────────── Per-event success data ─────────────────────────────

/** No success data: `data` must be absent. */
export const NoAckDataSchema = z.never();

/** `room.join`: the player's new (or resumed) session, its token and a snapshot to render immediately. */
export const RoomJoinResultSchema = SessionTokenDataSchema.extend({
  roomId: UuidSchema,
  /** Normalised and de-duplicated ("Mike 2"). */
  nickname: z.string().min(1),
  teamName: z.string().nullable(),
  snapshot: RoomSnapshotSchema,
});

/** `powerup.activate`: Second Chance returns the removed (incorrect) option. */
export const PowerUpActivateResultSchema = z.strictObject({
  removedOptionId: OptionIdSchema.optional(),
});

export const TeamSetResultSchema = z.strictObject({
  teamId: IdSchema.nullable(),
  teamName: z.string().nullable(),
});

/** Offset ≈ serverNow − (t0 + t1) / 2, where t1 is the client receive time. */
export const ClockSyncResultSchema = z.strictObject({
  t0: z.number(),
  serverNow: TimestampSchema,
});

export const NicknameModerateResultSchema = z.strictObject({
  playerSessionId: UuidSchema,
  status: PlayerStatusSchema,
  /** Nickname after the action (normalised/de-duplicated on rename). */
  nickname: z.string().min(1),
});

export const HostSyncResultSchema = z.strictObject({
  /** Null when no `eventId` was given and the venue has no live event. */
  eventId: UuidSchema.nullable(),
  roomId: UuidSchema.nullable(),
  seq: SeqSchema,
  /** Null until the event's session exists (event not opened yet). */
  host: HostStateSchema.nullable(),
});

export const CLIENT_ACK_DATA = {
  'room.join': RoomJoinResultSchema,
  'answer.submit': NoAckDataSchema,
  'powerup.activate': PowerUpActivateResultSchema,
  'state.request': RoomSnapshotSchema,
  'team.set': TeamSetResultSchema,
  'final.predict': NoAckDataSchema,
  'clock.sync': ClockSyncResultSchema,
} as const satisfies { readonly [E in ClientEventName]: z.ZodType };

export const HOST_ACK_DATA = {
  'event.open': NoAckDataSchema,
  'event.start': NoAckDataSchema,
  'event.pause': NoAckDataSchema,
  'event.resume': NoAckDataSchema,
  'event.end': NoAckDataSchema,
  'round.advance': NoAckDataSchema,
  'nickname.moderate': NicknameModerateResultSchema,
  'host.sync': HostSyncResultSchema,
} as const satisfies { readonly [E in HostEventName]: z.ZodType };

export type ClientAckData<E extends ClientEventName> = z.output<(typeof CLIENT_ACK_DATA)[E]>;
export type HostAckData<E extends HostEventName> = z.output<(typeof HOST_ACK_DATA)[E]>;

export type RoomJoinResult = z.output<typeof RoomJoinResultSchema>;
export type PowerUpActivateResult = z.output<typeof PowerUpActivateResultSchema>;
export type TeamSetResult = z.output<typeof TeamSetResultSchema>;
export type ClockSyncResult = z.output<typeof ClockSyncResultSchema>;
export type NicknameModerateResult = z.output<typeof NicknameModerateResultSchema>;
export type HostSyncResult = z.output<typeof HostSyncResultSchema>;
