import { Type } from "@sinclair/typebox";

import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import {
  type MessagePollResult,
  type MessageSendResult,
  sendMessage,
  sendPoll,
} from "../../infra/outbound/message.js";
import { resolveMessageProviderSelection } from "../../infra/outbound/provider-selection.js";
import {
  dispatchProviderMessageAction,
  listProviderMessageActions,
  supportsProviderMessageButtons,
} from "../../providers/plugins/message-actions.js";
import type { ProviderMessageActionName } from "../../providers/plugins/types.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import {
  GATEWAY_CLIENT_MODES,
  GATEWAY_CLIENT_NAMES,
} from "../../utils/message-provider.js";
import type { AnyAgentTool } from "./common.js";
import {
  jsonResult,
  readNumberParam,
  readStringArrayParam,
  readStringParam,
} from "./common.js";

const AllMessageActions = [
  "send",
  "poll",
  "react",
  "reactions",
  "read",
  "edit",
  "delete",
  "pin",
  "unpin",
  "list-pins",
  "permissions",
  "thread-create",
  "thread-list",
  "thread-reply",
  "search",
  "sticker",
  "member-info",
  "role-info",
  "emoji-list",
  "emoji-upload",
  "sticker-upload",
  "role-add",
  "role-remove",
  "channel-info",
  "channel-list",
  "channel-create",
  "channel-edit",
  "channel-delete",
  "channel-move",
  "category-create",
  "category-edit",
  "category-delete",
  "voice-status",
  "event-list",
  "event-create",
  "timeout",
  "kick",
  "ban",
];

const MessageToolCommonSchema = {
  provider: Type.Optional(Type.String()),
  to: Type.Optional(Type.String()),
  message: Type.Optional(Type.String()),
  media: Type.Optional(Type.String()),
  buttons: Type.Optional(
    Type.Array(
      Type.Array(
        Type.Object({
          text: Type.String(),
          callback_data: Type.String(),
        }),
      ),
      {
        description: "Telegram inline keyboard buttons (array of button rows)",
      },
    ),
  ),
  messageId: Type.Optional(Type.String()),
  replyTo: Type.Optional(Type.String()),
  threadId: Type.Optional(Type.String()),
  accountId: Type.Optional(Type.String()),
  dryRun: Type.Optional(Type.Boolean()),
  bestEffort: Type.Optional(Type.Boolean()),
  gifPlayback: Type.Optional(Type.Boolean()),
  emoji: Type.Optional(Type.String()),
  remove: Type.Optional(Type.Boolean()),
  limit: Type.Optional(Type.Number()),
  before: Type.Optional(Type.String()),
  after: Type.Optional(Type.String()),
  around: Type.Optional(Type.String()),
  pollQuestion: Type.Optional(Type.String()),
  pollOption: Type.Optional(Type.Array(Type.String())),
  pollDurationHours: Type.Optional(Type.Number()),
  pollMulti: Type.Optional(Type.Boolean()),
  channelId: Type.Optional(Type.String()),
  channelIds: Type.Optional(Type.Array(Type.String())),
  guildId: Type.Optional(Type.String()),
  userId: Type.Optional(Type.String()),
  authorId: Type.Optional(Type.String()),
  authorIds: Type.Optional(Type.Array(Type.String())),
  roleId: Type.Optional(Type.String()),
  roleIds: Type.Optional(Type.Array(Type.String())),
  emojiName: Type.Optional(Type.String()),
  stickerId: Type.Optional(Type.Array(Type.String())),
  stickerName: Type.Optional(Type.String()),
  stickerDesc: Type.Optional(Type.String()),
  stickerTags: Type.Optional(Type.String()),
  threadName: Type.Optional(Type.String()),
  autoArchiveMin: Type.Optional(Type.Number()),
  query: Type.Optional(Type.String()),
  eventName: Type.Optional(Type.String()),
  eventType: Type.Optional(Type.String()),
  startTime: Type.Optional(Type.String()),
  endTime: Type.Optional(Type.String()),
  desc: Type.Optional(Type.String()),
  location: Type.Optional(Type.String()),
  durationMin: Type.Optional(Type.Number()),
  until: Type.Optional(Type.String()),
  reason: Type.Optional(Type.String()),
  deleteDays: Type.Optional(Type.Number()),
  includeArchived: Type.Optional(Type.Boolean()),
  participant: Type.Optional(Type.String()),
  fromMe: Type.Optional(Type.Boolean()),
  gatewayUrl: Type.Optional(Type.String()),
  gatewayToken: Type.Optional(Type.String()),
  timeoutMs: Type.Optional(Type.Number()),
  name: Type.Optional(Type.String()),
  type: Type.Optional(Type.Number()),
  parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
  topic: Type.Optional(Type.String()),
  position: Type.Optional(Type.Number()),
  nsfw: Type.Optional(Type.Boolean()),
  rateLimitPerUser: Type.Optional(Type.Number()),
  categoryId: Type.Optional(Type.String()),
};

function buildMessageToolSchemaFromActions(
  actions: string[],
  options: { includeButtons: boolean },
) {
  const props: Record<string, unknown> = { ...MessageToolCommonSchema };
  if (!options.includeButtons) delete props.buttons;

  const schemas: Array<ReturnType<typeof Type.Object>> = [];
  if (actions.includes("send")) {
    schemas.push(
      Type.Object({
        action: Type.Literal("send"),
        to: Type.String(),
        message: Type.String(),
        ...props,
      }),
    );
  }

  const nonSendActions = actions.filter((action) => action !== "send");
  if (nonSendActions.length > 0) {
    schemas.push(
      Type.Object({
        action: Type.Union(
          nonSendActions.map((action) => Type.Literal(action)),
        ),
        ...props,
      }),
    );
  }

  return schemas.length === 1 ? schemas[0] : Type.Union(schemas);
}

const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, {
  includeButtons: true,
});

type MessageToolOptions = {
  agentAccountId?: string;
  config?: ClawdbotConfig;
  currentChannelId?: string;
  currentThreadTs?: string;
  replyToMode?: "off" | "first" | "all";
  hasRepliedRef?: { value: boolean };
};

function buildMessageToolSchema(cfg: ClawdbotConfig) {
  const actions = listProviderMessageActions(cfg);
  const includeButtons = supportsProviderMessageButtons(cfg);
  return buildMessageToolSchemaFromActions(
    actions.length > 0 ? actions : ["send"],
    { includeButtons },
  );
}

function resolveAgentAccountId(value?: string): string | undefined {
  const trimmed = value?.trim();
  if (!trimmed) return undefined;
  return normalizeAccountId(trimmed);
}

export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
  const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
  const schema = options?.config
    ? buildMessageToolSchema(options.config)
    : MessageToolSchema;

  return {
    label: "Message",
    name: "message",
    description:
      "Send messages and provider actions (polls, reactions, pins, threads, etc.) via configured provider plugins.",
    parameters: schema,
    execute: async (_toolCallId, args) => {
      const params = args as Record<string, unknown>;
      const cfg = options?.config ?? loadConfig();
      const action = readStringParam(params, "action", {
        required: true,
      }) as ProviderMessageActionName;

      const providerSelection = await resolveMessageProviderSelection({
        cfg,
        provider: readStringParam(params, "provider"),
      });
      const provider = providerSelection.provider;
      const accountId = readStringParam(params, "accountId") ?? agentAccountId;
      const dryRun = Boolean(params.dryRun);

      const gateway = {
        url: readStringParam(params, "gatewayUrl", { trim: false }),
        token: readStringParam(params, "gatewayToken", { trim: false }),
        timeoutMs: readNumberParam(params, "timeoutMs"),
        clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
        clientDisplayName: "agent",
        mode: GATEWAY_CLIENT_MODES.BACKEND,
      };

      const toolContext =
        options?.currentChannelId ||
        options?.currentThreadTs ||
        options?.replyToMode ||
        options?.hasRepliedRef
          ? {
              currentChannelId: options?.currentChannelId,
              currentThreadTs: options?.currentThreadTs,
              replyToMode: options?.replyToMode,
              hasRepliedRef: options?.hasRepliedRef,
            }
          : undefined;

      if (action === "send") {
        const to = readStringParam(params, "to", { required: true });
        let message = readStringParam(params, "message", {
          required: true,
          allowEmpty: true,
        });

        // Let send accept the same inline directives we use elsewhere.
        // Provider plugins consume `replyTo` / `media` / `buttons` from params.
        const parsed = parseReplyDirectives(message);
        message = parsed.text;
        params.message = message;
        if (!params.replyTo && parsed.replyToId)
          params.replyTo = parsed.replyToId;
        if (!params.media) {
          params.media = parsed.mediaUrls?.[0] || parsed.mediaUrl || undefined;
        }

        const mediaUrl = readStringParam(params, "media", { trim: false });
        const gifPlayback =
          typeof params.gifPlayback === "boolean" ? params.gifPlayback : false;
        const bestEffort =
          typeof params.bestEffort === "boolean"
            ? params.bestEffort
            : undefined;

        if (dryRun) {
          const result: MessageSendResult = await sendMessage({
            to,
            content: message,
            mediaUrl: mediaUrl || undefined,
            provider: provider || undefined,
            accountId: accountId ?? undefined,
            gifPlayback,
            dryRun,
            bestEffort,
            gateway,
          });
          return jsonResult(result);
        }

        const handled = await dispatchProviderMessageAction({
          provider,
          action,
          cfg,
          params,
          accountId,
          gateway,
          toolContext,
          dryRun,
        });
        if (handled) return handled;

        const result: MessageSendResult = await sendMessage({
          to,
          content: message,
          mediaUrl: mediaUrl || undefined,
          provider: provider || undefined,
          accountId: accountId ?? undefined,
          gifPlayback,
          dryRun,
          bestEffort,
          gateway,
        });
        return jsonResult(result);
      }

      if (action === "poll") {
        const to = readStringParam(params, "to", { required: true });
        const question = readStringParam(params, "pollQuestion", {
          required: true,
        });
        const options =
          readStringArrayParam(params, "pollOption", { required: true }) ?? [];
        const allowMultiselect =
          typeof params.pollMulti === "boolean" ? params.pollMulti : undefined;
        const durationHours = readNumberParam(params, "pollDurationHours", {
          integer: true,
        });

        const maxSelections = allowMultiselect
          ? Math.max(2, options.length)
          : 1;

        if (dryRun) {
          const result: MessagePollResult = await sendPoll({
            to,
            question,
            options,
            maxSelections,
            durationHours: durationHours ?? undefined,
            provider,
            dryRun,
            gateway,
          });
          return jsonResult(result);
        }

        const handled = await dispatchProviderMessageAction({
          provider,
          action,
          cfg,
          params,
          accountId,
          gateway,
          toolContext,
          dryRun,
        });
        if (handled) return handled;

        const result: MessagePollResult = await sendPoll({
          to,
          question,
          options,
          maxSelections,
          durationHours: durationHours ?? undefined,
          provider,
          dryRun,
          gateway,
        });
        return jsonResult(result);
      }

      const handled = await dispatchProviderMessageAction({
        provider,
        action,
        cfg,
        params,
        accountId,
        gateway,
        toolContext,
        dryRun,
      });
      if (handled) return handled;

      throw new Error(
        `Message action ${action} not supported for provider ${provider}.`,
      );
    },
  };
}
