/**
 * Client → server and host → server payloads (spec §7, ARCHITECTURE §4).
 *
 * All payloads are strict: unknown keys are rejected so a client can never smuggle fields such as
 * `now` or `points` into a command. Server time and identity always come from the server and the token.
 */
import { z } from 'zod';
import {
  IdempotencyKeySchema,
  NicknameInputSchema,
  OptionIdSchema,
  PositionSchema,
  PowerUpTypeSchema,
  SeqSchema,
  TeamNameInputSchema,
  UuidSchema,
  VenueCodeSchema,
} from './common';
import type { TokenScope } from './tokens';

// ───────────────────────────── Player / TV events ─────────────────────────────

export const RoomJoinPayloadSchema = z.strictObject({
  venueCode: VenueCodeSchema,
  /** Optional when the venue has a single room (QR posters carry it for multi-room venues). */
  roomId: UuidSchema.optional(),
  nickname: NicknameInputSchema,
  teamName: TeamNameInputSchema.optional(),
  /** Random per-browser id; only a salted hash is stored. */
  deviceId: UuidSchema,
  idempotencyKey: IdempotencyKeySchema,
});

export const AnswerSubmitPayloadSchema = z.strictObject({
  questionId: UuidSchema,
  optionId: OptionIdSchema,
  /** Wildcard question only: the tile to activate. Missing/invalid → engine auto-picks. */
  wildcardPosition: PositionSchema.nullable().optional(),
  idempotencyKey: IdempotencyKeySchema,
});

/**
 * `questionId` is optional for every type: Double Down targets the *next* question, whose id the phone
 * may not know yet (INTRO/REVEALED), and the engine resolves it. `from`/`to` belong to Category Swap only.
 */
export const PowerUpActivatePayloadSchema = z
  .strictObject({
    type: PowerUpTypeSchema,
    questionId: UuidSchema.optional(),
    from: PositionSchema.optional(),
    to: PositionSchema.optional(),
    idempotencyKey: IdempotencyKeySchema,
  })
  .superRefine((p, ctx) => {
    if (p.type === 'CATEGORY_SWAP') {
      if (p.from === undefined) ctx.addIssue({ code: 'custom', message: 'CATEGORY_SWAP requires from', path: ['from'] });
      if (p.to === undefined) ctx.addIssue({ code: 'custom', message: 'CATEGORY_SWAP requires to', path: ['to'] });
      if (p.from !== undefined && p.from === p.to) {
        ctx.addIssue({ code: 'custom', message: 'CATEGORY_SWAP needs two different tiles', path: ['to'] });
      }
    } else {
      if (p.from !== undefined) ctx.addIssue({ code: 'custom', message: `from is only valid for CATEGORY_SWAP`, path: ['from'] });
      if (p.to !== undefined) ctx.addIssue({ code: 'custom', message: `to is only valid for CATEGORY_SWAP`, path: ['to'] });
    }
  });

/** Reconnect / gap recovery: the server always answers with a full snapshot. */
export const StateRequestPayloadSchema = z.strictObject({
  lastSeq: SeqSchema.optional(),
});

export const TeamSetPayloadSchema = z.strictObject({
  teamName: TeamNameInputSchema,
  idempotencyKey: IdempotencyKeySchema,
});

/** Final Rush non-finalists predict the winner (non-scoring). `finalistId` = finalist's PlayerSession id. */
export const FinalPredictPayloadSchema = z.strictObject({
  finalistId: UuidSchema,
  idempotencyKey: IdempotencyKeySchema,
});

/** NTP-style offset estimate for countdown display only; `t0` = client send time (any client clock). */
export const ClockSyncPayloadSchema = z.strictObject({
  t0: z.number(),
});

export const CLIENT_EVENTS = {
  'room.join': RoomJoinPayloadSchema,
  'answer.submit': AnswerSubmitPayloadSchema,
  'powerup.activate': PowerUpActivatePayloadSchema,
  'state.request': StateRequestPayloadSchema,
  'team.set': TeamSetPayloadSchema,
  'final.predict': FinalPredictPayloadSchema,
  'clock.sync': ClockSyncPayloadSchema,
} as const;

// ───────────────────────────── Host events ─────────────────────────────

const hostEventShape = { eventId: UuidSchema, idempotencyKey: IdempotencyKeySchema };

export const EventOpenPayloadSchema = z.strictObject(hostEventShape);
export const EventStartPayloadSchema = z.strictObject(hostEventShape);
export const EventPausePayloadSchema = z.strictObject(hostEventShape);
export const EventResumePayloadSchema = z.strictObject(hostEventShape);
export const EventEndPayloadSchema = z.strictObject(hostEventShape);
/** Cut the current phase or break short. */
export const RoundAdvancePayloadSchema = z.strictObject(hostEventShape);

export const NICKNAME_MODERATION_ACTIONS = ['rename', 'kick'] as const;

export const NicknameModeratePayloadSchema = z
  .strictObject({
    ...hostEventShape,
    playerSessionId: UuidSchema,
    action: z.enum(NICKNAME_MODERATION_ACTIONS),
    nickname: NicknameInputSchema.optional(),
  })
  .superRefine((p, ctx) => {
    if (p.action === 'rename' && p.nickname === undefined) {
      ctx.addIssue({ code: 'custom', message: 'rename requires nickname', path: ['nickname'] });
    }
    if (p.action === 'kick' && p.nickname !== undefined) {
      ctx.addIssue({ code: 'custom', message: 'kick does not take a nickname', path: ['nickname'] });
    }
  });

/**
 * Attach the host socket to an event's room and receive `host.state`. Read-only, so the idempotency
 * key is optional; without `eventId` the server picks the venue's live event, if any.
 */
export const HostSyncPayloadSchema = z.strictObject({
  eventId: UuidSchema.optional(),
  idempotencyKey: IdempotencyKeySchema.optional(),
});

export const HOST_EVENTS = {
  'event.open': EventOpenPayloadSchema,
  'event.start': EventStartPayloadSchema,
  'event.pause': EventPausePayloadSchema,
  'event.resume': EventResumePayloadSchema,
  'event.end': EventEndPayloadSchema,
  'round.advance': RoundAdvancePayloadSchema,
  'nickname.moderate': NicknameModeratePayloadSchema,
  'host.sync': HostSyncPayloadSchema,
} as const;

// ───────────────────────────── Names & types ─────────────────────────────

export type ClientEventName = keyof typeof CLIENT_EVENTS;
export type HostEventName = keyof typeof HOST_EVENTS;
export type InboundEventName = ClientEventName | HostEventName;

export const CLIENT_EVENT_NAMES = Object.keys(CLIENT_EVENTS) as ClientEventName[];
export const HOST_EVENT_NAMES = Object.keys(HOST_EVENTS) as HostEventName[];

/** Every inbound event → its payload schema (one lookup table for the server's validation step). */
export const INBOUND_EVENTS = { ...CLIENT_EVENTS, ...HOST_EVENTS } as const;

/** Validated payload (what the server works with after `parse`). */
export type ClientPayload<E extends ClientEventName> = z.output<(typeof CLIENT_EVENTS)[E]>;
export type HostPayload<E extends HostEventName> = z.output<(typeof HOST_EVENTS)[E]>;
export type InboundPayload<E extends InboundEventName> = z.output<(typeof INBOUND_EVENTS)[E]>;
/** What a client may emit (identical to the output today: no transforms or defaults). */
export type InboundPayloadInput<E extends InboundEventName> = z.input<(typeof INBOUND_EVENTS)[E]>;

export type RoomJoinPayload = ClientPayload<'room.join'>;
export type AnswerSubmitPayload = ClientPayload<'answer.submit'>;
export type PowerUpActivatePayload = ClientPayload<'powerup.activate'>;
export type StateRequestPayload = ClientPayload<'state.request'>;
export type TeamSetPayload = ClientPayload<'team.set'>;
export type FinalPredictPayload = ClientPayload<'final.predict'>;
export type ClockSyncPayload = ClientPayload<'clock.sync'>;
export type NicknameModeratePayload = HostPayload<'nickname.moderate'>;
export type HostSyncPayload = HostPayload<'host.sync'>;

export function isClientEventName(name: string): name is ClientEventName {
  return Object.hasOwn(CLIENT_EVENTS, name);
}

export function isHostEventName(name: string): name is HostEventName {
  return Object.hasOwn(HOST_EVENTS, name);
}

// ───────────────────────────── Access policy ─────────────────────────────

/** Who is calling: a verified token scope, or `anonymous` before any token. */
export type CallerScope = TokenScope | 'anonymous';

/**
 * Which callers may send each event (ARCHITECTURE §4): anonymous sockets only `room.join` and
 * `clock.sync`; TV tokens are display-only; host commands need a host token. A guest may call
 * `room.join` again for the one-tap "Join the Championship".
 */
export const EVENT_ACCESS: { readonly [E in InboundEventName]: readonly CallerScope[] } = {
  'room.join': ['anonymous', 'guest'],
  'answer.submit': ['guest'],
  'powerup.activate': ['guest'],
  'state.request': ['guest', 'tv', 'host'],
  'team.set': ['guest'],
  'final.predict': ['guest'],
  'clock.sync': ['anonymous', 'guest', 'tv', 'host'],
  'event.open': ['host'],
  'event.start': ['host'],
  'event.pause': ['host'],
  'event.resume': ['host'],
  'event.end': ['host'],
  'round.advance': ['host'],
  'nickname.moderate': ['host'],
  'host.sync': ['host'],
};

export function canCall(scope: CallerScope, event: InboundEventName): boolean {
  return EVENT_ACCESS[event].includes(scope);
}
