// Discord plugin module implements handle action behavior.
import type { AgentToolResult } from "openclaw/plugin-sdk/agent-core";
import {
  readPositiveIntegerParam,
  readStringArrayParam,
  readStringParam,
} from "openclaw/plugin-sdk/agent-runtime";
import { readBooleanParam } from "openclaw/plugin-sdk/boolean-param";
import { resolveReactionMessageId } from "openclaw/plugin-sdk/channel-actions";
import type { ChannelMessageActionContext } from "openclaw/plugin-sdk/channel-contract";
import {
  adaptMessagePresentationForChannel,
  normalizeLegacyInteractiveReply,
  normalizeMessagePresentation,
  renderMessagePresentationFallbackText,
} from "openclaw/plugin-sdk/interactive-runtime";
import { normalizeOptionalStringifiedId } from "openclaw/plugin-sdk/string-coerce-runtime";
import { handleDiscordAction } from "../../action-runtime-api.js";
import {
  notifyDiscordActiveTurnThreadCreated,
  notifyDiscordActiveTurnThreadReplyDelivered,
} from "../active-turn-thread-route.js";
import { discordInboundEventDelivery } from "../inbound-event-delivery.js";
import {
  DISCORD_PRESENTATION_CAPABILITIES,
  isDiscordComponentSpecWithinMessageLimit,
} from "../outbound-components.js";
import {
  buildDiscordInteractiveComponents,
  buildDiscordPresentationComponents,
} from "../shared-interactive.js";
import { parseDiscordTarget, resolveDiscordChannelId } from "../targets.js";
import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-admin.js";
import type { DiscordMessagingActionOptions } from "./runtime.messaging.shared.js";
import { readDiscordAutoArchiveDurationParam } from "./runtime.shared.js";

const providerId = "discord";

function withCurrentSourceReplyRoute<T>(result: AgentToolResult<T>): AgentToolResult<T> {
  const details =
    result.details && typeof result.details === "object" && !Array.isArray(result.details)
      ? result.details
      : {};
  return {
    ...result,
    details: { ...details, sourceReplyRoute: "current-source" } as T,
  };
}

function readCurrentDiscordTarget(
  toolContext: Pick<ChannelMessageActionContext, "toolContext">["toolContext"],
): string | undefined {
  const provider = toolContext?.currentChannelProvider?.trim().toLowerCase();
  if (provider && provider !== providerId) {
    return undefined;
  }
  const target = toolContext?.currentChannelId?.trim();
  return target || undefined;
}

export async function handleDiscordMessageAction(
  ctx: Pick<
    ChannelMessageActionContext,
    | "action"
    | "params"
    | "cfg"
    | "accountId"
    | "requesterAccountId"
    | "requesterSenderId"
    | "senderIsOwner"
    | "toolContext"
    | "mediaAccess"
    | "mediaLocalRoots"
    | "mediaReadFile"
    | "sessionKey"
    | "inboundEventKind"
    | "conversationReadOrigin"
  >,
): Promise<AgentToolResult<unknown>> {
  const { action, params, cfg } = ctx;
  const accountId = ctx.accountId ?? readStringParam(params, "accountId");
  const readContext =
    ctx.requesterAccountId &&
    ctx.toolContext?.currentChannelProvider &&
    ctx.toolContext.currentChannelId
      ? {
          requesterAccountId: ctx.requesterAccountId,
          currentChannelProvider: ctx.toolContext.currentChannelProvider,
          currentChannelId: ctx.toolContext.currentChannelId,
        }
      : undefined;
  const readPolicyOptions: DiscordMessagingActionOptions | undefined =
    ctx.conversationReadOrigin || readContext
      ? {
          ...(ctx.conversationReadOrigin
            ? { conversationReadOrigin: ctx.conversationReadOrigin }
            : {}),
          ...(readContext ? { readContext } : {}),
        }
      : undefined;
  const actionOptions = {
    mediaAccess: ctx.mediaAccess,
    mediaLocalRoots: ctx.mediaLocalRoots,
    mediaReadFile: ctx.mediaReadFile,
    ...readPolicyOptions,
  } as const;
  const notifyVisibleOutbound = (
    result: AgentToolResult<unknown>,
    to: string,
    fallbackSessionKey?: string,
  ) => {
    const details =
      result.details && typeof result.details === "object" && !Array.isArray(result.details)
        ? (result.details as { ok?: unknown })
        : undefined;
    // Resolved failures are not delivery receipts; clearing room history would
    // otherwise permanently discard context without any visible reply.
    if (details?.ok !== true) {
      return;
    }
    discordInboundEventDelivery.notify({
      sessionKey: ctx.sessionKey ?? fallbackSessionKey ?? undefined,
      to,
      accountId,
      inboundEventKind: ctx.inboundEventKind,
    });
  };
  const withAdoptedThreadReplyRoute = (
    result: AgentToolResult<unknown>,
    to: string,
    fallbackSessionKey?: string,
  ) => {
    const details =
      result.details && typeof result.details === "object" && !Array.isArray(result.details)
        ? (result.details as { ok?: unknown })
        : undefined;
    // Only a positive runtime receipt may suppress the source fallback. A
    // resolved failure must leave the turn eligible for visible error delivery.
    if (details?.ok !== true) {
      return result;
    }
    let target;
    try {
      target = parseDiscordTarget(to, { defaultKind: "channel" });
    } catch {
      // Route classification runs after delivery and must not turn a
      // successfully resolved Discord target into a failed tool result.
      return result;
    }
    if (
      target?.kind === "channel" &&
      notifyDiscordActiveTurnThreadReplyDelivered({
        sessionKey: ctx.sessionKey ?? fallbackSessionKey,
        accountId,
        threadId: target.id,
      })
    ) {
      return withCurrentSourceReplyRoute(result);
    }
    return result;
  };

  const readTarget = () => {
    const target =
      readStringParam(params, "channelId") ??
      readStringParam(params, "to") ??
      readCurrentDiscordTarget(ctx.toolContext);
    if (!target) {
      throw new Error("Discord channel target is required (use channel:<id>).");
    }
    return target;
  };
  const resolveChannelId = () => resolveDiscordChannelId(readTarget());
  const readSendTarget = () => {
    const target =
      readStringParam(params, "to") ??
      readStringParam(params, "target") ??
      readCurrentDiscordTarget(ctx.toolContext);
    if (!target) {
      throw new Error("Discord channel target is required (use channel:<id>).");
    }
    return target;
  };

  if (action === "send") {
    const to = readSendTarget();
    const asVoice = readBooleanParam(params, "asVoice") === true;
    // Support media, path, and filePath for media URL
    const mediaUrl =
      readStringParam(params, "media", { trim: false }) ??
      readStringParam(params, "path", { trim: false }) ??
      readStringParam(params, "filePath", { trim: false });
    const requestedContent = readStringParam(params, "message", { allowEmpty: true });
    const presentation =
      params.components == null ? normalizeMessagePresentation(params.presentation) : undefined;
    const adaptedPresentation = presentation
      ? adaptMessagePresentationForChannel({
          presentation,
          capabilities: DISCORD_PRESENTATION_CAPABILITIES,
        })
      : undefined;
    const generatedPresentationComponents = buildDiscordPresentationComponents(adaptedPresentation);
    const presentationComponents =
      generatedPresentationComponents &&
      isDiscordComponentSpecWithinMessageLimit({
        spec: generatedPresentationComponents,
        fallbackText: requestedContent,
        includesMedia: Boolean(mediaUrl),
      })
        ? generatedPresentationComponents
        : undefined;
    const presentationFellBack = Boolean(
      generatedPresentationComponents && !presentationComponents,
    );
    const rawComponents = presentationFellBack
      ? undefined
      : (params.components ??
        presentationComponents ??
        buildDiscordInteractiveComponents(normalizeLegacyInteractiveReply(params.interactive)));
    const hasComponents =
      Boolean(rawComponents) &&
      (typeof rawComponents === "function" || typeof rawComponents === "object");
    const components = hasComponents ? rawComponents : undefined;
    const content = readStringParam(params, "message", {
      required: !asVoice && !hasComponents && !mediaUrl && !presentationFellBack,
      allowEmpty: true,
    });
    const deliveryContent =
      presentationFellBack && adaptedPresentation
        ? renderMessagePresentationFallbackText({
            text: content,
            presentation: adaptedPresentation,
          })
        : content;
    const filename = readStringParam(params, "filename");
    const replyTo = readStringParam(params, "replyTo");
    const rawEmbeds = params.embeds;
    const embeds = Array.isArray(rawEmbeds) ? rawEmbeds : undefined;
    const silent = readBooleanParam(params, "silent") === true;
    const suppressEmbeds = readBooleanParam(params, "suppressEmbeds");
    const sessionKey = readStringParam(params, "__sessionKey");
    const agentId = readStringParam(params, "__agentId");
    const threadName = readStringParam(params, "threadName");
    const result = await handleDiscordAction(
      {
        action: "sendMessage",
        accountId: accountId ?? undefined,
        to,
        content: deliveryContent ?? "",
        ...(threadName ? { threadName } : {}),
        mediaUrl: mediaUrl ?? undefined,
        filename: filename ?? undefined,
        replyTo: replyTo ?? undefined,
        components,
        embeds,
        asVoice,
        silent,
        ...(suppressEmbeds === undefined ? {} : { suppressEmbeds }),
        __sessionKey: sessionKey ?? undefined,
        __agentId: agentId ?? undefined,
      },
      cfg,
      actionOptions,
    );
    notifyVisibleOutbound(result, to, sessionKey);
    return withAdoptedThreadReplyRoute(result, to, sessionKey);
  }

  if (action === "upload-file") {
    const to = readSendTarget();
    const mediaUrl =
      readStringParam(params, "filePath", { trim: false }) ??
      readStringParam(params, "path", { trim: false }) ??
      readStringParam(params, "media", { trim: false });
    if (!mediaUrl) {
      throw new Error("upload-file requires filePath, path, or media.");
    }
    const content =
      readStringParam(params, "message", { allowEmpty: true }) ??
      readStringParam(params, "content", { allowEmpty: true });
    const filename = readStringParam(params, "filename");
    const replyTo = readStringParam(params, "replyTo");
    const silent = readBooleanParam(params, "silent") === true;
    const suppressEmbeds = readBooleanParam(params, "suppressEmbeds");
    const sessionKey = readStringParam(params, "__sessionKey");
    const agentId = readStringParam(params, "__agentId");
    const result = await handleDiscordAction(
      {
        action: "sendMessage",
        accountId: accountId ?? undefined,
        to,
        content: content ?? "",
        mediaUrl,
        filename: filename ?? undefined,
        replyTo: replyTo ?? undefined,
        silent,
        ...(suppressEmbeds === undefined ? {} : { suppressEmbeds }),
        __sessionKey: sessionKey ?? undefined,
        __agentId: agentId ?? undefined,
      },
      cfg,
      actionOptions,
    );
    notifyVisibleOutbound(result, to, sessionKey);
    return withAdoptedThreadReplyRoute(result, to, sessionKey);
  }

  if (action === "poll") {
    const to = readStringParam(params, "to", { required: true });
    const question = readStringParam(params, "pollQuestion", {
      required: true,
    });
    const answers = readStringArrayParam(params, "pollOption", { required: true });
    const allowMultiselect = readBooleanParam(params, "pollMulti");
    const durationHours = readPositiveIntegerParam(params, "pollDurationHours");
    const result = await handleDiscordAction(
      {
        action: "poll",
        accountId: accountId ?? undefined,
        to,
        question,
        answers,
        allowMultiselect,
        durationHours: durationHours ?? undefined,
        content: readStringParam(params, "message"),
      },
      cfg,
      actionOptions,
    );
    notifyVisibleOutbound(result, to);
    return result;
  }

  if (action === "react") {
    const messageIdRaw = resolveReactionMessageId({ args: params, toolContext: ctx.toolContext });
    const messageId = normalizeOptionalStringifiedId(messageIdRaw) ?? "";
    if (!messageId) {
      throw new Error(
        "messageId required. Provide messageId explicitly or react to the current inbound message.",
      );
    }
    const emoji = readStringParam(params, "emoji", { allowEmpty: true });
    const remove = readBooleanParam(params, "remove");
    return await handleDiscordAction(
      {
        action: "react",
        accountId: accountId ?? undefined,
        channelId: readTarget(),
        messageId,
        emoji,
        remove,
      },
      cfg,
      actionOptions,
    );
  }

  if (action === "reactions") {
    const messageId = readStringParam(params, "messageId", { required: true });
    const limit = readPositiveIntegerParam(params, "limit");
    return await handleDiscordAction(
      {
        action: "reactions",
        accountId: accountId ?? undefined,
        channelId: readTarget(),
        messageId,
        limit,
      },
      cfg,
      actionOptions,
    );
  }

  if (action === "read") {
    const limit = readPositiveIntegerParam(params, "limit");
    return await handleDiscordAction(
      {
        action: "readMessages",
        accountId: accountId ?? undefined,
        channelId: resolveChannelId(),
        limit,
        before: readStringParam(params, "before"),
        after: readStringParam(params, "after"),
        around: readStringParam(params, "around"),
      },
      cfg,
      actionOptions,
    );
  }

  if (action === "edit") {
    const messageId = readStringParam(params, "messageId", { required: true });
    const content = readStringParam(params, "message", { required: true });
    return await handleDiscordAction(
      {
        action: "editMessage",
        accountId: accountId ?? undefined,
        channelId: resolveChannelId(),
        messageId,
        content,
      },
      cfg,
      actionOptions,
    );
  }

  if (action === "delete") {
    const messageId = readStringParam(params, "messageId", { required: true });
    return await handleDiscordAction(
      {
        action: "deleteMessage",
        accountId: accountId ?? undefined,
        channelId: resolveChannelId(),
        messageId,
      },
      cfg,
      actionOptions,
    );
  }

  if (action === "pin" || action === "unpin" || action === "list-pins") {
    const messageId =
      action === "list-pins" ? undefined : readStringParam(params, "messageId", { required: true });
    return await handleDiscordAction(
      {
        action: action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins",
        accountId: accountId ?? undefined,
        channelId: resolveChannelId(),
        messageId,
      },
      cfg,
      actionOptions,
    );
  }

  if (action === "permissions") {
    return await handleDiscordAction(
      {
        action: "permissions",
        accountId: accountId ?? undefined,
        channelId: resolveChannelId(),
      },
      cfg,
      actionOptions,
    );
  }

  if (action === "thread-create") {
    const name = readStringParam(params, "threadName", { required: true });
    const messageId = readStringParam(params, "messageId");
    const content = readStringParam(params, "message");
    const autoArchiveMinutes = readDiscordAutoArchiveDurationParam(params, "autoArchiveMin");
    const appliedTags = readStringArrayParam(params, "appliedTags");
    const result = await handleDiscordAction(
      {
        action: "threadCreate",
        accountId: accountId ?? undefined,
        channelId: resolveChannelId(),
        name,
        messageId,
        content,
        autoArchiveMinutes,
        appliedTags: appliedTags ?? undefined,
      },
      cfg,
      actionOptions,
    );
    const details =
      result.details && typeof result.details === "object" && !Array.isArray(result.details)
        ? (result.details as { ok?: unknown; thread?: { id?: unknown } })
        : undefined;
    if (details?.ok === true) {
      const threadId = typeof details.thread?.id === "string" ? details.thread.id : undefined;
      await notifyDiscordActiveTurnThreadCreated({
        sessionKey: ctx.sessionKey,
        accountId,
        sourceChannelId: resolveChannelId(),
        sourceMessageId: messageId,
        threadId,
      });
    }
    notifyVisibleOutbound(result, resolveChannelId());
    return result;
  }

  if (action === "sticker") {
    const to = readStringParam(params, "to", { required: true });
    const stickerIds =
      readStringArrayParam(params, "stickerId", {
        required: true,
        label: "sticker-id",
      }) ?? [];
    const result = await handleDiscordAction(
      {
        action: "sticker",
        accountId: accountId ?? undefined,
        to,
        stickerIds,
        content: readStringParam(params, "message"),
      },
      cfg,
      actionOptions,
    );
    notifyVisibleOutbound(result, to);
    return result;
  }

  if (action === "set-presence") {
    return await handleDiscordAction(
      {
        action: "setPresence",
        accountId: accountId ?? undefined,
        status: readStringParam(params, "status"),
        activityType: readStringParam(params, "activityType"),
        activityName: readStringParam(params, "activityName"),
        activityUrl: readStringParam(params, "activityUrl"),
        activityState: readStringParam(params, "activityState"),
      },
      cfg,
      actionOptions,
    );
  }

  const adminResult = await tryHandleDiscordMessageActionGuildAdmin({
    ctx,
    resolveChannelId,
    readPolicyOptions,
    actionOptions,
  });
  if (adminResult !== undefined) {
    if (action === "thread-reply") {
      const threadId = readStringParam(params, "threadId") ?? readTarget();
      notifyVisibleOutbound(adminResult, threadId);
      return withAdoptedThreadReplyRoute(adminResult, threadId);
    }
    return adminResult;
  }

  throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
}
