import { z } from 'zod';
import { int, num, hpSchema, conditionSchema } from '@/lib/schemas/common';
import { characterSchema, spellcastingSchema, resourceSchema, defensesSchema } from '@/lib/schemas/character';

/**
 * Wire protocol shared by the client and the realtime server. Everything is
 * JSON-safe and Zod-validated on both ends. The GM is the sole authoritative
 * writer; players are read-only.
 */

const condSchema = z.object({ name: z.string(), value: int.optional() });

// ---- player-facing projections (the only campaign data that leaves the GM) ----

export const playerCharacterSchema = z.object({
  id: z.string(),
  name: z.string(),
  level: int,
  ac: int,
  imageId: z.string().optional(),
  hp: hpSchema,
  conditions: z.array(condSchema).default([]),
  // enforced-state mirrors, so a player sees their own slots/concentration live
  slots: z.array(z.object({ level: int, current: int, max: int })).optional(),
  concentration: z.string().nullable().optional(),
  resources: z.array(z.object({ name: z.string(), current: int, max: int })).optional(),
});

export const playerCombatantSchema = z.object({
  id: z.string(),
  name: z.string(),
  kind: z.enum(['pc', 'npc', 'monster']),
  initiative: int,
  isCurrent: z.boolean(),
  hp: hpSchema.optional(),
  status: z.object({ label: z.string(), cls: z.string() }).optional(),
  conditions: z.array(condSchema).default([]),
});

export const playerEncounterSchema = z.object({
  id: z.string(),
  name: z.string(),
  round: int,
  combatants: z.array(playerCombatantSchema),
});

const playerTokenSchema = z.object({
  id: z.string(), label: z.string(), color: z.string(), col: int, row: int, size: int,
  kind: z.enum(['pc', 'npc', 'monster', 'object']),
  imageId: z.string().optional(),
  hp: hpSchema.optional(),
  conditions: z.array(condSchema).default([]),
});
const playerDrawingSchema = z.object({
  id: z.string(), kind: z.enum(['freehand', 'line', 'rect', 'circle', 'arrow', 'text']),
  points: z.array(z.object({ x: num, y: num })), color: z.string(), width: num, text: z.string().optional(),
});
export const playerMapSchema = z.object({
  id: z.string(), name: z.string(), gridSize: int, showGrid: z.boolean(), fogEnabled: z.boolean(),
  revealed: z.array(z.string()), gridType: z.enum(['square', 'hex']), gridUnit: z.object({ feet: int }),
  tokens: z.array(playerTokenSchema), drawings: z.array(playerDrawingSchema), hasImage: z.boolean(),
});

export const handoutSchema = z.object({
  title: z.string(),
  body: z.string(),
  imageId: z.string().nullable(),
});

export const playerQuestSchema = z.object({
  title: z.string(),
  completed: z.boolean(),
  objectives: z.array(z.object({ text: z.string(), done: z.boolean() })),
});

export const snapshotSchema = z.object({
  campaignName: z.string(),
  system: z.string().optional(),
  calendarDay: int.nullable(),
  party: z.array(playerCharacterSchema),
  encounter: playerEncounterSchema.nullable(),
  map: playerMapSchema.nullable(),
  mapImageId: z.string().nullable(),
  handout: handoutSchema.nullable().optional(),
  quests: z.array(playerQuestSchema).default([]),
});
export type Snapshot = z.infer<typeof snapshotSchema>;

// ---- two-way play: seat claiming + player-owned mutations ----

/**
 * The only character fields a seated player may mutate live. Unknown keys (name,
 * abilities, class, …) are stripped by Zod, so a player can never rewrite identity.
 */
export const partialCharacterDiffSchema = z.object({
  hp: hpSchema.optional(),
  conditions: z.array(conditionSchema).optional(),
  spellcasting: spellcastingSchema.optional(),
  resources: z.array(resourceSchema).optional(),
  defenses: defensesSchema.optional(),
  /** the spell a seated player is concentrating on (or null when they drop it) */
  concentration: characterSchema.shape.concentration.optional(),
});
export type PartialCharacterDiff = z.infer<typeof partialCharacterDiffSchema>;

export const rollBroadcastSchema = z.object({
  playerName: z.string(),
  label: z.string(),
  expression: z.string(),
  total: int,
  breakdown: z.string(),
});
export type RollBroadcast = z.infer<typeof rollBroadcastSchema>;

/** A handout/note/image targeted at specific players. Image is inline (NOT the
 *  shared image channel) so non-recipients never receive it. */
export const privateHandoutSchema = z.object({
  title: z.string().max(200),
  body: z.string().max(5000),
  image: z.string().optional(),
});
export type PrivateHandout = z.infer<typeof privateHandoutSchema>;

export const rosterEntrySchema = z.object({ playerId: z.string(), name: z.string(), character: z.string().optional() });
export type RosterEntry = z.infer<typeof rosterEntrySchema>;

// ---- messages ----

export const clientMessageSchema = z.discriminatedUnion('t', [
  z.object({ t: z.literal('host'), campaignId: z.string(), password: z.string().max(128).optional(), resume: z.string().optional() }),
  z.object({ t: z.literal('join'), joinCode: z.string().max(16), password: z.string().max(128).optional(), playerId: z.string().max(64).optional() }),
  z.object({ t: z.literal('state'), gmSecret: z.string(), snapshot: snapshotSchema }),
  z.object({ t: z.literal('image'), gmSecret: z.string(), id: z.string(), dataUrl: z.string() }),
  z.object({ t: z.literal('requestImage'), id: z.string() }),
  // two-way play (player → server)
  z.object({ t: z.literal('claimSeat'), characterId: z.string(), offlineSnapshot: characterSchema.optional() }),
  z.object({ t: z.literal('playerPatch'), characterId: z.string(), diff: partialCharacterDiffSchema }),
  z.object({ t: z.literal('playerRoll'), characterId: z.string(), label: z.string().max(120), expression: z.string().max(200), total: int, breakdown: z.string().max(600) }),
  // two-way play (GM → server)
  z.object({ t: z.literal('seatGrant'), gmSecret: z.string(), targetPlayerId: z.string(), character: characterSchema }),
  z.object({ t: z.literal('gmRoll'), gmSecret: z.string(), label: z.string().max(120), expression: z.string().max(200), total: int, breakdown: z.string().max(600) }),
  // targeted private handout (GM → one player)
  z.object({ t: z.literal('privateState'), gmSecret: z.string(), targetPlayerId: z.string(), handout: privateHandoutSchema.nullable() }),
  // chat: table message (no `to`) or whisper. GM `to` = target playerId; player `to` set = whisper to GM.
  z.object({ t: z.literal('chat'), body: z.string().min(1).max(2000), to: z.string().optional() }),
  // a player sets their display name (shown in the roster + chat)
  z.object({ t: z.literal('setName'), name: z.string().max(40) }),
]);
export type ClientMessage = z.infer<typeof clientMessageSchema>;

export const serverMessageSchema = z.discriminatedUnion('t', [
  z.object({ t: z.literal('hosted'), roomId: z.string(), joinCode: z.string(), gmSecret: z.string() }),
  z.object({ t: z.literal('joined'), roomId: z.string() }),
  z.object({ t: z.literal('snapshot'), snapshot: snapshotSchema }),
  z.object({ t: z.literal('mapImage'), id: z.string(), dataUrl: z.string() }),
  z.object({ t: z.literal('error'), code: z.string(), message: z.string() }),
  // two-way play
  z.object({ t: z.literal('seatRequest'), playerId: z.string(), characterId: z.string(), offlineSnapshot: characterSchema.optional() }),
  z.object({ t: z.literal('seatGranted'), character: characterSchema }),
  z.object({ t: z.literal('playerPatched'), characterId: z.string(), diff: partialCharacterDiffSchema }),
  z.object({ t: z.literal('rollBroadcast'), ...rollBroadcastSchema.shape }),
  z.object({ t: z.literal('privateHandout'), handout: privateHandoutSchema.nullable() }),
  z.object({ t: z.literal('roster'), players: z.array(rosterEntrySchema) }),
  z.object({ t: z.literal('chat'), from: z.string(), body: z.string(), scope: z.enum(['table', 'whisper']), to: z.string().optional() }),
]);
export type ServerMessage = z.infer<typeof serverMessageSchema>;
