import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { DiscordActionConfig } from "../../config/config.js";
import { readDiscordComponentSpec } from "../../discord/components.js";
import {
  createThreadDiscord,
  deleteMessageDiscord,
  editMessageDiscord,
  fetchChannelPermissionsDiscord,
  fetchMessageDiscord,
  fetchReactionsDiscord,
  listPinsDiscord,
  listThreadsDiscord,
  pinMessageDiscord,
  reactMessageDiscord,
  readMessagesDiscord,
  removeOwnReactionsDiscord,
  removeReactionDiscord,
  searchMessagesDiscord,
  sendDiscordComponentMessage,
  sendMessageDiscord,
  sendPollDiscord,
  sendStickerDiscord,
  sendVoiceMessageDiscord,
  unpinMessageDiscord,
} from "../../discord/send.js";
import type { DiscordSendComponents, DiscordSendEmbeds } from "../../discord/send.shared.js";
import { resolveDiscordChannelId } from "../../discord/targets.js";
import { withNormalizedTimestamp } from "../date-time.js";
import { assertMediaNotDataUrl } from "../sandbox-paths.js";
import {
  type ActionGate,
  jsonResult,
  readReactionParams,
  readStringArrayParam,
  readStringParam,
} from "./common.js";

function parseDiscordMessageLink(link: string) {
  const normalized = link.trim();
  const match = normalized.match(
    /^(?:https?:\/\/)?(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(\d+)\/(\d+)\/(\d+)(?:\/?|\?.*)$/i,
  );
  if (!match) {
    throw new Error(
      "Invalid Discord message link. Expected https://discord.com/channels/<guildId>/<channelId>/<messageId>.",
    );
  }
  return {
    guildId: match[1],
    channelId: match[2],
    messageId: match[3],
  };
}

export async function handleDiscordMessagingAction(
  action: string,
  params: Record<string, unknown>,
  isActionEnabled: ActionGate<DiscordActionConfig>,
  options?: {
    mediaLocalRoots?: readonly string[];
  },
): Promise<AgentToolResult<unknown>> {
  const resolveChannelId = () =>
    resolveDiscordChannelId(
      readStringParam(params, "channelId", {
        required: true,
      }),
    );
  const accountId = readStringParam(params, "accountId");
  const normalizeMessage = (message: unknown) => {
    if (!message || typeof message !== "object") {
      return message;
    }
    return withNormalizedTimestamp(
      message as Record<string, unknown>,
      (message as { timestamp?: unknown }).timestamp,
    );
  };
  switch (action) {
    case "react": {
      if (!isActionEnabled("reactions")) {
        throw new Error("Discord reactions are disabled.");
      }
      const channelId = resolveChannelId();
      const messageId = readStringParam(params, "messageId", {
        required: true,
      });
      const { emoji, remove, isEmpty } = readReactionParams(params, {
        removeErrorMessage: "Emoji is required to remove a Discord reaction.",
      });
      if (remove) {
        if (accountId) {
          await removeReactionDiscord(channelId, messageId, emoji, { accountId });
        } else {
          await removeReactionDiscord(channelId, messageId, emoji);
        }
        return jsonResult({ ok: true, removed: emoji });
      }
      if (isEmpty) {
        const removed = accountId
          ? await removeOwnReactionsDiscord(channelId, messageId, { accountId })
          : await removeOwnReactionsDiscord(channelId, messageId);
        return jsonResult({ ok: true, removed: removed.removed });
      }
      if (accountId) {
        await reactMessageDiscord(channelId, messageId, emoji, { accountId });
      } else {
        await reactMessageDiscord(channelId, messageId, emoji);
      }
      return jsonResult({ ok: true, added: emoji });
    }
    case "reactions": {
      if (!isActionEnabled("reactions")) {
        throw new Error("Discord reactions are disabled.");
      }
      const channelId = resolveChannelId();
      const messageId = readStringParam(params, "messageId", {
        required: true,
      });
      const limitRaw = params.limit;
      const limit =
        typeof limitRaw === "number" && Number.isFinite(limitRaw) ? limitRaw : undefined;
      const reactions = await fetchReactionsDiscord(channelId, messageId, {
        ...(accountId ? { accountId } : {}),
        limit,
      });
      return jsonResult({ ok: true, reactions });
    }
    case "sticker": {
      if (!isActionEnabled("stickers")) {
        throw new Error("Discord stickers are disabled.");
      }
      const to = readStringParam(params, "to", { required: true });
      const content = readStringParam(params, "content");
      const stickerIds = readStringArrayParam(params, "stickerIds", {
        required: true,
        label: "stickerIds",
      });
      await sendStickerDiscord(to, stickerIds, {
        ...(accountId ? { accountId } : {}),
        content,
      });
      return jsonResult({ ok: true });
    }
    case "poll": {
      if (!isActionEnabled("polls")) {
        throw new Error("Discord polls are disabled.");
      }
      const to = readStringParam(params, "to", { required: true });
      const content = readStringParam(params, "content");
      const question = readStringParam(params, "question", {
        required: true,
      });
      const answers = readStringArrayParam(params, "answers", {
        required: true,
        label: "answers",
      });
      const allowMultiselectRaw = params.allowMultiselect;
      const allowMultiselect =
        typeof allowMultiselectRaw === "boolean" ? allowMultiselectRaw : undefined;
      const durationRaw = params.durationHours;
      const durationHours =
        typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : undefined;
      const maxSelections = allowMultiselect ? Math.max(2, answers.length) : 1;
      await sendPollDiscord(
        to,
        { question, options: answers, maxSelections, durationHours },
        { ...(accountId ? { accountId } : {}), content },
      );
      return jsonResult({ ok: true });
    }
    case "permissions": {
      if (!isActionEnabled("permissions")) {
        throw new Error("Discord permissions are disabled.");
      }
      const channelId = resolveChannelId();
      const permissions = accountId
        ? await fetchChannelPermissionsDiscord(channelId, { accountId })
        : await fetchChannelPermissionsDiscord(channelId);
      return jsonResult({ ok: true, permissions });
    }
    case "fetchMessage": {
      if (!isActionEnabled("messages")) {
        throw new Error("Discord message reads are disabled.");
      }
      const messageLink = readStringParam(params, "messageLink");
      let guildId = readStringParam(params, "guildId");
      let channelId = readStringParam(params, "channelId");
      let messageId = readStringParam(params, "messageId");
      if (messageLink) {
        const parsed = parseDiscordMessageLink(messageLink);
        guildId = parsed.guildId;
        channelId = parsed.channelId;
        messageId = parsed.messageId;
      }
      if (!guildId || !channelId || !messageId) {
        throw new Error(
          "Discord message fetch requires guildId, channelId, and messageId (or a valid messageLink).",
        );
      }
      const message = accountId
        ? await fetchMessageDiscord(channelId, messageId, { accountId })
        : await fetchMessageDiscord(channelId, messageId);
      return jsonResult({
        ok: true,
        message: normalizeMessage(message),
        guildId,
        channelId,
        messageId,
      });
    }
    case "readMessages": {
      if (!isActionEnabled("messages")) {
        throw new Error("Discord message reads are disabled.");
      }
      const channelId = resolveChannelId();
      const query = {
        limit:
          typeof params.limit === "number" && Number.isFinite(params.limit)
            ? params.limit
            : undefined,
        before: readStringParam(params, "before"),
        after: readStringParam(params, "after"),
        around: readStringParam(params, "around"),
      };
      const messages = accountId
        ? await readMessagesDiscord(channelId, query, { accountId })
        : await readMessagesDiscord(channelId, query);
      return jsonResult({
        ok: true,
        messages: messages.map((message) => normalizeMessage(message)),
      });
    }
    case "sendMessage": {
      if (!isActionEnabled("messages")) {
        throw new Error("Discord message sends are disabled.");
      }
      const to = readStringParam(params, "to", { required: true });
      const asVoice = params.asVoice === true;
      const silent = params.silent === true;
      const rawComponents = params.components;
      const componentSpec =
        rawComponents && typeof rawComponents === "object" && !Array.isArray(rawComponents)
          ? readDiscordComponentSpec(rawComponents)
          : null;
      const components: DiscordSendComponents | undefined =
        Array.isArray(rawComponents) || typeof rawComponents === "function"
          ? (rawComponents as DiscordSendComponents)
          : undefined;
      const content = readStringParam(params, "content", {
        required: !asVoice && !componentSpec && !components,
        allowEmpty: true,
      });
      const mediaUrl =
        readStringParam(params, "mediaUrl", { trim: false }) ??
        readStringParam(params, "path", { trim: false }) ??
        readStringParam(params, "filePath", { trim: false });
      const filename = readStringParam(params, "filename");
      const replyTo = readStringParam(params, "replyTo");
      const rawEmbeds = params.embeds;
      const embeds: DiscordSendEmbeds | undefined = Array.isArray(rawEmbeds)
        ? (rawEmbeds as DiscordSendEmbeds)
        : undefined;
      const sessionKey = readStringParam(params, "__sessionKey");
      const agentId = readStringParam(params, "__agentId");

      if (componentSpec) {
        if (asVoice) {
          throw new Error("Discord components cannot be sent as voice messages.");
        }
        if (embeds?.length) {
          throw new Error("Discord components cannot include embeds.");
        }
        const normalizedContent = content?.trim() ? content : undefined;
        const payload = componentSpec.text
          ? componentSpec
          : { ...componentSpec, text: normalizedContent };
        const result = await sendDiscordComponentMessage(to, payload, {
          ...(accountId ? { accountId } : {}),
          silent,
          replyTo: replyTo ?? undefined,
          sessionKey: sessionKey ?? undefined,
          agentId: agentId ?? undefined,
          mediaUrl: mediaUrl ?? undefined,
          filename: filename ?? undefined,
        });
        return jsonResult({ ok: true, result, components: true });
      }

      // Handle voice message sending
      if (asVoice) {
        if (!mediaUrl) {
          throw new Error(
            "Voice messages require a media file reference (mediaUrl, path, or filePath).",
          );
        }
        if (content && content.trim()) {
          throw new Error(
            "Voice messages cannot include text content (Discord limitation). Remove the content parameter.",
          );
        }
        assertMediaNotDataUrl(mediaUrl);
        const result = await sendVoiceMessageDiscord(to, mediaUrl, {
          ...(accountId ? { accountId } : {}),
          replyTo,
          silent,
        });
        return jsonResult({ ok: true, result, voiceMessage: true });
      }

      const result = await sendMessageDiscord(to, content ?? "", {
        ...(accountId ? { accountId } : {}),
        mediaUrl,
        mediaLocalRoots: options?.mediaLocalRoots,
        replyTo,
        components,
        embeds,
        silent,
      });
      return jsonResult({ ok: true, result });
    }
    case "editMessage": {
      if (!isActionEnabled("messages")) {
        throw new Error("Discord message edits are disabled.");
      }
      const channelId = resolveChannelId();
      const messageId = readStringParam(params, "messageId", {
        required: true,
      });
      const content = readStringParam(params, "content", {
        required: true,
      });
      const message = accountId
        ? await editMessageDiscord(channelId, messageId, { content }, { accountId })
        : await editMessageDiscord(channelId, messageId, { content });
      return jsonResult({ ok: true, message });
    }
    case "deleteMessage": {
      if (!isActionEnabled("messages")) {
        throw new Error("Discord message deletes are disabled.");
      }
      const channelId = resolveChannelId();
      const messageId = readStringParam(params, "messageId", {
        required: true,
      });
      if (accountId) {
        await deleteMessageDiscord(channelId, messageId, { accountId });
      } else {
        await deleteMessageDiscord(channelId, messageId);
      }
      return jsonResult({ ok: true });
    }
    case "threadCreate": {
      if (!isActionEnabled("threads")) {
        throw new Error("Discord threads are disabled.");
      }
      const channelId = resolveChannelId();
      const name = readStringParam(params, "name", { required: true });
      const messageId = readStringParam(params, "messageId");
      const content = readStringParam(params, "content");
      const autoArchiveMinutesRaw = params.autoArchiveMinutes;
      const autoArchiveMinutes =
        typeof autoArchiveMinutesRaw === "number" && Number.isFinite(autoArchiveMinutesRaw)
          ? autoArchiveMinutesRaw
          : undefined;
      const thread = accountId
        ? await createThreadDiscord(
            channelId,
            { name, messageId, autoArchiveMinutes, content },
            { accountId },
          )
        : await createThreadDiscord(channelId, { name, messageId, autoArchiveMinutes, content });
      return jsonResult({ ok: true, thread });
    }
    case "threadList": {
      if (!isActionEnabled("threads")) {
        throw new Error("Discord threads are disabled.");
      }
      const guildId = readStringParam(params, "guildId", {
        required: true,
      });
      const channelId = readStringParam(params, "channelId");
      const includeArchived =
        typeof params.includeArchived === "boolean" ? params.includeArchived : undefined;
      const before = readStringParam(params, "before");
      const limit =
        typeof params.limit === "number" && Number.isFinite(params.limit)
          ? params.limit
          : undefined;
      const threads = accountId
        ? await listThreadsDiscord(
            {
              guildId,
              channelId,
              includeArchived,
              before,
              limit,
            },
            { accountId },
          )
        : await listThreadsDiscord({
            guildId,
            channelId,
            includeArchived,
            before,
            limit,
          });
      return jsonResult({ ok: true, threads });
    }
    case "threadReply": {
      if (!isActionEnabled("threads")) {
        throw new Error("Discord threads are disabled.");
      }
      const channelId = resolveChannelId();
      const content = readStringParam(params, "content", {
        required: true,
      });
      const mediaUrl = readStringParam(params, "mediaUrl");
      const replyTo = readStringParam(params, "replyTo");
      const result = await sendMessageDiscord(`channel:${channelId}`, content, {
        ...(accountId ? { accountId } : {}),
        mediaUrl,
        mediaLocalRoots: options?.mediaLocalRoots,
        replyTo,
      });
      return jsonResult({ ok: true, result });
    }
    case "pinMessage": {
      if (!isActionEnabled("pins")) {
        throw new Error("Discord pins are disabled.");
      }
      const channelId = resolveChannelId();
      const messageId = readStringParam(params, "messageId", {
        required: true,
      });
      if (accountId) {
        await pinMessageDiscord(channelId, messageId, { accountId });
      } else {
        await pinMessageDiscord(channelId, messageId);
      }
      return jsonResult({ ok: true });
    }
    case "unpinMessage": {
      if (!isActionEnabled("pins")) {
        throw new Error("Discord pins are disabled.");
      }
      const channelId = resolveChannelId();
      const messageId = readStringParam(params, "messageId", {
        required: true,
      });
      if (accountId) {
        await unpinMessageDiscord(channelId, messageId, { accountId });
      } else {
        await unpinMessageDiscord(channelId, messageId);
      }
      return jsonResult({ ok: true });
    }
    case "listPins": {
      if (!isActionEnabled("pins")) {
        throw new Error("Discord pins are disabled.");
      }
      const channelId = resolveChannelId();
      const pins = accountId
        ? await listPinsDiscord(channelId, { accountId })
        : await listPinsDiscord(channelId);
      return jsonResult({ ok: true, pins: pins.map((pin) => normalizeMessage(pin)) });
    }
    case "searchMessages": {
      if (!isActionEnabled("search")) {
        throw new Error("Discord search is disabled.");
      }
      const guildId = readStringParam(params, "guildId", {
        required: true,
      });
      const content = readStringParam(params, "content", {
        required: true,
      });
      const channelId = readStringParam(params, "channelId");
      const channelIds = readStringArrayParam(params, "channelIds");
      const authorId = readStringParam(params, "authorId");
      const authorIds = readStringArrayParam(params, "authorIds");
      const limit =
        typeof params.limit === "number" && Number.isFinite(params.limit)
          ? params.limit
          : undefined;
      const channelIdList = [...(channelIds ?? []), ...(channelId ? [channelId] : [])];
      const authorIdList = [...(authorIds ?? []), ...(authorId ? [authorId] : [])];
      const results = accountId
        ? await searchMessagesDiscord(
            {
              guildId,
              content,
              channelIds: channelIdList.length ? channelIdList : undefined,
              authorIds: authorIdList.length ? authorIdList : undefined,
              limit,
            },
            { accountId },
          )
        : await searchMessagesDiscord({
            guildId,
            content,
            channelIds: channelIdList.length ? channelIdList : undefined,
            authorIds: authorIdList.length ? authorIdList : undefined,
            limit,
          });
      if (!results || typeof results !== "object") {
        return jsonResult({ ok: true, results });
      }
      const resultsRecord = results as Record<string, unknown>;
      const messages = resultsRecord.messages;
      const normalizedMessages = Array.isArray(messages)
        ? messages.map((group) =>
            Array.isArray(group) ? group.map((msg) => normalizeMessage(msg)) : group,
          )
        : messages;
      return jsonResult({
        ok: true,
        results: {
          ...resultsRecord,
          messages: normalizedMessages,
        },
      });
    }
    default:
      throw new Error(`Unknown action: ${action}`);
  }
}
