"use client";

import type {
  Attachment,
  Call,
  CalendarEvent,
  CallContact,
  FileCategory,
  ForwardMessageRequest,
  Message,
  MessageDraft,
  NotificationPreferences,
  SavedItem,
  SignupResponse,
  Task,
  ThreadSubscriptionStatus,
  ThreadSummary,
} from "@slackwsh/contracts";
import { getTokens, setTokens, clearTokens, type StoredTokens } from "./session-store";
import { markSelfAction } from "./self-echo";

/**
 * Tags a mutation's own realtime echo so the notification router can tell your
 * edit from a colleague's — see lib/self-echo.ts. Applied here rather than at
 * each call site because every mutation already passes through this module.
 */
function echo<T extends { id: number }>(kind: "task" | "event" | "call"): (row: T) => T {
  return (row) => {
    markSelfAction(kind, row?.id);
    return row;
  };
}

/**
 * Marks the echo *before* the request goes out, for mutations whose subject id
 * is already known.
 *
 * Marking on the response loses a race: the server publishes to Redis inside
 * the same handler, and Redis → gateway → websocket is a shorter path back to
 * this client than the API's own HTTP response. The realtime echo of your edit
 * routinely arrives first, so anything that waits for the response has already
 * missed it. (Creates are exempt — there is no id until the response, and a row
 * you created can't notify you anyway.)
 */
function withEcho<T>(kind: "task" | "event" | "call", id: string | number, run: () => Promise<T>): Promise<T> {
  markSelfAction(kind, id);
  return run();
}

import { getApiUrl, isLocalHostname } from "./public-env";

export interface ProfileUser {
  id: string | number;
  email: string;
  emailVerifiedAt: string | null;
  name: string;
  username: string;
  avatarUrl: string | null;
  tz: string;
  createdAt: string;
}

export interface WorkspaceStatus {
  manual: { text: string | null; emoji: string | null; expiresAt: string | null; pauseNotifications: boolean };
  effective: {
    text: string | null;
    emoji: string | null;
    expiresAt: string | null;
    source: "manual" | "scheduled" | "connect" | "focus" | "outside_hours" | null;
    dndActive: boolean;
    dndUntil: string | null;
  };
  availabilityMode: "auto" | "away" | "available";
  automatic: {
    connect: boolean;
    focus: boolean;
    outsideWorkingHours: boolean;
    focusModeEnabled: boolean;
    inConnect: boolean;
    workHoursStart: string;
    workHoursEnd: string;
    workingDays: number[];
  };
  schedules: Array<{
    id: number;
    text: string;
    emoji: string;
    startsAt: string;
    endsAt: string;
    pauseNotifications: boolean;
  }>;
}

/** Profile photos are served by the API, while pages are served by Next.js. */
export function assetUrl(path: string | null | undefined): string | null {
  if (!path) return null;
  if (/^(?:https?:|data:|blob:)/i.test(path)) return path;
  return `${getApiUrl()}${path.startsWith("/") ? path : `/${path}`}`;
}

export type ChannelNotifPref = "all" | "mentions" | "none";

export interface ChannelIntegration {
  id: number;
  provider: string;
  label: string | null;
  externalUrl: string | null;
  addedBy: number;
  addedAt: string;
}

export interface ChannelAboutPin {
  messageId: number;
  text: string;
  authorId: number;
  authorName: string;
  pinnedBy: number;
  pinnedByName: string;
  pinnedAt: string;
  createdAt: string;
}

/** Response shape of GET .../channels/:id/about. */
export interface ChannelAbout {
  channel: {
    id: number;
    workspaceId: number;
    type: string;
    name: string | null;
    topic: string | null;
    purpose: string | null;
    createdBy: number;
    isArchived: boolean;
    lastMessageAt: string | null;
    memberCount: number;
  };
  membership: {
    role: string;
    notifPref: ChannelNotifPref;
    isMuted: boolean;
    isStarred: boolean;
    joinedAt: string;
  };
  capabilities: {
    canEditRoom: boolean;
    canPin: boolean;
    canArchive: boolean;
    canLeave: boolean;
    canRemoveMembers: boolean;
    canDeletePermanently: boolean;
  };
  pins: ChannelAboutPin[];
  integrations: ChannelIntegration[];
}

export class ApiError extends Error {
  constructor(
    public status: number,
    public body: unknown,
  ) {
    super(`API error ${status}`);
  }
}

async function raw(path: string, init: RequestInit = {}, auth = false, baseUrl?: string): Promise<Response> {
  const base = baseUrl ?? getApiUrl();
  const headers = new Headers(init.headers);
  // Only declare a JSON body when there actually is one. A bodyless POST
  // (delete/read/revoke/deactivate endpoints) sent with this header makes the
  // server's body parser fail on the empty payload and return 400.
  const isFormData = typeof FormData !== "undefined" && init.body instanceof FormData;
  if (init.body != null && !isFormData) headers.set("content-type", "application/json");
  if (auth) {
    const tokens = await getTokens();
    if (tokens?.accessToken) headers.set("authorization", `Bearer ${tokens.accessToken}`);
  }
  return fetch(`${base}${path}`, { ...init, headers });
}

async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
  const res = await raw(path, init);
  if (!res.ok) throw new ApiError(res.status, await res.json().catch(() => null));
  return res.json() as Promise<T>;
}

/**
 * Single-flight refresh. Parallel refresh calls (RealtimeProvider + page
 * load + authed 401) used to present the same refresh token twice; the
 * second hit "reuse detected" and revoked every session — which looked like
 * "refresh logs me out".
 */
let refreshInFlight: Promise<StoredTokens | null> | null = null;

async function refreshSession(): Promise<StoredTokens | null> {
  if (refreshInFlight) return refreshInFlight;

  refreshInFlight = (async () => {
    const tokens = await getTokens();
    if (!tokens?.refreshToken) return null;
    try {
      const refreshed = await request<{ tokens: StoredTokens }>("/auth/refresh", {
        method: "POST",
        body: JSON.stringify({ refreshToken: tokens.refreshToken }),
      });
      await setTokens(refreshed.tokens);
      return refreshed.tokens;
    } catch (err) {
      // Dead/reused/expired refresh — wipe local session so login/workspaces
      // routing cannot bounce on stale tokens forever.
      if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
        await clearTokens();
      }
      return null;
    } finally {
      refreshInFlight = null;
    }
  })();

  return refreshInFlight;
}

/** Retries once with a refreshed access token on 401 (ADR-002 §7: 15-minute access tokens). */
async function authed(path: string, init: RequestInit = {}): Promise<unknown> {
  let res = await raw(path, init, true);
  if (res.status === 401) {
    const refreshed = await refreshSession();
    if (refreshed) res = await raw(path, init, true);
  }
  if (!res.ok) throw new ApiError(res.status, await res.json().catch(() => null));
  return res.json();
}

function decodeJwtPayload(accessToken: string): Record<string, unknown> | null {
  try {
    return JSON.parse(atob(accessToken.split(".")[1]!.replace(/-/g, "+").replace(/_/g, "/")));
  } catch {
    return null;
  }
}

function jwtExpiresAt(accessToken: string): number | null {
  const payload = decodeJwtPayload(accessToken);
  return typeof payload?.exp === "number" ? payload.exp * 1000 : null;
}

/** For "is this my own message" checks (e.g. skip notifying yourself) —
 * the access JWT's `sub` claim, decoded client-side with no verification
 * since it's only used for a UI convenience, never an authorization
 * decision (the server is what actually enforces identity on every write). */
export async function getCurrentUserId(): Promise<number | null> {
  const tokens = await getTokens();
  if (!tokens) return null;
  const payload = decodeJwtPayload(tokens.accessToken);
  const sub = payload?.sub;
  if (typeof sub !== "string" && typeof sub !== "number") return null;
  const id = Number(sub);
  return Number.isInteger(id) && id > 0 ? id : null;
}

/**
 * `authed()`'s automatic refresh-on-401 works for HTTP because there's
 * always a next request to retry. A socket has no such retry point — an
 * expired token just gets the connection rejected outright (see
 * apps/gateway's ws-auth.ts) — so anything opening a socket must proactively
 * refresh first rather than reactively after a failed connect.
 */
export async function getValidAccessToken(): Promise<string | null> {
  const tokens = await getTokens();
  if (!tokens) return null;

  const expiresAt = jwtExpiresAt(tokens.accessToken);
  const expiringSoon = expiresAt === null || expiresAt - Date.now() < 30_000;
  if (!expiringSoon) return tokens.accessToken;

  const refreshed = await refreshSession();
  return refreshed?.accessToken ?? null;
}

/** Ensure we have a usable access token for routing. Returns false when
 * refresh is rejected or impossible — never "optimistic true" on a dead
 * refresh token (that caused login ↔ workspaces redirect loops). */
export async function ensureSession(): Promise<boolean> {
  const tokens = await getTokens();
  if (!tokens?.refreshToken) return false;
  const access = await getValidAccessToken();
  return Boolean(access);
}

/** Clear local session and send the user to login (stops redirect loops). */
export async function forceLogin(router: { replace: (href: string) => void } | { push: (href: string) => void }) {
  await clearTokens();
  if ("replace" in router) router.replace("/login");
  else router.push("/login");
}

export const api = {
  signup: (input: { email: string; password: string; name: string }) =>
    request<SignupResponse>("/auth/signup", { method: "POST", body: JSON.stringify(input) }),

  login: async (input: { email: string; password: string }) => {
    const session = await request<{ user: unknown; tokens: StoredTokens }>(
      "/auth/login",
      { method: "POST", body: JSON.stringify(input) },
    );
    await setTokens(session.tokens);
    return session;
  },

  logout: async () => {
    const tokens = await getTokens();
    if (tokens?.refreshToken) {
      await request("/auth/logout", { method: "POST", body: JSON.stringify({ refreshToken: tokens.refreshToken }) }).catch(() => null);
    }
    await clearTokens();
  },

  requestPasswordReset: (email: string) =>
    request("/auth/password-reset/request", { method: "POST", body: JSON.stringify({ email }) }),

  profile: () => authed("/profile") as Promise<{ user: ProfileUser }>,

  uploadProfilePhoto: (photo: File) => {
    const body = new FormData();
    body.set("photo", photo);
    return authed("/profile/photo", { method: "POST", body }) as Promise<{ user: ProfileUser }>;
  },

  removeProfilePhoto: () =>
    authed("/profile/photo/remove", { method: "POST" }) as Promise<{ user: ProfileUser }>,

  myWorkspaces: () => authed("/workspaces/mine") as Promise<{ workspaces: Array<{ id: string; name: string; slug: string; role: string }> }>,

  createWorkspace: (input: { name: string; slug: string }) =>
    authed("/workspaces", { method: "POST", body: JSON.stringify(input) }),

  members: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/members`) as Promise<{
      members: Array<{
        member: {
          role: string;
          displayName?: string | null;
          title?: string | null;
          statusText?: string | null;
          statusEmoji?: string | null;
          statusExpiresAt?: string | null;
          statusSource?: string | null;
          dndActive?: boolean;
          dndUntil?: string | null;
          availabilityMode?: "auto" | "away" | "available";
          joinedAt?: string;
          deactivatedAt?: string | null;
          userId?: string | number;
        };
        user: {
          id: string | number;
          name: string;
          email: string;
          username?: string;
          avatarUrl?: string | null;
        };
      }>;
    }>,

  adminOverview: (workspaceId: string) => authed(`/workspaces/${workspaceId}/admin/overview`),

  createInvite: (workspaceId: string, input: { email: string; role?: string; expiresInHours?: number; maxUses?: number }) =>
    authed(`/workspaces/${workspaceId}/invites`, { method: "POST", body: JSON.stringify(input) }) as Promise<{
      id: string;
      token?: string;
      role: string;
      expiresAt: string;
      maxUses: number;
      userExists: boolean;
      notified: boolean;
    }>,

  acceptInvite: (token: string) => authed("/invites/accept", { method: "POST", body: JSON.stringify({ token }) }),

  pendingInvites: () =>
    authed("/invites/pending") as Promise<{
      invites: Array<{
        id: string;
        workspaceId: string;
        workspaceName: string;
        workspaceSlug: string;
        role: string;
        invitedByName: string | null;
        expiresAt: string;
        createdAt: string;
      }>;
    }>,

  acceptInviteById: (inviteId: string) =>
    authed("/invites/accept-by-id", { method: "POST", body: JSON.stringify({ inviteId }) }),

  declineInvite: (inviteId: string) =>
    authed("/invites/decline", { method: "POST", body: JSON.stringify({ inviteId }) }),

  notifications: () =>
    authed("/notifications") as Promise<{
      notifications: Array<{
        id: string;
        type: string;
        title: string;
        body: string;
        payload: Record<string, unknown>;
        readAt: string | null;
        createdAt: string;
      }>;
    }>,

  markNotificationRead: (notificationId: string) =>
    authed(`/notifications/${notificationId}/read`, { method: "POST" }),

  listChannels: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/channels`) as Promise<{
      channels: Array<{
        channel: { id: string; name: string | null; type: string };
        member: unknown;
        dmPeer?: { id: string; name: string; email: string; avatarUrl?: string | null } | null;
        dmPeers?: Array<{ id: string; name: string; email: string; avatarUrl?: string | null }>;
      }>;
    }>,

  createDm: (workspaceId: string, otherUserId: string) =>
    authed(`/workspaces/${workspaceId}/dms`, { method: "POST", body: JSON.stringify({ otherUserId }) }) as Promise<{
      id: string;
      name: string | null;
      type: string;
    }>,

  createGroupDm: (workspaceId: string, memberUserIds: string[]) =>
    authed(`/workspaces/${workspaceId}/dms/group`, { method: "POST", body: JSON.stringify({ memberUserIds }) }) as Promise<{
      id: string;
      name: string | null;
      type: string;
    }>,

  setDmClosed: (workspaceId: string, channelId: string, closed: boolean) =>
    authed(`/workspaces/${workspaceId}/dms/${channelId}/closed`, {
      method: "POST",
      body: JSON.stringify({ closed }),
    }) as Promise<{ closed: boolean }>,

  convertGroupDm: (workspaceId: string, channelId: string, name: string) =>
    authed(`/workspaces/${workspaceId}/dms/${channelId}/convert`, {
      method: "POST",
      body: JSON.stringify({ name }),
    }) as Promise<{ id: string; name: string; type: "private" }>,

  createChannel: (workspaceId: string, input: { name: string; type: "public" | "private"; topic?: string }) =>
    authed(`/workspaces/${workspaceId}/channels`, { method: "POST", body: JSON.stringify(input) }) as Promise<{
      id: string;
      name: string | null;
      type: string;
    }>,

  listChannelMembers: (workspaceId: string, channelId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/members`) as Promise<{
      members: Array<{ member: { role: string }; user: { id: string; name: string; email: string; avatarUrl?: string | null } }>;
    }>,

  addChannelMembers: (workspaceId: string, channelId: string, userIds: string[]) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/members`, {
      method: "POST",
      body: JSON.stringify({ userIds }),
    }) as Promise<{ added: string[] }>,

  removeChannelMember: (workspaceId: string, channelId: string, memberUserId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/members/${memberUserId}/remove`, {
      method: "POST",
    }) as Promise<{ removed: string }>,

  permanentlyDeleteChannel: (workspaceId: string, channelId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/permanent`, { method: "DELETE" }) as Promise<{ deleted: boolean }>,

  /** One-shot hydration for the About-this-room panel (channel row, my own
   * membership + notification prefs, pins, linked apps, what I'm allowed to do). */
  channelAbout: (workspaceId: string, channelId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/about`) as Promise<ChannelAbout>,

  updateChannel: (
    workspaceId: string,
    channelId: string,
    input: { name?: string; topic?: string; purpose?: string },
  ) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}`, {
      method: "POST",
      body: JSON.stringify(input),
    }) as Promise<{ updated: boolean; channel: ChannelAbout["channel"] }>,

  updateChannelPrefs: (
    workspaceId: string,
    channelId: string,
    input: { notifPref?: ChannelNotifPref; isMuted?: boolean; isStarred?: boolean },
  ) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/prefs`, {
      method: "POST",
      body: JSON.stringify(input),
    }) as Promise<{ membership: ChannelAbout["membership"] }>,

  addChannelIntegration: (
    workspaceId: string,
    channelId: string,
    input: { provider: string; label?: string; externalUrl?: string },
  ) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/integrations`, {
      method: "POST",
      body: JSON.stringify(input),
    }) as Promise<{ integration: ChannelIntegration }>,

  removeChannelIntegration: (workspaceId: string, channelId: string, integrationId: string | number) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/integrations/${integrationId}/remove`, {
      method: "POST",
    }) as Promise<{ removed: boolean }>,

  archiveChannel: (workspaceId: string, channelId: string, archived: boolean) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/archive`, {
      method: "POST",
      body: JSON.stringify({ archived }),
    }) as Promise<{ archived: boolean }>,

  leaveChannel: (workspaceId: string, channelId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/leave`, { method: "POST" }) as Promise<{ left: boolean }>,

  pinMessage: (workspaceId: string, channelId: string, messageId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/pin`, {
      method: "POST",
    }) as Promise<{ pinned: boolean }>,

  unpinMessage: (workspaceId: string, channelId: string, messageId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/unpin`, {
      method: "POST",
    }) as Promise<{ unpinned: boolean }>,

  addReaction: (workspaceId: string, channelId: string, messageId: string, emoji: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/reactions`, {
      method: "POST",
      body: JSON.stringify({ emoji }),
    }) as Promise<{ added: boolean }>,

  removeReaction: (workspaceId: string, channelId: string, messageId: string, emoji: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/reactions/remove`, {
      method: "POST",
      body: JSON.stringify({ emoji }),
    }) as Promise<{ removed: boolean }>,

  scrollback: (
    workspaceId: string,
    channelId: string,
    query: { afterSeq?: number; beforeSeq?: number; limit?: number } = {},
  ) => {
    const params = new URLSearchParams();
    if (query.afterSeq !== undefined) params.set("afterSeq", String(query.afterSeq));
    if (query.beforeSeq !== undefined) params.set("beforeSeq", String(query.beforeSeq));
    params.set("limit", String(query.limit ?? 50));
    return authed(`/workspaces/${workspaceId}/channels/${channelId}/messages?${params}`) as Promise<{ messages: unknown[] }>;
  },

  threadMessages: (workspaceId: string, channelId: string, messageId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/thread`) as Promise<{ messages: Message[] }>,

  message: (workspaceId: string, channelId: string, messageId: string | number) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}`) as Promise<{ message: Message }>,

  sendMessage: (
    workspaceId: string,
    channelId: string,
    input: { clientMsgId: string; text: string; blocks?: unknown; parentId?: string | null; isBroadcast?: boolean },
  ) => authed(`/workspaces/${workspaceId}/channels/${channelId}/messages`, { method: "POST", body: JSON.stringify(input) }),

  forwardMessage: (
    workspaceId: string,
    channelId: string,
    messageId: string | number,
    input: ForwardMessageRequest,
  ) => authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/forward`, {
    method: "POST",
    body: JSON.stringify(input),
  }) as Promise<{ messages: Message[] }>,

  editMessage: (
    workspaceId: string,
    channelId: string,
    messageId: string,
    input: { text: string; blocks?: unknown },
  ) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/edit`, {
      method: "POST",
      body: JSON.stringify(input),
    }) as Promise<Message>,

  deleteMessage: (workspaceId: string, channelId: string, messageId: string) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/delete`, {
      method: "POST",
    }) as Promise<{ deleted: boolean }>,

  savedItems: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/state/saved-items`) as Promise<{ items: SavedItem[] }>,

  saveItem: (workspaceId: string, messageId: string | number) =>
    authed(`/workspaces/${workspaceId}/state/saved-items/${messageId}`, { method: "POST" }) as Promise<{ saved: true }>,

  unsaveItem: (workspaceId: string, messageId: string | number) =>
    authed(`/workspaces/${workspaceId}/state/saved-items/${messageId}`, { method: "DELETE" }) as Promise<{ saved: false }>,

  drafts: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/state/drafts`) as Promise<{ drafts: MessageDraft[] }>,

  upsertDraft: (
    workspaceId: string,
    input: { channelId: string | number; threadRootMessageId?: string | number | null; text: string; blocks?: unknown },
  ) =>
    authed(`/workspaces/${workspaceId}/state/drafts`, { method: "POST", body: JSON.stringify(input) }) as Promise<{ draft: MessageDraft | null }>,

  deleteDraft: (
    workspaceId: string,
    input: { channelId: string | number; threadRootMessageId?: string | number | null },
  ) =>
    authed(`/workspaces/${workspaceId}/state/drafts/delete`, { method: "POST", body: JSON.stringify(input) }) as Promise<{ deleted: true }>,

  notificationPreferences: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/state/notification-preferences`) as Promise<{ preferences: NotificationPreferences }>,

  updateNotificationPreferences: (
    workspaceId: string,
    input: Partial<Pick<NotificationPreferences, "messages" | "calls" | "tasks" | "calendar" | "sound" | "inAppFlash">>,
  ) =>
    authed(`/workspaces/${workspaceId}/state/notification-preferences`, {
      method: "POST",
      body: JSON.stringify(input),
    }) as Promise<{ preferences: NotificationPreferences }>,

  uploadFile: async (workspaceId: string, channelId: string, file: File): Promise<Attachment & { url: string }> => {
    const declaredType = file.type || "application/octet-stream";
    const signed = await authed(`/workspaces/${workspaceId}/channels/${channelId}/files/presign`, {
      method: "POST",
      body: JSON.stringify({ name: file.name, type: declaredType, size: file.size }),
    }) as { attachment: Attachment; uploadUrl: string; uploadHeaders: Record<string, string> };
    const fromLoopback = typeof window !== "undefined" && isLocalHostname(window.location.hostname);
    let uploadedOk = false;
    if (!fromLoopback) {
      try {
        const uploaded = await fetch(signed.uploadUrl, { method: "PUT", headers: signed.uploadHeaders, body: file });
        uploadedOk = uploaded.ok;
      } catch {
        uploadedOk = false;
      }
    }
    if (!uploadedOk) {
      const body = new FormData();
      body.append("file", file, file.name);
      await authed(`/workspaces/${workspaceId}/channels/${channelId}/files/${signed.attachment.id}/bytes`, {
        method: "POST",
        body,
      });
    }
    const completed = await authed(
      `/workspaces/${workspaceId}/channels/${channelId}/files/${signed.attachment.id}/complete`,
      { method: "POST" },
    ) as { attachment: Attachment };
    return {
      ...completed.attachment,
      url: `/workspaces/${workspaceId}/channels/${channelId}/files/${completed.attachment.id}/access`,
    };
  },

  fileAccess: (workspaceId: string, channelId: string | number, fileId: string | number) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/files/${fileId}/access`) as Promise<{ url: string; expiresAt: string }>,

  resolveFileUrl: (path: string) => authed(path) as Promise<{ url: string; expiresAt: string }>,

  downloadFile: async (path: string): Promise<Blob> => {
    if (!path.endsWith("/access")) {
      let legacy = await raw(path, {}, true);
      if (legacy.status === 401 && await refreshSession()) legacy = await raw(path, {}, true);
      if (!legacy.ok) throw new ApiError(legacy.status, await legacy.json().catch(() => null));
      return legacy.blob();
    }
    const access = await authed(path) as { url: string };
    const res = await fetch(access.url);
    if (!res.ok) throw new ApiError(res.status, { message: "signed file download failed" });
    return res.blob();
  },

  deleteFile: (workspaceId: string, channelId: string | number, fileId: string | number) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/files/${fileId}`, { method: "DELETE" }) as Promise<{ deleted: boolean }>,

  browseFiles: (
    workspaceId: string,
    query: { q?: string; category?: FileCategory; channelId?: string | number; cursor?: string | number; limit?: number } = {},
  ) => {
    const params = new URLSearchParams();
    if (query.q) params.set("q", query.q);
    if (query.category) params.set("category", query.category);
    if (query.channelId != null) params.set("channelId", String(query.channelId));
    if (query.cursor != null) params.set("cursor", String(query.cursor));
    params.set("limit", String(query.limit ?? 30));
    return authed(`/workspaces/${workspaceId}/files?${params}`) as Promise<{
      files: Array<Attachment & { channelName: string | null; channelType: string; uploaderName: string; messageText: string | null }>;
      nextCursor: number | null;
    }>;
  },

  markChannelRead: (workspaceId: string, channelId: string, seq: number) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/read`, {
      method: "POST",
      body: JSON.stringify({ seq }),
    }) as Promise<{ updated: boolean }>,

  markMessageUnread: (workspaceId: string, channelId: string, messageId: string | number) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/unread`, {
      method: "POST",
    }) as Promise<{ lastReadSeq: number; unreadCount: number }>,

  markThreadRead: (workspaceId: string, channelId: string, messageId: string, seq: number) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/thread-read`, {
      method: "POST",
      body: JSON.stringify({ seq }),
    }) as Promise<{ updated: boolean; status: ThreadSubscriptionStatus }>,

  threadStatus: (workspaceId: string, channelId: string, messageId: string | number) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/thread-status`) as Promise<{
      status: ThreadSubscriptionStatus;
    }>,

  setThreadFollowing: (
    workspaceId: string,
    channelId: string,
    messageId: string | number,
    following: boolean,
  ) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/thread-following`, {
      method: "POST",
      body: JSON.stringify({ following }),
    }) as Promise<{ status: ThreadSubscriptionStatus }>,

  followedThreads: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/threads`) as Promise<{ threads: ThreadSummary[] }>,

  /** Presence snapshot — served by apps/api from the same Redis keys the
   * gateway writes. Goes through NEXT_PUBLIC_API_URL (/api/…) so Apache only
   * needs the Nest proxy, not a separate gateway HTTP path. Live updates still
   * arrive over Socket.IO (NEXT_PUBLIC_GATEWAY_URL). */
  presenceSnapshot: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/presence`) as Promise<{
      presence: Record<string, { status: string; lastSeen: string }>;
    }>,

  search: (workspaceId: string, q: string) =>
    authed(`/workspaces/${workspaceId}/search?q=${encodeURIComponent(q)}`) as Promise<{
      results: Array<{ message: Message; channelName: string | null; authorName: string }>;
    }>,

  myStatus: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/me/status`) as Promise<WorkspaceStatus>,

  setStatus: (
    workspaceId: string,
    input: { text: string; emoji?: string; expiresAt?: string | null; pauseNotifications?: boolean },
  ) =>
    authed(`/workspaces/${workspaceId}/me/status`, { method: "POST", body: JSON.stringify(input) }) as Promise<WorkspaceStatus>,

  setAvailability: (workspaceId: string, mode: "auto" | "away" | "available") =>
    authed(`/workspaces/${workspaceId}/me/availability`, { method: "POST", body: JSON.stringify({ mode }) }) as Promise<WorkspaceStatus>,

  setStatusContext: (workspaceId: string, input: { focusModeEnabled?: boolean; inConnect?: boolean }) =>
    authed(`/workspaces/${workspaceId}/me/status/context`, { method: "POST", body: JSON.stringify(input) }) as Promise<WorkspaceStatus>,

  updateAutomaticStatus: (
    workspaceId: string,
    input: {
      connect: boolean;
      focus: boolean;
      outsideWorkingHours: boolean;
      workHoursStart: string;
      workHoursEnd: string;
      workingDays: number[];
    },
  ) =>
    authed(`/workspaces/${workspaceId}/me/status/automatic`, { method: "POST", body: JSON.stringify(input) }) as Promise<WorkspaceStatus>,

  createScheduledStatus: (
    workspaceId: string,
    input: { text: string; emoji: string; startsAt: string; endsAt: string; pauseNotifications: boolean },
  ) =>
    authed(`/workspaces/${workspaceId}/me/status/scheduled`, { method: "POST", body: JSON.stringify(input) }) as Promise<WorkspaceStatus>,

  updateScheduledStatus: (
    workspaceId: string,
    scheduleId: string | number,
    input: { text: string; emoji: string; startsAt: string; endsAt: string; pauseNotifications: boolean },
  ) =>
    authed(`/workspaces/${workspaceId}/me/status/scheduled/${scheduleId}`, { method: "POST", body: JSON.stringify(input) }) as Promise<WorkspaceStatus>,

  deleteScheduledStatus: (workspaceId: string, scheduleId: string | number) =>
    authed(`/workspaces/${workspaceId}/me/status/scheduled/${scheduleId}/delete`, { method: "POST" }) as Promise<WorkspaceStatus>,

  mentionActivity: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/search/mentions`) as Promise<{
      results: Array<{ message: Message; channelName: string | null; authorName: string; mentionType: string; readAt: string | null }>;
    }>,

  updateActivityRead: (workspaceId: string, messageIds: Array<string | number>, read: boolean) =>
    authed(`/workspaces/${workspaceId}/search/activity/read`, {
      method: "POST",
      body: JSON.stringify({ messageIds, read }),
    }) as Promise<{ messageIds: number[]; read: boolean }>,

  unreads: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/search/unreads`) as Promise<{
      results: Array<{
        message: Message;
        channelName: string | null;
        channelType: string;
        authorName: string;
      }>;
    }>,

  verifyEmail: (token: string) => request("/auth/verify-email", { method: "POST", body: JSON.stringify({ token }) }),

  confirmPasswordReset: (token: string, newPassword: string) =>
    request("/auth/password-reset/confirm", { method: "POST", body: JSON.stringify({ token, newPassword }) }),

  listDevices: () =>
    authed("/auth/devices") as Promise<{
      sessions: Array<{ id: string; deviceLabel: string | null; ip: string | null; userAgent: string | null; createdAt: string; expiresAt: string }>;
    }>,

  revokeDevice: (sessionId: string) => authed(`/auth/devices/${sessionId}/revoke`, { method: "POST" }),

  changeMemberRole: (workspaceId: string, targetUserId: string, role: string) =>
    authed(`/workspaces/${workspaceId}/members/${targetUserId}/role`, { method: "POST", body: JSON.stringify({ role }) }),

  deactivateMember: (workspaceId: string, targetUserId: string) =>
    authed(`/workspaces/${workspaceId}/members/${targetUserId}/deactivate`, { method: "POST" }),

  reactivateMember: (workspaceId: string, targetUserId: string) =>
    authed(`/workspaces/${workspaceId}/members/${targetUserId}/reactivate`, { method: "POST" }),

  updateWorkspaceSettings: (workspaceId: string, input: Record<string, unknown>) =>
    authed(`/workspaces/${workspaceId}/settings`, { method: "POST", body: JSON.stringify(input) }),

  listTasks: (
    workspaceId: string,
    filters: {
      status?: string;
      assigneeUserId?: string;
      channelId?: string;
      mine?: boolean;
      deleted?: boolean;
      dueBefore?: string;
      dueAfter?: string;
      limit?: number;
    } = {},
  ) => {
    const params = new URLSearchParams();
    if (filters.status) params.set("status", filters.status);
    if (filters.assigneeUserId) params.set("assigneeUserId", filters.assigneeUserId);
    if (filters.channelId) params.set("channelId", filters.channelId);
    if (filters.mine) params.set("mine", "true");
    if (filters.deleted) params.set("deleted", "true");
    if (filters.dueBefore) params.set("dueBefore", filters.dueBefore);
    if (filters.dueAfter) params.set("dueAfter", filters.dueAfter);
    if (filters.limit != null) params.set("limit", String(filters.limit));
    return authed(`/workspaces/${workspaceId}/tasks?${params}`) as Promise<{ tasks: Task[] }>;
  },

  unreadAssignedTasks: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/tasks/unread-assigned`) as Promise<{ unread: number }>,

  /** Opening the Tasks page acknowledges every unseen assignment in this workspace. */
  markAssignedTasksSeen: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/tasks/mark-assigned-seen`, { method: "POST" }) as Promise<{ seen: number }>,

  createTask: (
    workspaceId: string,
    input: {
      title: string;
      description?: string;
      assigneeUserId?: string | null;
      dueAt?: string | null;
      channelId?: string | null;
      status?: string;
    },
  ) =>
    (authed(`/workspaces/${workspaceId}/tasks`, { method: "POST", body: JSON.stringify(input) }) as Promise<Task>).then(
      echo<Task>("task"),
    ),

  updateTask: (
    workspaceId: string,
    taskId: string,
    input: { title?: string; description?: string | null; dueAt?: string | null; channelId?: string | null },
  ) =>
    withEcho("task", taskId, () =>
      authed(`/workspaces/${workspaceId}/tasks/${taskId}/update`, {
        method: "POST",
        body: JSON.stringify(input),
      }) as Promise<Task>,
    ),

  assignTask: (workspaceId: string, taskId: string, assigneeUserId: string | null) =>
    withEcho("task", taskId, () =>
      authed(`/workspaces/${workspaceId}/tasks/${taskId}/assign`, {
        method: "POST",
        body: JSON.stringify({ assigneeUserId }),
      }) as Promise<Task>,
    ),

  updateTaskStatus: (workspaceId: string, taskId: string, status: string) =>
    withEcho("task", taskId, () =>
      authed(`/workspaces/${workspaceId}/tasks/${taskId}/status`, {
        method: "POST",
        body: JSON.stringify({ status }),
      }) as Promise<Task>,
    ),

  deleteTask: (workspaceId: string, taskId: string) =>
    withEcho("task", taskId, () =>
      authed(`/workspaces/${workspaceId}/tasks/${taskId}/delete`, { method: "POST" }) as Promise<{ deleted: boolean }>,
    ),

  restoreTask: (workspaceId: string, taskId: string) =>
    withEcho("task", taskId, () =>
      authed(`/workspaces/${workspaceId}/tasks/${taskId}/restore`, { method: "POST" }) as Promise<Task>,
    ),

  /** Windowed read — the API requires from/to (recurring series are expanded
   * per request), and returns concrete occurrences, so `id` repeats across a
   * series. Address one occurrence with seriesId + occurrenceDate. */
  listEvents: (
    workspaceId: string,
    range: { from: string; to: string; channelId?: string; mine?: boolean },
  ) => {
    const params = new URLSearchParams({ from: range.from, to: range.to });
    if (range.channelId) params.set("channelId", range.channelId);
    if (range.mine) params.set("mine", "true");
    return authed(`/workspaces/${workspaceId}/events?${params}`) as Promise<{ events: CalendarEvent[] }>;
  },

  createEvent: (
    workspaceId: string,
    input: {
      title: string;
      description?: string | null;
      location?: string | null;
      startsAt: string;
      endsAt: string;
      allDay?: boolean;
      timezone?: string;
      recurrenceRule?: string | null;
      channelId?: string | null;
      attendeeUserIds?: Array<string | number>;
    },
  ) =>
    (
      authed(`/workspaces/${workspaceId}/events`, { method: "POST", body: JSON.stringify(input) }) as Promise<CalendarEvent>
    ).then(echo<CalendarEvent>("event")),

  updateEvent: (
    workspaceId: string,
    eventId: string,
    input: {
      scope?: "series" | "occurrence";
      occurrenceDate?: string;
      title?: string;
      description?: string | null;
      location?: string | null;
      startsAt?: string;
      endsAt?: string;
      allDay?: boolean;
      timezone?: string;
      recurrenceRule?: string | null;
      channelId?: string | null;
    },
  ) =>
    withEcho("event", eventId, () =>
      authed(`/workspaces/${workspaceId}/events/${eventId}/update`, {
        method: "POST",
        body: JSON.stringify(input),
      }) as Promise<CalendarEvent>,
    ),

  inviteToEvent: (workspaceId: string, eventId: string, attendeeUserIds: Array<string | number>) =>
    withEcho("event", eventId, () =>
      authed(`/workspaces/${workspaceId}/events/${eventId}/invite`, {
        method: "POST",
        body: JSON.stringify({ attendeeUserIds }),
      }) as Promise<CalendarEvent>,
    ),

  rsvpEvent: (workspaceId: string, eventId: string, status: "going" | "maybe" | "declined" | "needs_action") =>
    withEcho("event", eventId, () =>
      authed(`/workspaces/${workspaceId}/events/${eventId}/rsvp`, {
        method: "POST",
        body: JSON.stringify({ status }),
      }) as Promise<CalendarEvent>,
    ),

  deleteEvent: (
    workspaceId: string,
    eventId: string,
    options: { scope?: "series" | "occurrence"; occurrenceDate?: string } = {},
  ) =>
    authed(`/workspaces/${workspaceId}/events/${eventId}/delete`, {
      method: "POST",
      body: JSON.stringify(options),
    }) as Promise<{ deleted: boolean }>,

  createTaskFromMessage: (
    workspaceId: string,
    channelId: string,
    messageId: string,
    input: { title?: string; description?: string; assigneeUserId?: string | null; dueAt?: string | null },
  ) =>
    authed(`/workspaces/${workspaceId}/channels/${channelId}/messages/${messageId}/create-task`, {
      method: "POST",
      body: JSON.stringify(input),
    }) as Promise<Task>,

  /** Paged, newest-first, and always scoped server-side to calls you were on —
   * unlike listEvents there is no date window, because a call is an instant
   * rather than a range. Page with `before` (a keyset cursor), not an offset. */
  listCalls: (
    workspaceId: string,
    filters: {
      active?: boolean;
      missed?: boolean;
      direction?: "incoming" | "outgoing";
      withUserId?: string;
      channelId?: string;
      limit?: number;
      before?: string;
    } = {},
  ) => {
    const params = new URLSearchParams();
    if (filters.active) params.set("active", "true");
    if (filters.missed) params.set("missed", "true");
    if (filters.direction) params.set("direction", filters.direction);
    if (filters.withUserId) params.set("withUserId", filters.withUserId);
    if (filters.channelId) params.set("channelId", filters.channelId);
    if (filters.limit) params.set("limit", String(filters.limit));
    if (filters.before) params.set("before", filters.before);
    const query = params.toString();
    return authed(`/workspaces/${workspaceId}/calls${query ? `?${query}` : ""}`) as Promise<{ calls: Call[] }>;
  },

  startCall: (
    workspaceId: string,
    input: {
      kind?: "audio" | "video";
      inviteeUserIds: Array<string | number>;
      channelId?: string | null;
      eventId?: string | null;
      title?: string | null;
    },
  ) => authed(`/workspaces/${workspaceId}/calls`, { method: "POST", body: JSON.stringify(input) }) as Promise<Call>,

  getCall: (workspaceId: string, callId: string) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}`) as Promise<Call>,

  joinCall: (workspaceId: string, callId: string) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/join`, { method: "POST" }) as Promise<Call>,

  declineCall: (workspaceId: string, callId: string) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/decline`, { method: "POST" }) as Promise<Call>,

  leaveCall: (workspaceId: string, callId: string) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/leave`, { method: "POST" }) as Promise<Call>,

  endCall: (workspaceId: string, callId: string) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/end`, { method: "POST" }) as Promise<Call>,

  inviteToCall: (workspaceId: string, callId: string, inviteeUserIds: Array<string | number>) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/invite`, {
      method: "POST",
      body: JSON.stringify({ inviteeUserIds }),
    }) as Promise<Call>,

  startConnect: (workspaceId: string, channelId: string | number) =>
    authed(`/workspaces/${workspaceId}/calls/connect`, {
      method: "POST",
      body: JSON.stringify({ channelId }),
    }) as Promise<Call>,

  callMediaToken: (workspaceId: string, callId: string | number) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/media-token`, { method: "POST" }) as Promise<{
      configured: boolean;
      url: string | null;
      token: string | null;
      roomName: string | null;
      identity: string | null;
    }>,

  startCallRecording: (workspaceId: string, callId: string | number) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/recording/start`, { method: "POST" }) as Promise<Call>,

  stopCallRecording: (workspaceId: string, callId: string | number) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/recording/stop`, { method: "POST" }) as Promise<Call>,

  dialPstn: (workspaceId: string, callId: string | number, e164: string) =>
    authed(`/workspaces/${workspaceId}/calls/${callId}/dial`, {
      method: "POST",
      body: JSON.stringify({ e164 }),
    }) as Promise<Call>,

  /** Who you actually talk to, ranked by how often and how recently. Members
   * with no shared history are absent — the caller unions this with the member
   * roster so a new user still has someone to call. */
  callContacts: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/calls/contacts`) as Promise<{ contacts: CallContact[] }>,

  unseenMissedCalls: (workspaceId: string) =>
    authed(`/workspaces/${workspaceId}/calls/unseen`) as Promise<{ unseen: number }>,

  /** Omit `callIds` to acknowledge everything unseen — the "opened the calls
   * page" case. */
  markCallsSeen: (workspaceId: string, callIds?: Array<string | number>) =>
    authed(`/workspaces/${workspaceId}/calls/seen`, {
      method: "POST",
      ...(callIds ? { body: JSON.stringify({ callIds }) } : {}),
    }) as Promise<{ seen: number }>,

  listIntegrations: () =>
    authed("/integrations") as Promise<{
      integrations: Array<{
        provider: "google_calendar" | "outlook_calendar" | "zoom" | "openai";
        connected: boolean;
        accountEmail?: string | null;
        accountLabel?: string | null;
        secretLast4?: string | null;
        configured: boolean;
      }>;
    }>,

  saveOpenAiKey: (apiKey: string) =>
    authed("/integrations/openai", { method: "POST", body: JSON.stringify({ apiKey }) }),

  disconnectIntegration: (provider: "google_calendar" | "outlook_calendar" | "zoom" | "openai") =>
    authed("/integrations/disconnect", { method: "POST", body: JSON.stringify({ provider }) }),

  startIntegrationOAuth: (provider: "google_calendar" | "outlook_calendar" | "zoom", returnTo?: string) =>
    authed("/integrations/oauth/start", {
      method: "POST",
      body: JSON.stringify({ provider, returnTo }),
    }) as Promise<{ url: string }>,

  rewriteMessage: (input: {
    text: string;
    tone?: "professional" | "friendly" | "concise" | "formal" | "casual";
    mode?: "rewrite" | "proofread";
  }) =>
    authed("/integrations/rewrite", { method: "POST", body: JSON.stringify(input) }) as Promise<{ text: string }>,

  listExternalCalendarEvents: (range: { from: string; to: string }) => {
    const params = new URLSearchParams({ from: range.from, to: range.to });
    return authed(`/integrations/calendar/external?${params}`) as Promise<{
      events: Array<{
        id: string;
        provider: "google_calendar" | "outlook_calendar" | "zoom";
        title: string;
        startsAt: string;
        endsAt: string;
        allDay: boolean;
        location: string | null;
        htmlLink: string | null;
      }>;
    }>;
  },

  createZoomMeeting: (input: {
    topic: string;
    startsAt: string;
    durationMinutes?: number;
    timezone?: string;
  }) =>
    authed("/integrations/zoom/meetings", { method: "POST", body: JSON.stringify(input) }) as Promise<{
      joinUrl: string;
      meetingId: string | number;
      startUrl?: string;
    }>,

  vapidPublicKey: () => authed("/push/vapid-public-key") as Promise<{ publicKey: string }>,

  pushSubscribe: (input: { endpoint: string; keys: { p256dh: string; auth: string }; userAgent?: string }) =>
    authed("/push/subscribe", { method: "POST", body: JSON.stringify(input) }),

  pushUnsubscribe: (endpoint: string) =>
    authed("/push/subscribe", { method: "DELETE", body: JSON.stringify({ endpoint }) }),
};
