/**
 * Shared Slack Web API helpers.
 *
 * Thin, reusable wrappers around the raw `@slack/web-api` client so the bot
 * adapter, the polling/user adapters, and the in-process Slack MCP tools all
 * share one implementation for the handful of calls they each make.
 *
 * These helpers deliberately do NOT translate Markdown -> Slack mrkdwn: that
 * is a per-caller policy (the channel adapters convert; the MCP tools pass the
 * agent's text through). Convert before calling if you need it.
 */

import type { WebClient } from "@slack/web-api";

/** A single message in a channel/thread, normalized for display. */
export interface SlackTranscriptMessage {
  ts: string;
  user: string;
  text: string;
  reactions?: Array<{ name: string; count: number }>;
}

/** A Slack conversation (channel, DM, group), normalized. */
export interface SlackChannelSummary {
  id: string;
  name: string;
  purpose?: string;
  isMember?: boolean;
  isPrivate?: boolean;
  isIm?: boolean;
}

const sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));

// ---------------------------------------------------------------------------
// Messages
// ---------------------------------------------------------------------------

/**
 * Post a message to a channel or thread. Returns the message `ts` (used as the
 * id for later edits), or `undefined` if Slack didn't return one.
 */
export async function postSlackMessage(
  client: WebClient,
  channel: string,
  text: string,
  threadTs?: string,
): Promise<string | undefined> {
  const result = await client.chat.postMessage({
    channel,
    text,
    thread_ts: threadTs,
  });
  return result.ts;
}

/** Edit an existing message in place. */
export async function updateSlackMessage(
  client: WebClient,
  channel: string,
  ts: string,
  text: string,
): Promise<void> {
  await client.chat.update({ channel, ts, text });
}

// ---------------------------------------------------------------------------
// Native assistant status ("is thinking…")
// ---------------------------------------------------------------------------

/**
 * Set the native loading status on an assistant thread via
 * `assistant.threads.setStatus` -- the indicator Slack renders below the
 * composer while an AI app builds its reply. Pass an empty string to clear it.
 *
 * Requires the app to have the Agents & AI Apps feature enabled, the
 * `assistant:write` scope, and a real assistant thread. Throws otherwise --
 * callers should treat a rejection as "not available" and fall back to a
 * placeholder message.
 */
export async function setAssistantStatus(
  client: WebClient,
  channel: string,
  threadTs: string,
  status: string,
): Promise<void> {
  await client.assistant.threads.setStatus({
    channel_id: channel,
    thread_ts: threadTs,
    status,
  });
}

// ---------------------------------------------------------------------------
// Reading
// ---------------------------------------------------------------------------

/** Normalize raw Slack message objects into {@link SlackTranscriptMessage}. */
export function toTranscriptMessages(
  raw: Array<Record<string, unknown>>,
): SlackTranscriptMessage[] {
  return raw.map((m) => {
    const reactions = m.reactions as Array<{ name?: string; count?: number }> | undefined;
    return {
      ts: (m.ts as string) ?? "",
      user: (m.user as string) ?? (m.bot_id as string) ?? (m.username as string) ?? "unknown",
      text: (m.text as string) ?? "",
      ...(reactions?.length
        ? {
            reactions: reactions.map((r) => ({ name: r.name ?? "", count: r.count ?? 0 })),
          }
        : {}),
    };
  });
}

/**
 * Fetch a thread's replies (parent + all replies up to `limit`) and return
 * them normalized. Pair with {@link formatSlackTranscript} for display.
 */
export async function getSlackThreadTranscript(
  client: WebClient,
  channel: string,
  threadTs: string,
  opts: { limit?: number } = {},
): Promise<SlackTranscriptMessage[]> {
  const result = await client.conversations.replies({
    channel,
    ts: threadTs,
    limit: opts.limit ?? 50,
  });
  return toTranscriptMessages((result.messages ?? []) as Array<Record<string, unknown>>);
}

/**
 * Render normalized messages as a compact plaintext transcript, one line per
 * message: `[ts] <user> text  :emoji: (n)`. Supply `resolveName` to swap raw
 * user ids for display names.
 */
export function formatSlackTranscript(
  messages: SlackTranscriptMessage[],
  opts: { resolveName?: (userId: string) => string } = {},
): string {
  return messages
    .map((m) => {
      const who = opts.resolveName ? opts.resolveName(m.user) : m.user;
      let line = `[${m.ts}] <${who}> ${m.text}`;
      if (m.reactions?.length) {
        line += "  " + m.reactions.map((r) => `:${r.name}: (${r.count})`).join(" ");
      }
      return line;
    })
    .join("\n");
}

// ---------------------------------------------------------------------------
// Channels
// ---------------------------------------------------------------------------

/**
 * List conversations the token can see, transparently following cursor
 * pagination. `onlyMember` keeps just the channels the token is a member of;
 * `interCallDelayMs` throttles paged requests to respect rate limits.
 */
export async function listSlackChannels(
  client: WebClient,
  opts: {
    types?: string;
    limit?: number;
    excludeArchived?: boolean;
    onlyMember?: boolean;
    interCallDelayMs?: number;
  } = {},
): Promise<SlackChannelSummary[]> {
  const channels: SlackChannelSummary[] = [];
  const max = opts.limit ?? Number.POSITIVE_INFINITY;
  const pageSize = Math.min(Number.isFinite(max) ? max : 200, 200);
  let cursor: string | undefined;

  do {
    const result = await client.conversations.list({
      types: opts.types ?? "public_channel",
      limit: pageSize,
      cursor,
      exclude_archived: opts.excludeArchived ?? true,
    });

    for (const ch of (result.channels ?? []) as Array<Record<string, unknown>>) {
      const id = ch.id as string | undefined;
      if (!id) continue;
      if (opts.onlyMember && !ch.is_member) continue;
      const purpose = (ch.purpose as { value?: string } | undefined)?.value;
      channels.push({
        id,
        name: (ch.name as string) ?? "",
        ...(purpose ? { purpose } : {}),
        isMember: Boolean(ch.is_member),
        isPrivate: Boolean(ch.is_private),
        isIm: Boolean(ch.is_im),
      });
      if (channels.length >= max) return channels;
    }

    cursor = (result.response_metadata?.next_cursor as string | undefined) || undefined;
    if (cursor && opts.interCallDelayMs) await sleep(opts.interCallDelayMs);
  } while (cursor);

  return channels;
}
