import type { SlackActionMiddlewareArgs, SlackCommandMiddlewareArgs } from "@slack/bolt";
import type { ChatCommandDefinition, CommandArgs } from "../../auto-reply/commands-registry.js";
import type { ResolvedSlackAccount } from "../accounts.js";
import type { SlackMonitorContext } from "./context.js";
import { resolveChunkMode } from "../../auto-reply/chunk.js";
import {
  buildCommandTextFromArgs,
  findCommandByNativeName,
  listNativeCommandSpecsForConfig,
  parseCommandArgs,
  resolveCommandArgMenu,
} from "../../auto-reply/commands-registry.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
import { listSkillCommandsForAgents } from "../../auto-reply/skill-commands.js";
import { formatAllowlistMatchMeta } from "../../channels/allowlist-match.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { resolveConversationLabel } from "../../channels/conversation-label.js";
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
import { resolveNativeCommandsEnabled, resolveNativeSkillsEnabled } from "../../config/commands.js";
import { resolveMarkdownTableMode } from "../../config/markdown-tables.js";
import { danger, logVerbose } from "../../globals.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js";
import {
  readChannelAllowFromStore,
  upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
import {
  normalizeAllowList,
  normalizeAllowListLower,
  resolveSlackAllowListMatch,
  resolveSlackUserAllowed,
} from "./allow-list.js";
import { resolveSlackChannelConfig, type SlackChannelConfigResolved } from "./channel-config.js";
import { buildSlackSlashCommandMatcher, resolveSlackSlashCommandConfig } from "./commands.js";
import { normalizeSlackChannelType } from "./context.js";
import { isSlackChannelAllowedByPolicy } from "./policy.js";
import { deliverSlackSlashReplies } from "./replies.js";

type SlackBlock = { type: string; [key: string]: unknown };

const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg";
const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg";

function chunkItems<T>(items: T[], size: number): T[][] {
  if (size <= 0) {
    return [items];
  }
  const rows: T[][] = [];
  for (let i = 0; i < items.length; i += size) {
    rows.push(items.slice(i, i + size));
  }
  return rows;
}

function encodeSlackCommandArgValue(parts: {
  command: string;
  arg: string;
  value: string;
  userId: string;
}) {
  return [
    SLACK_COMMAND_ARG_VALUE_PREFIX,
    encodeURIComponent(parts.command),
    encodeURIComponent(parts.arg),
    encodeURIComponent(parts.value),
    encodeURIComponent(parts.userId),
  ].join("|");
}

function parseSlackCommandArgValue(raw?: string | null): {
  command: string;
  arg: string;
  value: string;
  userId: string;
} | null {
  if (!raw) {
    return null;
  }
  const parts = raw.split("|");
  if (parts.length !== 5 || parts[0] !== SLACK_COMMAND_ARG_VALUE_PREFIX) {
    return null;
  }
  const [, command, arg, value, userId] = parts;
  if (!command || !arg || !value || !userId) {
    return null;
  }
  const decode = (text: string) => {
    try {
      return decodeURIComponent(text);
    } catch {
      return null;
    }
  };
  const decodedCommand = decode(command);
  const decodedArg = decode(arg);
  const decodedValue = decode(value);
  const decodedUserId = decode(userId);
  if (!decodedCommand || !decodedArg || !decodedValue || !decodedUserId) {
    return null;
  }
  return {
    command: decodedCommand,
    arg: decodedArg,
    value: decodedValue,
    userId: decodedUserId,
  };
}

function buildSlackCommandArgMenuBlocks(params: {
  title: string;
  command: string;
  arg: string;
  choices: Array<{ value: string; label: string }>;
  userId: string;
}) {
  const rows = chunkItems(params.choices, 5).map((choices) => ({
    type: "actions",
    elements: choices.map((choice) => ({
      type: "button",
      action_id: SLACK_COMMAND_ARG_ACTION_ID,
      text: { type: "plain_text", text: choice.label },
      value: encodeSlackCommandArgValue({
        command: params.command,
        arg: params.arg,
        value: choice.value,
        userId: params.userId,
      }),
    })),
  }));
  return [
    {
      type: "section",
      text: { type: "mrkdwn", text: params.title },
    },
    ...rows,
  ];
}

export function registerSlackMonitorSlashCommands(params: {
  ctx: SlackMonitorContext;
  account: ResolvedSlackAccount;
}) {
  const { ctx, account } = params;
  const cfg = ctx.cfg;
  const runtime = ctx.runtime;

  const supportsInteractiveArgMenus =
    typeof (ctx.app as { action?: unknown }).action === "function";

  const slashCommand = resolveSlackSlashCommandConfig(
    ctx.slashCommand ?? account.config.slashCommand,
  );

  const handleSlashCommand = async (p: {
    command: SlackCommandMiddlewareArgs["command"];
    ack: SlackCommandMiddlewareArgs["ack"];
    respond: SlackCommandMiddlewareArgs["respond"];
    prompt: string;
    commandArgs?: CommandArgs;
    commandDefinition?: ChatCommandDefinition;
  }) => {
    const { command, ack, respond, prompt, commandArgs, commandDefinition } = p;
    try {
      if (!prompt.trim()) {
        await ack({
          text: "Message required.",
          response_type: "ephemeral",
        });
        return;
      }
      await ack();

      if (ctx.botUserId && command.user_id === ctx.botUserId) {
        return;
      }

      const channelInfo = await ctx.resolveChannelName(command.channel_id);
      const rawChannelType =
        channelInfo?.type ?? (command.channel_name === "directmessage" ? "im" : undefined);
      const channelType = normalizeSlackChannelType(rawChannelType, command.channel_id);
      const isDirectMessage = channelType === "im";
      const isGroupDm = channelType === "mpim";
      const isRoom = channelType === "channel" || channelType === "group";
      const isRoomish = isRoom || isGroupDm;

      if (
        !ctx.isChannelAllowed({
          channelId: command.channel_id,
          channelName: channelInfo?.name,
          channelType,
        })
      ) {
        await respond({
          text: "This channel is not allowed.",
          response_type: "ephemeral",
        });
        return;
      }

      const storeAllowFrom = await readChannelAllowFromStore("slack").catch(() => []);
      const effectiveAllowFrom = normalizeAllowList([...ctx.allowFrom, ...storeAllowFrom]);
      const effectiveAllowFromLower = normalizeAllowListLower(effectiveAllowFrom);

      let commandAuthorized = true;
      let channelConfig: SlackChannelConfigResolved | null = null;
      if (isDirectMessage) {
        if (!ctx.dmEnabled || ctx.dmPolicy === "disabled") {
          await respond({
            text: "Slack DMs are disabled.",
            response_type: "ephemeral",
          });
          return;
        }
        if (ctx.dmPolicy !== "open") {
          const sender = await ctx.resolveUserName(command.user_id);
          const senderName = sender?.name ?? undefined;
          const allowMatch = resolveSlackAllowListMatch({
            allowList: effectiveAllowFromLower,
            id: command.user_id,
            name: senderName,
          });
          const allowMatchMeta = formatAllowlistMatchMeta(allowMatch);
          if (!allowMatch.allowed) {
            if (ctx.dmPolicy === "pairing") {
              const { code, created } = await upsertChannelPairingRequest({
                channel: "slack",
                id: command.user_id,
                meta: { name: senderName },
              });
              if (created) {
                logVerbose(
                  `slack pairing request sender=${command.user_id} name=${
                    senderName ?? "unknown"
                  } (${allowMatchMeta})`,
                );
                await respond({
                  text: buildPairingReply({
                    channel: "slack",
                    idLine: `Your Slack user id: ${command.user_id}`,
                    code,
                  }),
                  response_type: "ephemeral",
                });
              }
            } else {
              logVerbose(
                `slack: blocked slash sender ${command.user_id} (dmPolicy=${ctx.dmPolicy}, ${allowMatchMeta})`,
              );
              await respond({
                text: "You are not authorized to use this command.",
                response_type: "ephemeral",
              });
            }
            return;
          }
          commandAuthorized = true;
        }
      }

      if (isRoom) {
        channelConfig = resolveSlackChannelConfig({
          channelId: command.channel_id,
          channelName: channelInfo?.name,
          channels: ctx.channelsConfig,
          defaultRequireMention: ctx.defaultRequireMention,
        });
        if (ctx.useAccessGroups) {
          const channelAllowlistConfigured =
            Boolean(ctx.channelsConfig) && Object.keys(ctx.channelsConfig ?? {}).length > 0;
          const channelAllowed = channelConfig?.allowed !== false;
          if (
            !isSlackChannelAllowedByPolicy({
              groupPolicy: ctx.groupPolicy,
              channelAllowlistConfigured,
              channelAllowed,
            })
          ) {
            await respond({
              text: "This channel is not allowed.",
              response_type: "ephemeral",
            });
            return;
          }
          // 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 && (ctx.groupPolicy !== "open" || hasExplicitConfig)) {
            await respond({
              text: "This channel is not allowed.",
              response_type: "ephemeral",
            });
            return;
          }
        }
      }

      const sender = await ctx.resolveUserName(command.user_id);
      const senderName = sender?.name ?? command.user_name ?? command.user_id;
      const channelUsersAllowlistConfigured =
        isRoom && Array.isArray(channelConfig?.users) && channelConfig.users.length > 0;
      const channelUserAllowed = channelUsersAllowlistConfigured
        ? resolveSlackUserAllowed({
            allowList: channelConfig?.users,
            userId: command.user_id,
            userName: senderName,
          })
        : false;
      if (channelUsersAllowlistConfigured && !channelUserAllowed) {
        await respond({
          text: "You are not authorized to use this command here.",
          response_type: "ephemeral",
        });
        return;
      }

      const ownerAllowed = resolveSlackAllowListMatch({
        allowList: effectiveAllowFromLower,
        id: command.user_id,
        name: senderName,
      }).allowed;
      if (isRoomish) {
        commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
          useAccessGroups: ctx.useAccessGroups,
          authorizers: [
            { configured: effectiveAllowFromLower.length > 0, allowed: ownerAllowed },
            { configured: channelUsersAllowlistConfigured, allowed: channelUserAllowed },
          ],
        });
        if (ctx.useAccessGroups && !commandAuthorized) {
          await respond({
            text: "You are not authorized to use this command.",
            response_type: "ephemeral",
          });
          return;
        }
      }

      if (commandDefinition && supportsInteractiveArgMenus) {
        const menu = resolveCommandArgMenu({
          command: commandDefinition,
          args: commandArgs,
          cfg,
        });
        if (menu) {
          const commandLabel = commandDefinition.nativeName ?? commandDefinition.key;
          const title =
            menu.title ?? `Choose ${menu.arg.description || menu.arg.name} for /${commandLabel}.`;
          const blocks = buildSlackCommandArgMenuBlocks({
            title,
            command: commandLabel,
            arg: menu.arg.name,
            // @ts-ignore -- cherry-pick upstream type mismatch
            // @ts-ignore -- cherry-pick upstream type mismatch
            choices: menu.choices,
            userId: command.user_id,
          });
          await respond({
            text: title,
            blocks,
            response_type: "ephemeral",
          });
          return;
        }
      }

      const channelName = channelInfo?.name;
      const roomLabel = channelName ? `#${channelName}` : `#${command.channel_id}`;
      const route = resolveAgentRoute({
        cfg,
        channel: "slack",
        accountId: account.accountId,
        teamId: ctx.teamId || undefined,
        peer: {
          kind: isDirectMessage ? "dm" : isRoom ? "channel" : "group",
          id: isDirectMessage ? command.user_id : command.channel_id,
        },
      });

      const untrustedChannelMetadata = isRoomish
        ? buildUntrustedChannelMetadata({
            source: "slack",
            label: "Slack channel description",
            entries: [channelInfo?.topic, channelInfo?.purpose],
          })
        : undefined;
      const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter(
        (entry): entry is string => Boolean(entry),
      );
      const groupSystemPrompt =
        systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;

      const ctxPayload = finalizeInboundContext({
        Body: prompt,
        RawBody: prompt,
        CommandBody: prompt,
        CommandArgs: commandArgs,
        From: isDirectMessage
          ? `slack:${command.user_id}`
          : isRoom
            ? `slack:channel:${command.channel_id}`
            : `slack:group:${command.channel_id}`,
        To: `slash:${command.user_id}`,
        ChatType: isDirectMessage ? "direct" : "channel",
        ConversationLabel:
          resolveConversationLabel({
            ChatType: isDirectMessage ? "direct" : "channel",
            SenderName: senderName,
            GroupSubject: isRoomish ? roomLabel : undefined,
            From: isDirectMessage
              ? `slack:${command.user_id}`
              : isRoom
                ? `slack:channel:${command.channel_id}`
                : `slack:group:${command.channel_id}`,
          }) ?? (isDirectMessage ? senderName : roomLabel),
        GroupSubject: isRoomish ? roomLabel : undefined,
        GroupSystemPrompt: isRoomish ? groupSystemPrompt : undefined,
        UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
        SenderName: senderName,
        SenderId: command.user_id,
        Provider: "slack" as const,
        Surface: "slack" as const,
        WasMentioned: true,
        MessageSid: command.trigger_id,
        Timestamp: Date.now(),
        SessionKey:
          `agent:${route.agentId}:${slashCommand.sessionPrefix}:${command.user_id}`.toLowerCase(),
        CommandTargetSessionKey: route.sessionKey,
        AccountId: route.accountId,
        CommandSource: "native" as const,
        CommandAuthorized: commandAuthorized,
        OriginatingChannel: "slack" as const,
        OriginatingTo: `user:${command.user_id}`,
      });

      const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
        cfg,
        agentId: route.agentId,
        channel: "slack",
        accountId: route.accountId,
      });

      const { counts } = await dispatchReplyWithDispatcher({
        ctx: ctxPayload,
        cfg,
        dispatcherOptions: {
          ...prefixOptions,
          deliver: async (payload) => {
            await deliverSlackSlashReplies({
              replies: [payload],
              respond,
              ephemeral: slashCommand.ephemeral,
              textLimit: ctx.textLimit,
              chunkMode: resolveChunkMode(cfg, "slack", route.accountId),
              tableMode: resolveMarkdownTableMode({
                cfg,
                channel: "slack",
                accountId: route.accountId,
              }),
            });
          },
          onError: (err, info) => {
            runtime.error?.(danger(`slack slash ${info.kind} reply failed: ${String(err)}`));
          },
        },
        replyOptions: {
          skillFilter: channelConfig?.skills,
          onModelSelected,
        },
      });
      if (counts.final + counts.tool + counts.block === 0) {
        await deliverSlackSlashReplies({
          replies: [],
          respond,
          ephemeral: slashCommand.ephemeral,
          textLimit: ctx.textLimit,
          chunkMode: resolveChunkMode(cfg, "slack", route.accountId),
          tableMode: resolveMarkdownTableMode({
            cfg,
            channel: "slack",
            accountId: route.accountId,
          }),
        });
      }
    } catch (err) {
      runtime.error?.(danger(`slack slash handler failed: ${String(err)}`));
      await respond({
        text: "Sorry, something went wrong handling that command.",
        response_type: "ephemeral",
      });
    }
  };

  const nativeEnabled = resolveNativeCommandsEnabled({
    providerId: "slack",
    providerSetting: account.config.commands?.native,
    globalSetting: cfg.commands?.native,
  });
  const nativeSkillsEnabled = resolveNativeSkillsEnabled({
    providerId: "slack",
    providerSetting: account.config.commands?.nativeSkills,
    globalSetting: cfg.commands?.nativeSkills,
  });
  const skillCommands =
    nativeEnabled && nativeSkillsEnabled ? listSkillCommandsForAgents({ cfg }) : [];
  const nativeCommands = nativeEnabled
    ? listNativeCommandSpecsForConfig(cfg, { skillCommands, provider: "slack" })
    : [];
  if (nativeCommands.length > 0) {
    for (const command of nativeCommands) {
      ctx.app.command(
        `/${command.name}`,
        async ({ command: cmd, ack, respond }: SlackCommandMiddlewareArgs) => {
          const commandDefinition = findCommandByNativeName(command.name, "slack");
          const rawText = cmd.text?.trim() ?? "";
          const commandArgs = commandDefinition
            ? parseCommandArgs(commandDefinition, rawText)
            : rawText
              ? ({ raw: rawText } satisfies CommandArgs)
              : undefined;
          const prompt = commandDefinition
            ? buildCommandTextFromArgs(commandDefinition, commandArgs)
            : rawText
              ? `/${command.name} ${rawText}`
              : `/${command.name}`;
          await handleSlashCommand({
            command: cmd,
            ack,
            respond,
            prompt,
            commandArgs,
            commandDefinition: commandDefinition ?? undefined,
          });
        },
      );
    }
  } else if (slashCommand.enabled) {
    ctx.app.command(
      buildSlackSlashCommandMatcher(slashCommand.name),
      async ({ command, ack, respond }: SlackCommandMiddlewareArgs) => {
        await handleSlashCommand({
          command,
          ack,
          respond,
          prompt: command.text?.trim() ?? "",
        });
      },
    );
  } else {
    logVerbose("slack: slash commands disabled");
  }

  if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) {
    return;
  }

  const registerArgAction = (actionId: string) => {
    (
      ctx.app as unknown as {
        action: NonNullable<(typeof ctx.app & { action?: unknown })["action"]>;
      }
    ).action(actionId, async (args: SlackActionMiddlewareArgs) => {
      const { ack, body, respond } = args;
      const action = args.action as { value?: string };
      await ack();
      const respondFn =
        respond ??
        (async (payload: { text: string; blocks?: SlackBlock[]; response_type?: string }) => {
          if (!body.channel?.id || !body.user?.id) {
            return;
          }
          await ctx.app.client.chat.postEphemeral({
            token: ctx.botToken,
            channel: body.channel.id,
            user: body.user.id,
            text: payload.text,
            blocks: payload.blocks,
          });
        });
      const parsed = parseSlackCommandArgValue(action?.value);
      if (!parsed) {
        await respondFn({
          text: "Sorry, that button is no longer valid.",
          response_type: "ephemeral",
        });
        return;
      }
      if (body.user?.id && parsed.userId !== body.user.id) {
        await respondFn({
          text: "That menu is for another user.",
          response_type: "ephemeral",
        });
        return;
      }
      const commandDefinition = findCommandByNativeName(parsed.command, "slack");
      const commandArgs: CommandArgs = {
        values: { [parsed.arg]: parsed.value },
      };
      const prompt = commandDefinition
        ? buildCommandTextFromArgs(commandDefinition, commandArgs)
        : `/${parsed.command} ${parsed.value}`;
      const user = body.user;
      const userName =
        user && "name" in user && user.name
          ? user.name
          : user && "username" in user && user.username
            ? user.username
            : (user?.id ?? "");
      const triggerId = "trigger_id" in body ? body.trigger_id : undefined;
      const commandPayload = {
        user_id: user?.id ?? "",
        user_name: userName,
        channel_id: body.channel?.id ?? "",
        channel_name: body.channel?.name ?? body.channel?.id ?? "",
        trigger_id: triggerId ?? String(Date.now()),
      } as SlackCommandMiddlewareArgs["command"];
      await handleSlashCommand({
        command: commandPayload,
        ack: async () => {},
        respond: respondFn,
        prompt,
        commandArgs,
        commandDefinition: commandDefinition ?? undefined,
      });
    });
  };
  registerArgAction(SLACK_COMMAND_ARG_ACTION_ID);
}
