import type { App } from "@slack/bolt";
import type { HistoryEntry } from "../../auto-reply/reply/history.js";
import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js";
import type { OpenClawConfig, SlackReactionNotificationMode } from "../../config/config.js";
import { resolveSessionKey, type SessionScope } from "../../config/sessions.js";
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
import { logVerbose } from "../../globals.js";
import { createDedupeCache } from "../../infra/dedupe.js";
import { getChildLogger } from "../../logging.js";
import type { RuntimeEnv } from "../../runtime.js";
import type { SlackMessageEvent } from "../types.js";
import { normalizeAllowList, normalizeAllowListLower, normalizeSlackSlug } from "./allow-list.js";
import type { SlackChannelConfigEntries } from "./channel-config.js";
import { resolveSlackChannelConfig } from "./channel-config.js";
import { isSlackChannelAllowedByPolicy } from "./policy.js";

export function inferSlackChannelType(
  channelId?: string | null,
): SlackMessageEvent["channel_type"] | undefined {
  const trimmed = channelId?.trim();
  if (!trimmed) {
    return undefined;
  }
  if (trimmed.startsWith("D")) {
    return "im";
  }
  if (trimmed.startsWith("C")) {
    return "channel";
  }
  if (trimmed.startsWith("G")) {
    return "group";
  }
  return undefined;
}

export function normalizeSlackChannelType(
  channelType?: string | null,
  channelId?: string | null,
): SlackMessageEvent["channel_type"] {
  const normalized = channelType?.trim().toLowerCase();
  if (
    normalized === "im" ||
    normalized === "mpim" ||
    normalized === "channel" ||
    normalized === "group"
  ) {
    return normalized;
  }
  return inferSlackChannelType(channelId) ?? "channel";
}

export type SlackMonitorContext = {
  cfg: OpenClawConfig;
  accountId: string;
  botToken: string;
  app: App;
  runtime: RuntimeEnv;

  botUserId: string;
  teamId: string;
  apiAppId: string;

  historyLimit: number;
  channelHistories: Map<string, HistoryEntry[]>;
  sessionScope: SessionScope;
  mainKey: string;

  dmEnabled: boolean;
  dmPolicy: DmPolicy;
  allowFrom: string[];
  allowNameMatching: boolean;
  groupDmEnabled: boolean;
  groupDmChannels: string[];
  defaultRequireMention: boolean;
  channelsConfig?: SlackChannelConfigEntries;
  groupPolicy: GroupPolicy;
  useAccessGroups: boolean;
  reactionMode: SlackReactionNotificationMode;
  reactionAllowlist: Array<string | number>;
  replyToMode: "off" | "first" | "all";
  threadHistoryScope: "thread" | "channel";
  threadInheritParent: boolean;
  slashCommand: Required<import("../../config/config.js").SlackSlashCommandConfig>;
  textLimit: number;
  ackReactionScope: string;
  mediaMaxBytes: number;
  removeAckAfterReply: boolean;

  logger: ReturnType<typeof getChildLogger>;
  markMessageSeen: (channelId: string | undefined, ts?: string) => boolean;
  shouldDropMismatchedSlackEvent: (body: unknown) => boolean;
  resolveSlackSystemEventSessionKey: (params: {
    channelId?: string | null;
    channelType?: string | null;
  }) => string;
  isChannelAllowed: (params: {
    channelId?: string;
    channelName?: string;
    channelType?: SlackMessageEvent["channel_type"];
  }) => boolean;
  resolveChannelName: (channelId: string) => Promise<{
    name?: string;
    type?: SlackMessageEvent["channel_type"];
    topic?: string;
    purpose?: string;
  }>;
  resolveUserName: (userId: string) => Promise<{ name?: string }>;
  setSlackThreadStatus: (params: {
    channelId: string;
    threadTs?: string;
    status: string;
  }) => Promise<void>;
};

export function createSlackMonitorContext(params: {
  cfg: OpenClawConfig;
  accountId: string;
  botToken: string;
  app: App;
  runtime: RuntimeEnv;

  botUserId: string;
  teamId: string;
  apiAppId: string;

  historyLimit: number;
  sessionScope: SessionScope;
  mainKey: string;

  dmEnabled: boolean;
  dmPolicy: DmPolicy;
  allowFrom: Array<string | number> | undefined;
  allowNameMatching: boolean;
  groupDmEnabled: boolean;
  groupDmChannels: Array<string | number> | undefined;
  defaultRequireMention?: boolean;
  channelsConfig?: SlackMonitorContext["channelsConfig"];
  groupPolicy: SlackMonitorContext["groupPolicy"];
  useAccessGroups: boolean;
  reactionMode: SlackReactionNotificationMode;
  reactionAllowlist: Array<string | number>;
  replyToMode: SlackMonitorContext["replyToMode"];
  threadHistoryScope: SlackMonitorContext["threadHistoryScope"];
  threadInheritParent: SlackMonitorContext["threadInheritParent"];
  slashCommand: SlackMonitorContext["slashCommand"];
  textLimit: number;
  ackReactionScope: string;
  mediaMaxBytes: number;
  removeAckAfterReply: boolean;
}): SlackMonitorContext {
  const channelHistories = new Map<string, HistoryEntry[]>();
  const logger = getChildLogger({ module: "slack-auto-reply" });

  const channelCache = new Map<
    string,
    {
      name?: string;
      type?: SlackMessageEvent["channel_type"];
      topic?: string;
      purpose?: string;
    }
  >();
  const userCache = new Map<string, { name?: string }>();
  const seenMessages = createDedupeCache({ ttlMs: 60_000, maxSize: 500 });

  const allowFrom = normalizeAllowList(params.allowFrom);
  const groupDmChannels = normalizeAllowList(params.groupDmChannels);
  const defaultRequireMention = params.defaultRequireMention ?? true;

  const markMessageSeen = (channelId: string | undefined, ts?: string) => {
    if (!channelId || !ts) {
      return false;
    }
    return seenMessages.check(`${channelId}:${ts}`);
  };

  const resolveSlackSystemEventSessionKey = (p: {
    channelId?: string | null;
    channelType?: string | null;
  }) => {
    const channelId = p.channelId?.trim() ?? "";
    if (!channelId) {
      return params.mainKey;
    }
    const channelType = normalizeSlackChannelType(p.channelType, channelId);
    const isDirectMessage = channelType === "im";
    const isGroup = channelType === "mpim";
    const from = isDirectMessage
      ? `slack:${channelId}`
      : isGroup
        ? `slack:group:${channelId}`
        : `slack:channel:${channelId}`;
    const chatType = isDirectMessage ? "direct" : isGroup ? "group" : "channel";
    return resolveSessionKey(
      params.sessionScope,
      { From: from, ChatType: chatType, Provider: "slack" },
      params.mainKey,
    );
  };

  const resolveChannelName = async (channelId: string) => {
    const cached = channelCache.get(channelId);
    if (cached) {
      return cached;
    }
    try {
      const info = await params.app.client.conversations.info({
        token: params.botToken,
        channel: channelId,
      });
      const name = info.channel && "name" in info.channel ? info.channel.name : undefined;
      const channel = info.channel ?? undefined;
      const type: SlackMessageEvent["channel_type"] | undefined = channel?.is_im
        ? "im"
        : channel?.is_mpim
          ? "mpim"
          : channel?.is_channel
            ? "channel"
            : channel?.is_group
              ? "group"
              : undefined;
      const topic = channel && "topic" in channel ? (channel.topic?.value ?? undefined) : undefined;
      const purpose =
        channel && "purpose" in channel ? (channel.purpose?.value ?? undefined) : undefined;
      const entry = { name, type, topic, purpose };
      channelCache.set(channelId, entry);
      return entry;
    } catch {
      return {};
    }
  };

  const resolveUserName = async (userId: string) => {
    const cached = userCache.get(userId);
    if (cached) {
      return cached;
    }
    try {
      const info = await params.app.client.users.info({
        token: params.botToken,
        user: userId,
      });
      const profile = info.user?.profile;
      const name = profile?.display_name || profile?.real_name || info.user?.name || undefined;
      const entry = { name };
      userCache.set(userId, entry);
      return entry;
    } catch {
      return {};
    }
  };

  const setSlackThreadStatus = async (p: {
    channelId: string;
    threadTs?: string;
    status: string;
  }) => {
    if (!p.threadTs) {
      return;
    }
    const payload = {
      token: params.botToken,
      channel_id: p.channelId,
      thread_ts: p.threadTs,
      status: p.status,
    };
    const client = params.app.client as unknown as {
      assistant?: {
        threads?: {
          setStatus?: (args: typeof payload) => Promise<unknown>;
        };
      };
      apiCall?: (method: string, args: typeof payload) => Promise<unknown>;
    };
    try {
      if (client.assistant?.threads?.setStatus) {
        await client.assistant.threads.setStatus(payload);
        return;
      }
      if (typeof client.apiCall === "function") {
        await client.apiCall("assistant.threads.setStatus", payload);
      }
    } catch (err) {
      logVerbose(`slack status update failed for channel ${p.channelId}: ${String(err)}`);
    }
  };

  const isChannelAllowed = (p: {
    channelId?: string;
    channelName?: string;
    channelType?: SlackMessageEvent["channel_type"];
  }) => {
    const channelType = normalizeSlackChannelType(p.channelType, p.channelId);
    const isDirectMessage = channelType === "im";
    const isGroupDm = channelType === "mpim";
    const isRoom = channelType === "channel" || channelType === "group";

    if (isDirectMessage && !params.dmEnabled) {
      return false;
    }
    if (isGroupDm && !params.groupDmEnabled) {
      return false;
    }

    if (isGroupDm && groupDmChannels.length > 0) {
      const allowList = normalizeAllowListLower(groupDmChannels);
      const candidates = [
        p.channelId,
        p.channelName ? `#${p.channelName}` : undefined,
        p.channelName,
        p.channelName ? normalizeSlackSlug(p.channelName) : undefined,
      ]
        .filter((value): value is string => Boolean(value))
        .map((value) => value.toLowerCase());
      const permitted =
        allowList.includes("*") || candidates.some((candidate) => allowList.includes(candidate));
      if (!permitted) {
        return false;
      }
    }

    if (isRoom && p.channelId) {
      const channelConfig = resolveSlackChannelConfig({
        channelId: p.channelId,
        channelName: p.channelName,
        channels: params.channelsConfig,
        defaultRequireMention,
      });
      const channelMatchMeta = formatAllowlistMatchMeta(channelConfig);
      const channelAllowed = channelConfig?.allowed !== false;
      const channelAllowlistConfigured =
        Boolean(params.channelsConfig) && Object.keys(params.channelsConfig ?? {}).length > 0;
      if (
        !isSlackChannelAllowedByPolicy({
          groupPolicy: params.groupPolicy,
          channelAllowlistConfigured,
          channelAllowed,
        })
      ) {
        logVerbose(
          `slack: drop channel ${p.channelId} (groupPolicy=${params.groupPolicy}, ${channelMatchMeta})`,
        );
        return false;
      }
      // When groupPolicy is "open", only block channels that are EXPLICITLY denied
      // (i.e., have a matching config entry with allow:false). Channels not in the
      // config (matchSource undefined) should be allowed under open policy.
      const hasExplicitConfig = Boolean(channelConfig?.matchSource);
      if (!channelAllowed && (params.groupPolicy !== "open" || hasExplicitConfig)) {
        logVerbose(`slack: drop channel ${p.channelId} (${channelMatchMeta})`);
        return false;
      }
      logVerbose(`slack: allow channel ${p.channelId} (${channelMatchMeta})`);
    }

    return true;
  };

  const shouldDropMismatchedSlackEvent = (body: unknown) => {
    if (!body || typeof body !== "object") {
      return false;
    }
    const raw = body as { api_app_id?: unknown; team_id?: unknown };
    const incomingApiAppId = typeof raw.api_app_id === "string" ? raw.api_app_id : "";
    const incomingTeamId = typeof raw.team_id === "string" ? raw.team_id : "";

    if (params.apiAppId && incomingApiAppId && incomingApiAppId !== params.apiAppId) {
      logVerbose(
        `slack: drop event with api_app_id=${incomingApiAppId} (expected ${params.apiAppId})`,
      );
      return true;
    }
    if (params.teamId && incomingTeamId && incomingTeamId !== params.teamId) {
      logVerbose(`slack: drop event with team_id=${incomingTeamId} (expected ${params.teamId})`);
      return true;
    }
    return false;
  };

  return {
    cfg: params.cfg,
    accountId: params.accountId,
    botToken: params.botToken,
    app: params.app,
    runtime: params.runtime,
    botUserId: params.botUserId,
    teamId: params.teamId,
    apiAppId: params.apiAppId,
    historyLimit: params.historyLimit,
    channelHistories,
    sessionScope: params.sessionScope,
    mainKey: params.mainKey,
    dmEnabled: params.dmEnabled,
    dmPolicy: params.dmPolicy,
    allowFrom,
    allowNameMatching: params.allowNameMatching,
    groupDmEnabled: params.groupDmEnabled,
    groupDmChannels,
    defaultRequireMention,
    channelsConfig: params.channelsConfig,
    groupPolicy: params.groupPolicy,
    useAccessGroups: params.useAccessGroups,
    reactionMode: params.reactionMode,
    reactionAllowlist: params.reactionAllowlist,
    replyToMode: params.replyToMode,
    threadHistoryScope: params.threadHistoryScope,
    threadInheritParent: params.threadInheritParent,
    slashCommand: params.slashCommand,
    textLimit: params.textLimit,
    ackReactionScope: params.ackReactionScope,
    mediaMaxBytes: params.mediaMaxBytes,
    removeAckAfterReply: params.removeAckAfterReply,
    logger,
    markMessageSeen,
    shouldDropMismatchedSlackEvent,
    resolveSlackSystemEventSessionKey,
    isChannelAllowed,
    resolveChannelName,
    resolveUserName,
    setSlackThreadStatus,
  };
}
