import { z } from "zod";

// Primary keys are integers everywhere (§4.7).
export const EntityId = z.coerce.number().int().positive();
export type EntityId = z.infer<typeof EntityId>;
/** @deprecated Use EntityId */
export const Ulid = EntityId;

export const Role = z.enum([
  "owner",
  "admin",
  "member",
  "multi_channel_guest",
  "single_channel_guest",
  "bot",
]);
export type Role = z.infer<typeof Role>;

export const ChannelType = z.enum(["public", "private", "dm", "group_dm"]);
export type ChannelType = z.infer<typeof ChannelType>;

const PublicAssetUrl = z.union([z.string().url(), z.string().regex(/^\/profile-photos\/\d+\/[a-f0-9-]+\.webp$/i)]);

export const User = z.object({
  id: Ulid,
  email: z.string().email(),
  emailVerifiedAt: z.string().datetime().nullable(),
  name: z.string().min(1).max(120),
  // Global handle used to resolve @mentions (core.parseMentions) to a user
  // id — see ARCHITECTURE §4.7 / the mentions gap noted in PHASE2_STATUS.md.
  username: z.string().min(1).max(32),
  avatarUrl: PublicAssetUrl.nullable(),
  tz: z.string(),
  createdAt: z.string().datetime(),
});
export type User = z.infer<typeof User>;

export const Workspace = z.object({
  id: Ulid,
  orgId: Ulid,
  slug: z.string().regex(/^[a-z0-9-]{2,64}$/),
  name: z.string().min(1).max(120),
  iconUrl: z.string().url().nullable(),
  retentionDays: z.number().int().nonnegative().nullable(),
});
export type Workspace = z.infer<typeof Workspace>;

export const WorkspaceMember = z.object({
  workspaceId: Ulid,
  userId: Ulid,
  role: Role,
  displayName: z.string().nullable(),
  title: z.string().nullable(),
  statusText: z.string().nullable(),
  statusEmoji: z.string().nullable(),
  statusExpiresAt: z.string().datetime().nullable(),
  availabilityMode: z.enum(["auto", "away", "available"]),
  dndEnabled: z.boolean(),
  dndUntil: z.string().datetime().nullable(),
  joinedAt: z.string().datetime(),
  deactivatedAt: z.string().datetime().nullable(),
});
export type WorkspaceMember = z.infer<typeof WorkspaceMember>;

export const Channel = z.object({
  id: Ulid,
  workspaceId: Ulid,
  type: ChannelType,
  name: z.string().min(1).max(80).nullable(),
  topic: z.string().nullable(),
  purpose: z.string().nullable(),
  createdBy: Ulid,
  isArchived: z.boolean(),
  lastMessageAt: z.string().datetime().nullable(),
  memberCount: z.number().int().nonnegative(),
});
export type Channel = z.infer<typeof Channel>;

// The `blocks` wire format — versioned per §8.1. v1 is a minimal ProseMirror-doc
// subset sufficient for the MVP composer (§4.2): paragraphs, marks, and links.
export const ForwardedMessageSnapshot = z.object({
  messageId: Ulid,
  channelId: Ulid,
  parentId: Ulid.nullable(),
  channelName: z.string().nullable(),
  channelType: ChannelType,
  authorId: Ulid,
  authorName: z.string().min(1).max(120),
  authorAvatarUrl: PublicAssetUrl.nullable(),
  text: z.string().max(40_000),
  createdAt: z.string().datetime(),
  attachmentNames: z.array(z.string().min(1).max(255)).max(10),
});
export type ForwardedMessageSnapshot = z.infer<typeof ForwardedMessageSnapshot>;

export const BlocksV1 = z.object({
  v: z.literal(1),
  doc: z.record(z.string(), z.unknown()),
  forwardedMessage: ForwardedMessageSnapshot.optional(),
});
export type BlocksV1 = z.infer<typeof BlocksV1>;

export const Blocks = z.discriminatedUnion("v", [BlocksV1]);
export type Blocks = z.infer<typeof Blocks>;

export const MessageReactionSummary = z.object({
  emoji: z.string().min(1).max(64),
  count: z.number().int().nonnegative(),
  reactedByMe: z.boolean(),
});
export type MessageReactionSummary = z.infer<typeof MessageReactionSummary>;

export const Message = z.object({
  id: Ulid,
  workspaceId: Ulid,
  channelId: Ulid,
  seq: z.number().int().positive(),
  clientMsgId: z.string().uuid(),
  authorId: Ulid,
  type: z.enum(["text", "system"]),
  text: z.string(),
  blocks: Blocks,
  revision: z.number().int().nonnegative(),
  parentId: Ulid.nullable(),
  isBroadcast: z.boolean(),
  threadReplyCount: z.number().int().nonnegative(),
  threadLastReplyAt: z.string().datetime().nullable(),
  editedAt: z.string().datetime().nullable(),
  deletedAt: z.string().datetime().nullable(),
  createdAt: z.string().datetime(),
  reactions: z.array(MessageReactionSummary).optional(),
});
export type Message = z.infer<typeof Message>;

export const ThreadParticipant = z.object({
  userId: Ulid,
  name: z.string(),
  avatarUrl: z.string().nullable(),
});
export type ThreadParticipant = z.infer<typeof ThreadParticipant>;

export const ThreadSummary = z.object({
  rootMessage: Message,
  latestReply: Message.nullable(),
  channelName: z.string().nullable(),
  following: z.boolean(),
  reason: z.string(),
  lastReadReplySeq: z.number().int().nonnegative(),
  unreadReplyCount: z.number().int().nonnegative(),
  participants: z.array(ThreadParticipant),
});
export type ThreadSummary = z.infer<typeof ThreadSummary>;

export const ThreadSubscriptionStatus = z.object({
  workspaceId: Ulid,
  channelId: Ulid,
  rootMessageId: Ulid,
  following: z.boolean(),
  reason: z.string().nullable(),
  lastReadReplySeq: z.number().int().nonnegative(),
  unreadReplyCount: z.number().int().nonnegative(),
});
export type ThreadSubscriptionStatus = z.infer<typeof ThreadSubscriptionStatus>;

export const SavedItem = z.object({
  id: Ulid,
  workspaceId: Ulid,
  channelId: Ulid,
  channelName: z.string().nullable(),
  authorName: z.string(),
  text: z.string(),
  createdAt: z.string().datetime(),
  savedAt: z.string().datetime(),
});
export type SavedItem = z.infer<typeof SavedItem>;

export const MessageDraft = z.object({
  id: Ulid,
  workspaceId: Ulid,
  channelId: Ulid,
  threadRootMessageId: Ulid.nullable(),
  text: z.string().max(40_000),
  blocks: Blocks.nullable(),
  updatedAt: z.string().datetime(),
});
export type MessageDraft = z.infer<typeof MessageDraft>;

export const NotificationPreferences = z.object({
  workspaceId: Ulid,
  messages: z.enum(["all", "mentions", "off"]),
  calls: z.boolean(),
  tasks: z.boolean(),
  calendar: z.boolean(),
  sound: z.boolean(),
  inAppFlash: z.boolean(),
  updatedAt: z.string().datetime().nullable(),
});
export type NotificationPreferences = z.infer<typeof NotificationPreferences>;

export const Reaction = z.object({
  messageId: Ulid,
  userId: Ulid,
  emoji: z.string().min(1).max(64),
});
export type Reaction = z.infer<typeof Reaction>;

export const MentionTargetType = z.enum(["user", "group", "channel", "here", "everyone"]);
export const MessageMention = z.object({
  messageId: Ulid,
  targetType: MentionTargetType,
  targetId: Ulid.nullable(),
});
export type MessageMention = z.infer<typeof MessageMention>;

export const ChannelMember = z.object({
  channelId: Ulid,
  userId: Ulid,
  role: z.enum(["member", "owner"]),
  joinedAt: z.string().datetime(),
  lastReadSeq: z.number().int().nonnegative(),
  lastReadAt: z.string().datetime().nullable(),
  mentionCount: z.number().int().nonnegative(),
  unreadCount: z.number().int().nonnegative().optional(),
  isMuted: z.boolean(),
  isStarred: z.boolean(),
  isClosed: z.boolean(),
});
export type ChannelMember = z.infer<typeof ChannelMember>;

export const TaskStatus = z.enum(["todo", "in_progress", "done"]);
export type TaskStatus = z.infer<typeof TaskStatus>;

export const Task = z.object({
  id: EntityId,
  workspaceId: EntityId,
  title: z.string().min(1).max(200),
  description: z.string().max(10_000).nullable(),
  status: TaskStatus,
  assigneeUserId: EntityId.nullable(),
  dueAt: z.string().datetime().nullable(),
  createdBy: EntityId,
  channelId: EntityId.nullable(),
  sourceMessageId: EntityId.nullable(),
  completedAt: z.string().datetime().nullable(),
  deletedAt: z.string().datetime().nullable(),
  deletedBy: EntityId.nullable(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});
export type Task = z.infer<typeof Task>;

export const AttendeeStatus = z.enum(["needs_action", "going", "maybe", "declined"]);
export type AttendeeStatus = z.infer<typeof AttendeeStatus>;

export const EventAttendee = z.object({
  userId: EntityId,
  status: AttendeeStatus,
  respondedAt: z.string().datetime().nullable(),
});
export type EventAttendee = z.infer<typeof EventAttendee>;

/**
 * One calendar entry as the client sees it. A recurring series is stored as a
 * single row but read back as concrete occurrences (expanded server-side), so
 * `id` is NOT unique across a response: every occurrence of a series carries
 * the same `id`, and `seriesId` + `occurrenceDate` is the pair that addresses
 * one of them for an occurrence-scoped edit. `isOccurrence` distinguishes an
 * expanded instance from a stored row.
 */
export const CalendarEvent = z.object({
  id: EntityId,
  workspaceId: EntityId,
  title: z.string().min(1).max(200),
  description: z.string().max(10_000).nullable(),
  location: z.string().max(300).nullable(),
  startsAt: z.string().datetime(),
  endsAt: z.string().datetime(),
  allDay: z.boolean(),
  timezone: z.string(),
  recurrenceRule: z.string().nullable(),
  channelId: EntityId.nullable(),
  createdBy: EntityId,
  attendees: z.array(EventAttendee),
  /** The series this occurrence belongs to (equals `id` for a one-off). */
  seriesId: EntityId,
  /** YYYY-MM-DD start date of this occurrence in the event's timezone. */
  occurrenceDate: z.string(),
  isOccurrence: z.boolean(),
  /** True when this occurrence has been detached from its series. */
  isOverride: z.boolean(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});
export type CalendarEvent = z.infer<typeof CalendarEvent>;

export const CallKind = z.enum(["audio", "video", "connect"]);
export type CallKind = z.infer<typeof CallKind>;

export const CallRecordingStatus = z.enum(["idle", "recording", "processing", "ready", "failed"]);
export type CallRecordingStatus = z.infer<typeof CallRecordingStatus>;

export const CallTranscriptStatus = z.enum(["idle", "processing", "ready", "failed"]);
export type CallTranscriptStatus = z.infer<typeof CallTranscriptStatus>;

/** See libs/data/src/schema/calls.ts for why `missed` is a call status and
 * not merely an absence of joins. */
export const CallStatus = z.enum(["ringing", "active", "ended", "missed"]);
export type CallStatus = z.infer<typeof CallStatus>;

export const CallParticipantState = z.enum(["ringing", "joined", "left", "declined", "missed"]);
export type CallParticipantState = z.infer<typeof CallParticipantState>;

export const CallParticipant = z.object({
  userId: EntityId,
  state: CallParticipantState,
  invitedAt: z.string().datetime(),
  joinedAt: z.string().datetime().nullable(),
  leftAt: z.string().datetime().nullable(),
  seenAt: z.string().datetime().nullable(),
});
export type CallParticipant = z.infer<typeof CallParticipant>;

/**
 * One call as the client sees it. Unlike CalendarEvent this is a plain row —
 * there is no expansion, so `id` is unique across a response.
 *
 * `durationSeconds` is served rather than left to the client because the
 * answered/ended pair it comes from has a rule attached: duration is measured
 * from `answeredAt`, so a call nobody picked up is 0 seconds long and not
 * "however long it rang for". It is null while the call is still going.
 */
export const Call = z.object({
  id: EntityId,
  workspaceId: EntityId,
  kind: CallKind,
  status: CallStatus,
  channelId: EntityId.nullable(),
  eventId: EntityId.nullable(),
  title: z.string().max(200).nullable(),
  startedBy: EntityId,
  startedAt: z.string().datetime(),
  answeredAt: z.string().datetime().nullable(),
  endedAt: z.string().datetime().nullable(),
  durationSeconds: z.number().int().nonnegative().nullable(),
  participants: z.array(CallParticipant),
  recordingStatus: CallRecordingStatus.default("idle"),
  recordingObjectKey: z.string().nullable().default(null),
  transcriptStatus: CallTranscriptStatus.default("idle"),
  transcript: z.string().nullable().default(null),
  summary: z.string().nullable().default(null),
});
export type Call = z.infer<typeof Call>;

/**
 * One row of the "people to call" directory: a workspace member plus the call
 * history that decides where they rank. Computed server-side because the
 * ranking needs an aggregate over every call the caller took part in, which is
 * not something a client should have to page through history to reconstruct.
 */
export const CallContact = z.object({
  userId: EntityId,
  callCount: z.number().int().nonnegative(),
  lastCallAt: z.string().datetime().nullable(),
});
export type CallContact = z.infer<typeof CallContact>;

// Per-thread read cursor — new in v3.0, fixes ADR-014 / invariant I5b.
export const ThreadSubscription = z.object({
  userId: Ulid,
  rootMessageId: Ulid,
  channelId: Ulid,
  workspaceId: Ulid,
  lastReadReplySeq: z.number().int().nonnegative(),
  reason: z.enum(["authored", "replied", "mentioned", "manual"]),
  isMuted: z.boolean(),
  subscribedAt: z.string().datetime(),
});
export type ThreadSubscription = z.infer<typeof ThreadSubscription>;
