import { z } from "zod";
import { CallKind, EntityId } from "./entities";

/** Page size cap for GET /workspaces/:workspaceId/calls. History is unbounded
 * in principle, so the read is paged rather than windowed by date the way the
 * calendar's is — a call has one instant, not a range to overlap. */
export const MAX_CALL_PAGE_SIZE = 100;

/**
 * How many peers one call may hold.
 *
 * Phase 6 moved media onto LiveKit (an SFU), so the old mesh bandwidth
 * argument no longer caps this. Twelve is the Phase 6 exit criterion
 * (ARCHITECTURE.md §8) and is still enforced server-side so a client cannot
 * bypass it.
 */
export const MAX_CALL_PARTICIPANTS = 12;

export const StartCallRequest = z
  .object({
    kind: CallKind.default("audio"),
    /** Who to ring. The caller is added as a joined participant implicitly and
     * does not need to be listed. */
    inviteeUserIds: z.array(EntityId).max(MAX_CALL_PARTICIPANTS),
    /** Optional origin context — the DM or room the call was started from. */
    channelId: EntityId.nullable().optional(),
    /** Set when this call is the realisation of a scheduled calendar event. */
    eventId: EntityId.nullable().optional(),
    title: z.string().min(1).max(200).nullable().optional(),
  })
  .refine((v) => v.kind === "connect" || v.inviteeUserIds.length > 0, {
    message: "a call needs at least one other person",
    path: ["inviteeUserIds"],
  });
export type StartCallRequest = z.infer<typeof StartCallRequest>;

/** Join-or-start the persistent audio room for a channel (feature 8.1). */
export const StartConnectRequest = z.object({
  channelId: EntityId,
  /** Connect is audio by default; video is opt-in once you are in the room. */
  video: z.boolean().optional(),
});
export type StartConnectRequest = z.infer<typeof StartConnectRequest>;

export const CallMediaTokenResponse = z.object({
  /** False when LiveKit is not configured — the client falls back to mesh. */
  configured: z.boolean(),
  url: z.string().nullable(),
  token: z.string().nullable(),
  roomName: z.string().nullable(),
  identity: z.string().nullable(),
});
export type CallMediaTokenResponse = z.infer<typeof CallMediaTokenResponse>;

export const DialPstnRequest = z.object({
  e164: z.string().regex(/^\+[1-9]\d{7,14}$/, "E.164 phone number required"),
});
export type DialPstnRequest = z.infer<typeof DialPstnRequest>;

/** Which side of the call the caller was on. Not stored — derived per-reader
 * from `startedBy`, since one row is outgoing for the caller and incoming for
 * everyone else. */
export const CallDirection = z.enum(["incoming", "outgoing"]);
export type CallDirection = z.infer<typeof CallDirection>;

export const ListCallsQuery = z.object({
  /** Only calls that are still live — the active-calls strip's read. */
  active: z
    .enum(["true", "false", "1", "0"])
    .transform((v) => v === "true" || v === "1")
    .optional(),
  /** Only calls this user never answered. */
  missed: z
    .enum(["true", "false", "1", "0"])
    .transform((v) => v === "true" || v === "1")
    .optional(),
  direction: CallDirection.optional(),
  /** Restrict to calls involving this person, for a per-contact history. */
  withUserId: EntityId.optional(),
  /** Restrict to calls started from this room or DM. */
  channelId: EntityId.optional(),
  limit: z.coerce.number().int().positive().max(MAX_CALL_PAGE_SIZE).default(50),
  /** Keyset cursor: return calls started strictly before this instant. Chosen
   * over an offset because history grows at the head, where an offset would
   * silently skip or repeat rows between pages. */
  before: z.string().datetime().optional(),
});
export type ListCallsQuery = z.infer<typeof ListCallsQuery>;

/** Adds people to a call already in progress. Replace-wholesale would be
 * wrong here (unlike event invites): the existing participants are *on* the
 * call, so their rows are not the caller's to rewrite. */
export const InviteToCallRequest = z.object({
  inviteeUserIds: z.array(EntityId).min(1).max(MAX_CALL_PARTICIPANTS),
});
export type InviteToCallRequest = z.infer<typeof InviteToCallRequest>;

/** Marks calls as acknowledged in the caller's own history, clearing the
 * missed badge. Omitting `callIds` acknowledges every unseen call — the
 * "opened the calls page" case. */
export const MarkCallsSeenRequest = z.object({
  callIds: z.array(EntityId).max(MAX_CALL_PAGE_SIZE).optional(),
});
export type MarkCallsSeenRequest = z.infer<typeof MarkCallsSeenRequest>;

/**
 * WebRTC signalling relay, carried over the socket rather than HTTP — offers,
 * answers and ICE candidates are a fast back-and-forth between two specific
 * clients and there is nothing to persist.
 *
 * `data` is deliberately opaque: it is an SDP blob or an ICE candidate whose
 * shape belongs to the browser's WebRTC implementation, not to this protocol.
 * The gateway authorises the relay (both ends must be participants of the
 * named call) and forwards the payload without inspecting it.
 */
export const CallSignalKind = z.enum(["offer", "answer", "ice", "ready"]);
export type CallSignalKind = z.infer<typeof CallSignalKind>;

export const CallSignalRequest = z.object({
  /** Carried because the gateway has no ambient workspace scope — every
   * tenant-scoped read needs it, and it is checked against the call's own
   * workspace rather than trusted. */
  workspaceId: EntityId,
  callId: EntityId,
  toUserId: EntityId,
  signal: CallSignalKind,
  data: z.unknown(),
});
export type CallSignalRequest = z.infer<typeof CallSignalRequest>;
