import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
import type { PollInput } from "openclaw/plugin-sdk";
import { getMatrixRuntime } from "../runtime.js";
import { buildPollStartContent, M_POLL_START } from "./poll-types.js";
import { enqueueSend } from "./send-queue.js";
import { resolveMatrixClient, resolveMediaMaxBytes } from "./send/client.js";
import {
  buildReplyRelation,
  buildTextContent,
  buildThreadRelation,
  resolveMatrixMsgType,
  resolveMatrixVoiceDecision,
} from "./send/formatting.js";
import {
  buildMediaContent,
  prepareImageInfo,
  resolveMediaDurationMs,
  uploadMediaMaybeEncrypted,
} from "./send/media.js";
import { normalizeThreadId, resolveMatrixRoomId } from "./send/targets.js";
import {
  EventType,
  MsgType,
  RelationType,
  type MatrixOutboundContent,
  type MatrixSendOpts,
  type MatrixSendResult,
  type ReactionEventContent,
} from "./send/types.js";

const MATRIX_TEXT_LIMIT = 4000;
const getCore = () => getMatrixRuntime();

export type { MatrixSendOpts, MatrixSendResult } from "./send/types.js";
export { resolveMatrixRoomId } from "./send/targets.js";

export async function sendMessageMatrix(
  to: string,
  message: string,
  opts: MatrixSendOpts = {},
): Promise<MatrixSendResult> {
  const trimmedMessage = message?.trim() ?? "";
  if (!trimmedMessage && !opts.mediaUrl) {
    throw new Error("Matrix send requires text or media");
  }
  const { client, stopOnDone } = await resolveMatrixClient({
    client: opts.client,
    timeoutMs: opts.timeoutMs,
    accountId: opts.accountId,
  });
  try {
    const roomId = await resolveMatrixRoomId(client, to);
    return await enqueueSend(roomId, async () => {
      const cfg = getCore().config.loadConfig();
      const tableMode = getCore().channel.text.resolveMarkdownTableMode({
        cfg,
        channel: "matrix",
        accountId: opts.accountId,
      });
      const convertedMessage = getCore().channel.text.convertMarkdownTables(
        trimmedMessage,
        tableMode,
      );
      const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix");
      const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT);
      const chunkMode = getCore().channel.text.resolveChunkMode(cfg, "matrix", opts.accountId);
      const chunks = getCore().channel.text.chunkMarkdownTextWithMode(
        convertedMessage,
        chunkLimit,
        chunkMode,
      );
      const threadId = normalizeThreadId(opts.threadId);
      const relation = threadId
        ? buildThreadRelation(threadId, opts.replyToId)
        : buildReplyRelation(opts.replyToId);
      const sendContent = async (content: MatrixOutboundContent) => {
        // @vector-im/matrix-bot-sdk uses sendMessage differently
        const eventId = await client.sendMessage(roomId, content);
        return eventId;
      };

      let lastMessageId = "";
      if (opts.mediaUrl) {
        const maxBytes = resolveMediaMaxBytes(opts.accountId);
        const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes);
        const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, {
          contentType: media.contentType,
          filename: media.fileName,
        });
        const durationMs = await resolveMediaDurationMs({
          buffer: media.buffer,
          contentType: media.contentType,
          fileName: media.fileName,
          kind: media.kind,
        });
        const baseMsgType = resolveMatrixMsgType(media.contentType, media.fileName);
        const { useVoice } = resolveMatrixVoiceDecision({
          wantsVoice: opts.audioAsVoice === true,
          contentType: media.contentType,
          fileName: media.fileName,
        });
        const msgtype = useVoice ? MsgType.Audio : baseMsgType;
        const isImage = msgtype === MsgType.Image;
        const imageInfo = isImage
          ? await prepareImageInfo({ buffer: media.buffer, client })
          : undefined;
        const [firstChunk, ...rest] = chunks;
        const body = useVoice ? "Voice message" : (firstChunk ?? media.fileName ?? "(file)");
        const content = buildMediaContent({
          msgtype,
          body,
          url: uploaded.url,
          file: uploaded.file,
          filename: media.fileName,
          mimetype: media.contentType,
          size: media.buffer.byteLength,
          durationMs,
          relation,
          isVoice: useVoice,
          imageInfo,
        });
        const eventId = await sendContent(content);
        lastMessageId = eventId ?? lastMessageId;
        const textChunks = useVoice ? chunks : rest;
        const followupRelation = threadId ? relation : undefined;
        for (const chunk of textChunks) {
          const text = chunk.trim();
          if (!text) {
            continue;
          }
          const followup = buildTextContent(text, followupRelation);
          const followupEventId = await sendContent(followup);
          lastMessageId = followupEventId ?? lastMessageId;
        }
      } else {
        for (const chunk of chunks.length ? chunks : [""]) {
          const text = chunk.trim();
          if (!text) {
            continue;
          }
          const content = buildTextContent(text, relation);
          const eventId = await sendContent(content);
          lastMessageId = eventId ?? lastMessageId;
        }
      }

      return {
        messageId: lastMessageId || "unknown",
        roomId,
      };
    });
  } finally {
    if (stopOnDone) {
      client.stop();
    }
  }
}

export async function sendPollMatrix(
  to: string,
  poll: PollInput,
  opts: MatrixSendOpts = {},
): Promise<{ eventId: string; roomId: string }> {
  if (!poll.question?.trim()) {
    throw new Error("Matrix poll requires a question");
  }
  if (!poll.options?.length) {
    throw new Error("Matrix poll requires options");
  }
  const { client, stopOnDone } = await resolveMatrixClient({
    client: opts.client,
    timeoutMs: opts.timeoutMs,
    accountId: opts.accountId,
  });

  try {
    const roomId = await resolveMatrixRoomId(client, to);
    const pollContent = buildPollStartContent(poll);
    const threadId = normalizeThreadId(opts.threadId);
    const pollPayload = threadId
      ? { ...pollContent, "m.relates_to": buildThreadRelation(threadId) }
      : pollContent;
    // @vector-im/matrix-bot-sdk sendEvent returns eventId string directly
    const eventId = await client.sendEvent(roomId, M_POLL_START, pollPayload);

    return {
      eventId: eventId ?? "unknown",
      roomId,
    };
  } finally {
    if (stopOnDone) {
      client.stop();
    }
  }
}

export async function sendTypingMatrix(
  roomId: string,
  typing: boolean,
  timeoutMs?: number,
  client?: MatrixClient,
): Promise<void> {
  const { client: resolved, stopOnDone } = await resolveMatrixClient({
    client,
    timeoutMs,
  });
  try {
    const resolvedTimeoutMs = typeof timeoutMs === "number" ? timeoutMs : 30_000;
    await resolved.setTyping(roomId, typing, resolvedTimeoutMs);
  } finally {
    if (stopOnDone) {
      resolved.stop();
    }
  }
}

export async function sendReadReceiptMatrix(
  roomId: string,
  eventId: string,
  client?: MatrixClient,
): Promise<void> {
  if (!eventId?.trim()) {
    return;
  }
  const { client: resolved, stopOnDone } = await resolveMatrixClient({
    client,
  });
  try {
    const resolvedRoom = await resolveMatrixRoomId(resolved, roomId);
    await resolved.sendReadReceipt(resolvedRoom, eventId.trim());
  } finally {
    if (stopOnDone) {
      resolved.stop();
    }
  }
}

export async function reactMatrixMessage(
  roomId: string,
  messageId: string,
  emoji: string,
  client?: MatrixClient,
): Promise<void> {
  if (!emoji.trim()) {
    throw new Error("Matrix reaction requires an emoji");
  }
  const { client: resolved, stopOnDone } = await resolveMatrixClient({
    client,
  });
  try {
    const resolvedRoom = await resolveMatrixRoomId(resolved, roomId);
    const reaction: ReactionEventContent = {
      "m.relates_to": {
        rel_type: RelationType.Annotation,
        event_id: messageId,
        key: emoji,
      },
    };
    await resolved.sendEvent(resolvedRoom, EventType.Reaction, reaction);
  } finally {
    if (stopOnDone) {
      resolved.stop();
    }
  }
}
