import type { Block, KnownBlock, WebClient } from "@slack/web-api";
import { loadConfig } from "openclaw/plugin-sdk/config-runtime";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { resolveSlackAccount } from "./accounts.js";
import { buildSlackBlocksFallbackText } from "./blocks-fallback.js";
import { validateSlackBlocksArray } from "./blocks-input.js";
import { createSlackWebClient, createSlackWriteClient } from "./client.js";
import { resolveSlackMedia } from "./monitor/media.js";
import type { SlackMediaResult } from "./monitor/media.js";
import { sendMessageSlack } from "./send.js";
import { resolveSlackBotToken } from "./token.js";

export type SlackActionClientOpts = {
  accountId?: string;
  token?: string;
  client?: WebClient;
};

export type SlackMessageSummary = {
  ts?: string;
  text?: string;
  user?: string;
  thread_ts?: string;
  reply_count?: number;
  reactions?: Array<{
    name?: string;
    count?: number;
    users?: string[];
  }>;
  /** File attachments on this message. Present when the message has files. */
  files?: Array<{
    id?: string;
    name?: string;
    mimetype?: string;
  }>;
};

export type SlackPin = {
  type?: string;
  message?: { ts?: string; text?: string };
  file?: { id?: string; name?: string };
};

function resolveToken(explicit?: string, accountId?: string) {
  const cfg = loadConfig();
  const account = resolveSlackAccount({ cfg, accountId });
  const token = resolveSlackBotToken(explicit ?? account.botToken ?? undefined);
  if (!token) {
    logVerbose(
      `slack actions: missing bot token for account=${account.accountId} explicit=${Boolean(
        explicit,
      )} source=${account.botTokenSource ?? "unknown"}`,
    );
    throw new Error("SLACK_BOT_TOKEN or channels.slack.botToken is required for Slack actions");
  }
  return token;
}

function normalizeEmoji(raw: string) {
  const trimmed = raw.trim();
  if (!trimmed) {
    throw new Error("Emoji is required for Slack reactions");
  }
  return trimmed.replace(/^:+|:+$/g, "");
}

async function getClient(opts: SlackActionClientOpts = {}, mode: "read" | "write" = "read") {
  if (opts.client) {
    return opts.client;
  }
  const token = resolveToken(opts.token, opts.accountId);
  return mode === "write" ? createSlackWriteClient(token) : createSlackWebClient(token);
}

async function resolveBotUserId(client: WebClient) {
  const auth = await client.auth.test();
  if (!auth?.user_id) {
    throw new Error("Failed to resolve Slack bot user id");
  }
  return auth.user_id;
}

export async function reactSlackMessage(
  channelId: string,
  messageId: string,
  emoji: string,
  opts: SlackActionClientOpts = {},
) {
  const client = await getClient(opts, "write");
  await client.reactions.add({
    channel: channelId,
    timestamp: messageId,
    name: normalizeEmoji(emoji),
  });
}

export async function removeSlackReaction(
  channelId: string,
  messageId: string,
  emoji: string,
  opts: SlackActionClientOpts = {},
) {
  const client = await getClient(opts, "write");
  await client.reactions.remove({
    channel: channelId,
    timestamp: messageId,
    name: normalizeEmoji(emoji),
  });
}

export async function removeOwnSlackReactions(
  channelId: string,
  messageId: string,
  opts: SlackActionClientOpts = {},
): Promise<string[]> {
  const client = await getClient(opts, "write");
  const userId = await resolveBotUserId(client);
  const reactions = await listSlackReactions(channelId, messageId, { client });
  const toRemove = new Set<string>();
  for (const reaction of reactions ?? []) {
    const name = reaction?.name;
    if (!name) {
      continue;
    }
    const users = reaction?.users ?? [];
    if (users.includes(userId)) {
      toRemove.add(name);
    }
  }
  if (toRemove.size === 0) {
    return [];
  }
  await Promise.all(
    Array.from(toRemove, (name) =>
      client.reactions.remove({
        channel: channelId,
        timestamp: messageId,
        name,
      }),
    ),
  );
  return Array.from(toRemove);
}

export async function listSlackReactions(
  channelId: string,
  messageId: string,
  opts: SlackActionClientOpts = {},
): Promise<SlackMessageSummary["reactions"]> {
  const client = await getClient(opts);
  const result = await client.reactions.get({
    channel: channelId,
    timestamp: messageId,
    full: true,
  });
  const message = result.message as SlackMessageSummary | undefined;
  return message?.reactions ?? [];
}

export async function sendSlackMessage(
  to: string,
  content: string,
  opts: SlackActionClientOpts & {
    mediaUrl?: string;
    mediaAccess?: {
      localRoots?: readonly string[];
      readFile?: (filePath: string) => Promise<Buffer>;
    };
    mediaLocalRoots?: readonly string[];
    mediaReadFile?: (filePath: string) => Promise<Buffer>;
    threadTs?: string;
    uploadFileName?: string;
    uploadTitle?: string;
    blocks?: (Block | KnownBlock)[];
  } = {},
) {
  return await sendMessageSlack(to, content, {
    accountId: opts.accountId,
    token: opts.token,
    mediaUrl: opts.mediaUrl,
    mediaAccess: opts.mediaAccess,
    mediaLocalRoots: opts.mediaLocalRoots,
    mediaReadFile: opts.mediaReadFile,
    client: opts.client,
    threadTs: opts.threadTs,
    ...(opts.uploadFileName ? { uploadFileName: opts.uploadFileName } : {}),
    ...(opts.uploadTitle ? { uploadTitle: opts.uploadTitle } : {}),
    blocks: opts.blocks,
  });
}

export async function editSlackMessage(
  channelId: string,
  messageId: string,
  content: string,
  opts: SlackActionClientOpts & { blocks?: (Block | KnownBlock)[] } = {},
) {
  const client = await getClient(opts, "write");
  const blocks = opts.blocks == null ? undefined : validateSlackBlocksArray(opts.blocks);
  const trimmedContent = content.trim();
  await client.chat.update({
    channel: channelId,
    ts: messageId,
    text: trimmedContent || (blocks ? buildSlackBlocksFallbackText(blocks) : " "),
    ...(blocks ? { blocks } : {}),
  });
}

export async function deleteSlackMessage(
  channelId: string,
  messageId: string,
  opts: SlackActionClientOpts = {},
) {
  const client = await getClient(opts, "write");
  await client.chat.delete({
    channel: channelId,
    ts: messageId,
  });
}

export async function readSlackMessages(
  channelId: string,
  opts: SlackActionClientOpts & {
    limit?: number;
    before?: string;
    after?: string;
    threadId?: string;
  } = {},
): Promise<{ messages: SlackMessageSummary[]; hasMore: boolean }> {
  const client = await getClient(opts);

  // Use conversations.replies for thread messages, conversations.history for channel messages.
  if (opts.threadId) {
    const result = await client.conversations.replies({
      channel: channelId,
      ts: opts.threadId,
      limit: opts.limit,
      latest: opts.before,
      oldest: opts.after,
    });
    return {
      // conversations.replies includes the parent message; drop it for replies-only reads.
      messages: (result.messages ?? []).filter(
        (message) => (message as SlackMessageSummary)?.ts !== opts.threadId,
      ) as SlackMessageSummary[],
      hasMore: Boolean(result.has_more),
    };
  }

  const result = await client.conversations.history({
    channel: channelId,
    limit: opts.limit,
    latest: opts.before,
    oldest: opts.after,
  });
  return {
    messages: (result.messages ?? []) as SlackMessageSummary[],
    hasMore: Boolean(result.has_more),
  };
}

export async function getSlackMemberInfo(userId: string, opts: SlackActionClientOpts = {}) {
  const client = await getClient(opts);
  return await client.users.info({ user: userId });
}

export async function listSlackEmojis(opts: SlackActionClientOpts = {}) {
  const client = await getClient(opts);
  return await client.emoji.list();
}

export async function pinSlackMessage(
  channelId: string,
  messageId: string,
  opts: SlackActionClientOpts = {},
) {
  const client = await getClient(opts, "write");
  await client.pins.add({ channel: channelId, timestamp: messageId });
}

export async function unpinSlackMessage(
  channelId: string,
  messageId: string,
  opts: SlackActionClientOpts = {},
) {
  const client = await getClient(opts, "write");
  await client.pins.remove({ channel: channelId, timestamp: messageId });
}

export async function listSlackPins(
  channelId: string,
  opts: SlackActionClientOpts = {},
): Promise<SlackPin[]> {
  const client = await getClient(opts);
  const result = await client.pins.list({ channel: channelId });
  return (result.items ?? []) as SlackPin[];
}

type SlackFileInfoSummary = {
  id?: string;
  name?: string;
  mimetype?: string;
  url_private?: string;
  url_private_download?: string;
  channels?: unknown;
  groups?: unknown;
  ims?: unknown;
  shares?: unknown;
};

type SlackFileThreadShare = {
  channelId: string;
  ts?: string;
  threadTs?: string;
};

function normalizeSlackScopeValue(value: string | undefined): string | undefined {
  const trimmed = value?.trim();
  return trimmed ? trimmed : undefined;
}

function collectSlackDirectShareChannelIds(file: SlackFileInfoSummary): Set<string> {
  const ids = new Set<string>();
  for (const group of [file.channels, file.groups, file.ims]) {
    if (!Array.isArray(group)) {
      continue;
    }
    for (const entry of group) {
      if (typeof entry !== "string") {
        continue;
      }
      const normalized = normalizeSlackScopeValue(entry);
      if (normalized) {
        ids.add(normalized);
      }
    }
  }
  return ids;
}

function collectSlackShareMaps(file: SlackFileInfoSummary): Array<Record<string, unknown>> {
  if (!file.shares || typeof file.shares !== "object" || Array.isArray(file.shares)) {
    return [];
  }
  const shares = file.shares as Record<string, unknown>;
  return [shares.public, shares.private].filter(
    (value): value is Record<string, unknown> =>
      Boolean(value) && typeof value === "object" && !Array.isArray(value),
  );
}

function collectSlackSharedChannelIds(file: SlackFileInfoSummary): Set<string> {
  const ids = new Set<string>();
  for (const shareMap of collectSlackShareMaps(file)) {
    for (const channelId of Object.keys(shareMap)) {
      const normalized = normalizeSlackScopeValue(channelId);
      if (normalized) {
        ids.add(normalized);
      }
    }
  }
  return ids;
}

function collectSlackThreadShares(
  file: SlackFileInfoSummary,
  channelId: string,
): SlackFileThreadShare[] {
  const matches: SlackFileThreadShare[] = [];
  for (const shareMap of collectSlackShareMaps(file)) {
    const rawEntries = shareMap[channelId];
    if (!Array.isArray(rawEntries)) {
      continue;
    }
    for (const rawEntry of rawEntries) {
      if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
        continue;
      }
      const entry = rawEntry as Record<string, unknown>;
      const ts = typeof entry.ts === "string" ? normalizeSlackScopeValue(entry.ts) : undefined;
      const threadTs =
        typeof entry.thread_ts === "string" ? normalizeSlackScopeValue(entry.thread_ts) : undefined;
      matches.push({ channelId, ts, threadTs });
    }
  }
  return matches;
}

function hasSlackScopeMismatch(params: {
  file: SlackFileInfoSummary;
  channelId?: string;
  threadId?: string;
}): boolean {
  const channelId = normalizeSlackScopeValue(params.channelId);
  if (!channelId) {
    return false;
  }
  const threadId = normalizeSlackScopeValue(params.threadId);

  const directIds = collectSlackDirectShareChannelIds(params.file);
  const sharedIds = collectSlackSharedChannelIds(params.file);
  const hasChannelEvidence = directIds.size > 0 || sharedIds.size > 0;
  const inChannel = directIds.has(channelId) || sharedIds.has(channelId);
  if (hasChannelEvidence && !inChannel) {
    return true;
  }

  if (!threadId) {
    return false;
  }
  const threadShares = collectSlackThreadShares(params.file, channelId);
  if (threadShares.length === 0) {
    return false;
  }
  const threadEvidence = threadShares.filter((entry) => entry.threadTs || entry.ts);
  if (threadEvidence.length === 0) {
    return false;
  }
  return !threadEvidence.some((entry) => entry.threadTs === threadId || entry.ts === threadId);
}

/**
 * Downloads a Slack file by ID and saves it to the local media store.
 * Fetches a fresh download URL via files.info to avoid using stale private URLs.
 * Returns null when the file cannot be found or downloaded.
 */
export async function downloadSlackFile(
  fileId: string,
  opts: SlackActionClientOpts & { maxBytes: number; channelId?: string; threadId?: string },
): Promise<SlackMediaResult | null> {
  const token = resolveToken(opts.token, opts.accountId);
  const client = await getClient(opts);

  // Fetch fresh file metadata (includes a current url_private_download).
  const info = await client.files.info({ file: fileId });
  const file = info.file as SlackFileInfoSummary | undefined;

  if (!file?.url_private_download && !file?.url_private) {
    return null;
  }
  if (hasSlackScopeMismatch({ file, channelId: opts.channelId, threadId: opts.threadId })) {
    return null;
  }

  const results = await resolveSlackMedia({
    files: [
      {
        id: file.id,
        name: file.name,
        mimetype: file.mimetype,
        url_private: file.url_private,
        url_private_download: file.url_private_download,
      },
    ],
    token,
    maxBytes: opts.maxBytes,
  });

  return results?.[0] ?? null;
}
