// Whatsapp plugin module implements send behavior.
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { generateSecureUuid } from "openclaw/plugin-sdk/core";
import { redactIdentifier } from "openclaw/plugin-sdk/logging-core";
import {
  convertMarkdownTables,
  resolveMarkdownTableMode,
} from "openclaw/plugin-sdk/markdown-table-runtime";
import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
import { normalizePollInput, type PollInput } from "openclaw/plugin-sdk/poll-runtime";
import { createSubsystemLogger, getChildLogger } from "openclaw/plugin-sdk/runtime-env";
import {
  resolveDefaultWhatsAppAccountId,
  resolveWhatsAppAccount,
  resolveWhatsAppMediaMaxBytes,
} from "./accounts.js";
import { registerWhatsAppApprovalReactionTargetForOutboundMessage } from "./approval-reactions.js";
import { getRegisteredWhatsAppConnectionController } from "./connection-controller-registry.js";
import { resolveWhatsAppDocumentFileName } from "./document-filename.js";
import type { ActiveWebListener, ActiveWebSendOptions } from "./inbound/types.js";
import { isWhatsAppNewsletterJid } from "./normalize.js";
import {
  normalizeWhatsAppPayloadText,
  prepareWhatsAppOutboundMedia,
  resolveWhatsAppOutboundMediaUrls,
} from "./outbound-media-contract.js";
import { loadOutboundMediaFromUrl } from "./outbound-media.runtime.js";
import { markdownToWhatsApp, toWhatsappJid } from "./text-runtime.js";

const outboundLog = createSubsystemLogger("gateway/channels/whatsapp").child("outbound");

function supportsForcedDocumentDelivery(kind: "image" | "audio" | "video" | "document"): boolean {
  return kind === "image" || kind === "video";
}

type PreparedWhatsAppOutboundMedia = Awaited<ReturnType<typeof prepareWhatsAppOutboundMedia>>;

type WhatsAppMediaSendState = {
  mediaBuffer: Buffer;
  mediaType: string;
  text: string;
  forceDocumentDelivery: boolean;
  documentFileName?: string;
  visibleTextAfterVoice?: string;
};

function buildWhatsAppMediaSendState(params: {
  media: PreparedWhatsAppOutboundMedia;
  caption?: string;
  forceDocument?: boolean;
}): WhatsAppMediaSendState {
  const { media, caption } = params;
  const forceDocumentDelivery = Boolean(
    params.forceDocument && supportsForcedDocumentDelivery(media.kind),
  );
  let text = caption ?? "";
  let documentFileName = media.kind === "document" ? media.fileName : undefined;
  let visibleTextAfterVoice: string | undefined;
  if (media.kind === "audio" && caption) {
    visibleTextAfterVoice = caption;
    text = "";
  }
  if (forceDocumentDelivery) {
    documentFileName ??= resolveWhatsAppDocumentFileName({
      fileName: media.fileName,
      mimetype: media.mimetype,
    });
  }
  return {
    mediaBuffer: media.buffer,
    mediaType: media.mimetype,
    text,
    forceDocumentDelivery,
    ...(documentFileName ? { documentFileName } : {}),
    ...(visibleTextAfterVoice ? { visibleTextAfterVoice } : {}),
  };
}

function resolveOutboundWhatsAppAccountId(params: {
  cfg: OpenClawConfig;
  accountId?: string;
}): string | undefined {
  const explicitAccountId = params.accountId?.trim();
  if (explicitAccountId) {
    return explicitAccountId;
  }
  return resolveDefaultWhatsAppAccountId(params.cfg);
}

function requireOutboundActiveWebListener(params: { cfg: OpenClawConfig; accountId?: string }): {
  accountId: string;
  listener: ActiveWebListener;
} {
  const accountId = resolveOutboundWhatsAppAccountId(params);
  const resolvedAccountId = accountId ?? resolveDefaultWhatsAppAccountId(params.cfg);
  const listener =
    getRegisteredWhatsAppConnectionController(resolvedAccountId)?.getActiveListener() ?? null;
  if (!listener) {
    throw new Error(
      `No active WhatsApp Web listener (account: ${resolvedAccountId}). Start the gateway, then link WhatsApp with: ${formatCliCommand(`openclaw channels login --channel whatsapp --account ${resolvedAccountId}`)}.`,
    );
  }
  return { accountId: resolvedAccountId, listener };
}

function resolveActualSentRemoteJid(result: unknown, fallbackJid: string): string {
  if (!result || typeof result !== "object") {
    return fallbackJid;
  }
  const rawKeys = (result as { keys?: unknown }).keys;
  const keys: Array<{ remoteJid?: unknown }> = Array.isArray(rawKeys) ? rawKeys : [];
  for (const key of keys) {
    if (typeof key?.remoteJid === "string" && key.remoteJid.trim()) {
      return key.remoteJid.trim();
    }
  }
  return fallbackJid;
}

export async function sendMessageWhatsApp(
  to: string,
  body: string,
  options: {
    verbose: boolean;
    cfg: OpenClawConfig;
    mediaUrl?: string;
    mediaUrls?: readonly string[];
    mediaAccess?: {
      localRoots?: readonly string[];
      readFile?: (filePath: string) => Promise<Buffer>;
    };
    mediaLocalRoots?: readonly string[];
    mediaReadFile?: (filePath: string) => Promise<Buffer>;
    mediaPayload?: {
      buffer: Buffer;
      contentType?: string;
      kind?: "image" | "audio" | "video" | "document";
      fileName?: string;
    };
    gifPlayback?: boolean;
    audioAsVoice?: boolean;
    forceDocument?: boolean;
    accountId?: string;
    quotedMessageKey?: {
      id: string;
      remoteJid: string;
      fromMe: boolean;
      participant?: string;
      messageText?: string;
    };
    preserveLeadingWhitespace?: boolean;
    /** Report each accepted internal platform send before the next fallible send. */
    onDeliveryResult?: (result: { messageId: string; toJid: string }) => Promise<void> | void;
  },
): Promise<{ messageId: string; toJid: string }> {
  let text = options.preserveLeadingWhitespace ? body : normalizeWhatsAppPayloadText(body);
  const jid = toWhatsappJid(to);
  const mediaUrls = resolveWhatsAppOutboundMediaUrls(options);
  const mediaPayload = options.mediaPayload;
  const primaryMediaUrl = mediaUrls[0] ?? mediaPayload?.fileName;
  const hasMedia = Boolean(mediaPayload || primaryMediaUrl);
  if (!text && !hasMedia) {
    return { messageId: "", toJid: jid };
  }
  const correlationId = generateSecureUuid();
  const startedAt = Date.now();
  const cfg = requireRuntimeConfig(options.cfg, "WhatsApp send");
  const { listener: active, accountId: resolvedAccountId } = requireOutboundActiveWebListener({
    cfg,
    accountId: options.accountId,
  });
  const account = resolveWhatsAppAccount({
    cfg,
    accountId: resolvedAccountId ?? options.accountId,
  });
  const tableMode = resolveMarkdownTableMode({
    cfg,
    channel: "whatsapp",
    accountId: resolvedAccountId ?? options.accountId,
  });
  text = convertMarkdownTables(text ?? "", tableMode);
  text = markdownToWhatsApp(text);
  const redactedTo = redactIdentifier(to);
  const logger = getChildLogger({
    module: "web-outbound",
    correlationId,
    to: redactedTo,
  });
  try {
    const redactedJid = redactIdentifier(jid);
    let mediaBuffer: Buffer | undefined;
    let mediaType: string | undefined;
    let documentFileName: string | undefined;
    let visibleTextAfterVoice: string | undefined;
    let forceDocumentDelivery = false;
    let media: PreparedWhatsAppOutboundMedia | undefined;
    if (mediaPayload) {
      media = await prepareWhatsAppOutboundMedia(mediaPayload, primaryMediaUrl);
    } else if (primaryMediaUrl) {
      media = await prepareWhatsAppOutboundMedia(
        await loadOutboundMediaFromUrl(primaryMediaUrl, {
          maxBytes: resolveWhatsAppMediaMaxBytes(account),
          optimizeImages: options.forceDocument ? false : undefined,
          mediaAccess: options.mediaAccess,
          mediaLocalRoots: options.mediaLocalRoots,
          mediaReadFile: options.mediaReadFile,
        }),
        primaryMediaUrl,
      );
    }
    if (media) {
      const mediaSendState = buildWhatsAppMediaSendState({
        media,
        caption: text || undefined,
        forceDocument: options.forceDocument,
      });
      mediaBuffer = mediaSendState.mediaBuffer;
      mediaType = mediaSendState.mediaType;
      documentFileName = mediaSendState.documentFileName;
      visibleTextAfterVoice = mediaSendState.visibleTextAfterVoice;
      forceDocumentDelivery = mediaSendState.forceDocumentDelivery;
      text = mediaSendState.text;
    }
    outboundLog.info(`Sending message -> ${redactedJid}${hasMedia ? " (media)" : ""}`);
    logger.info({ jid: redactedJid, hasMedia }, "sending message");
    if (!isWhatsAppNewsletterJid(jid)) {
      await active.sendComposingTo(to);
    }
    const hasExplicitAccountId = Boolean(options.accountId?.trim());
    const accountId = hasExplicitAccountId ? resolvedAccountId : undefined;
    const sendOptions: ActiveWebSendOptions | undefined =
      options.gifPlayback ||
      forceDocumentDelivery ||
      accountId ||
      documentFileName ||
      options.quotedMessageKey
        ? {
            ...(options.gifPlayback ? { gifPlayback: true } : {}),
            ...(forceDocumentDelivery ? { asDocument: true } : {}),
            ...(documentFileName ? { fileName: documentFileName } : {}),
            ...(options.quotedMessageKey ? { quotedMessageKey: options.quotedMessageKey } : {}),
            accountId,
          }
        : undefined;
    const result = sendOptions
      ? await active.sendMessage(to, text, mediaBuffer, mediaType, sendOptions)
      : await active.sendMessage(to, text, mediaBuffer, mediaType);
    const messageId = (result as { messageId?: string })?.messageId ?? "unknown";
    const sentRemoteJid = resolveActualSentRemoteJid(result, jid);
    if (visibleTextAfterVoice) {
      // Voice captions require a second platform send. Persist the accepted voice
      // first so a caption failure cannot make recovery replay the voice note.
      await options.onDeliveryResult?.({ messageId, toJid: sentRemoteJid });
      const captionResult = sendOptions
        ? await active.sendMessage(to, visibleTextAfterVoice, undefined, undefined, sendOptions)
        : await active.sendMessage(to, visibleTextAfterVoice, undefined, undefined);
      await options.onDeliveryResult?.({
        messageId: (captionResult as { messageId?: string })?.messageId ?? "unknown",
        toJid: resolveActualSentRemoteJid(captionResult, jid),
      });
    }
    if (messageId && messageId !== "unknown" && text) {
      registerWhatsAppApprovalReactionTargetForOutboundMessage({
        accountId: resolvedAccountId,
        remoteJid: sentRemoteJid,
        messageId,
        text,
      });
    }
    const durationMs = Date.now() - startedAt;
    outboundLog.info(
      `Sent message ${messageId} -> ${redactedJid}${hasMedia ? " (media)" : ""} (${durationMs}ms)`,
    );
    logger.info({ jid: redactedJid, messageId }, "sent message");
    return { messageId, toJid: sentRemoteJid };
  } catch (err) {
    logger.error({ err: String(err), to: redactedTo, hasMedia }, "failed to send via web session");
    throw err;
  }
}

export async function sendTypingWhatsApp(
  to: string,
  options: {
    cfg: OpenClawConfig;
    accountId?: string;
  },
): Promise<void> {
  const cfg = requireRuntimeConfig(options.cfg, "WhatsApp typing send");
  const { listener: active } = requireOutboundActiveWebListener({
    cfg,
    accountId: options.accountId,
  });
  if (!isWhatsAppNewsletterJid(toWhatsappJid(to))) {
    await active.sendComposingTo(to);
  }
}

export async function sendReactionWhatsApp(
  chatJid: string,
  messageId: string,
  emoji: string,
  options: {
    verbose: boolean;
    fromMe?: boolean;
    participant?: string;
    accountId?: string;
    cfg: OpenClawConfig;
  },
): Promise<void> {
  const correlationId = generateSecureUuid();
  const cfg = requireRuntimeConfig(options.cfg, "WhatsApp reaction");
  const { listener: active } = requireOutboundActiveWebListener({
    cfg,
    accountId: options.accountId,
  });
  const redactedChatJid = redactIdentifier(chatJid);
  const logger = getChildLogger({
    module: "web-outbound",
    correlationId,
    chatJid: redactedChatJid,
    messageId,
  });
  try {
    const jid = toWhatsappJid(chatJid);
    const redactedJid = redactIdentifier(jid);
    outboundLog.info(`Sending reaction "${emoji}" -> message ${messageId}`);
    logger.info({ chatJid: redactedJid, messageId, emoji }, "sending reaction");
    await active.sendReaction(
      chatJid,
      messageId,
      emoji,
      options.fromMe ?? false,
      options.participant,
    );
    outboundLog.info(`Sent reaction "${emoji}" -> message ${messageId}`);
    logger.info({ chatJid: redactedJid, messageId, emoji }, "sent reaction");
  } catch (err) {
    logger.error(
      { err: String(err), chatJid: redactedChatJid, messageId, emoji },
      "failed to send reaction via web session",
    );
    throw err;
  }
}

export async function sendPollWhatsApp(
  to: string,
  poll: PollInput,
  options: { verbose: boolean; accountId?: string; cfg: OpenClawConfig },
): Promise<{ messageId: string; toJid: string }> {
  const correlationId = generateSecureUuid();
  const startedAt = Date.now();
  const cfg = requireRuntimeConfig(options.cfg, "WhatsApp poll");
  const { listener: active } = requireOutboundActiveWebListener({
    cfg,
    accountId: options.accountId,
  });
  const redactedTo = redactIdentifier(to);
  const logger = getChildLogger({
    module: "web-outbound",
    correlationId,
    to: redactedTo,
  });
  try {
    const jid = toWhatsappJid(to);
    const redactedJid = redactIdentifier(jid);
    const normalized = normalizePollInput(poll, { maxOptions: 12 });
    outboundLog.info(`Sending poll -> ${redactedJid}`);
    logger.info(
      {
        jid: redactedJid,
        optionCount: normalized.options.length,
        maxSelections: normalized.maxSelections,
      },
      "sending poll",
    );
    const result = await active.sendPoll(to, normalized);
    const messageId = (result as { messageId?: string })?.messageId ?? "unknown";
    const durationMs = Date.now() - startedAt;
    outboundLog.info(`Sent poll ${messageId} -> ${redactedJid} (${durationMs}ms)`);
    logger.info({ jid: redactedJid, messageId }, "sent poll");
    return { messageId, toJid: jid };
  } catch (err) {
    logger.error({ err: String(err), to: redactedTo }, "failed to send poll via web session");
    throw err;
  }
}
