/**
 * Feishu lark client + long-connection event dispatcher.
 *
 * Handles: ws event receiving (im.message.receive_v1 + card.action.trigger),
 * message sending, streaming patch via interactive cards, emoji
 * reactions, file download/upload, and the card action callback.
 */
import * as lark from "@larksuiteoapi/node-sdk";
import { readFile } from "node:fs/promises";
import type { Readable } from "node:stream";
import {
  WorkspaceFileBoundaryError,
  writeNewWorkspaceFileNoFollow,
} from "../security/workspaceFiles.js";
import type { FastifyBaseLogger } from "fastify";
import { DEFAULT_MAX_MESSAGE_LENGTH, splitAtBoundary } from "./textStream.js";
import type { SenderNameCache } from "./senderCache.js";

export interface FeishuConfig {
  readonly appId: string;
  readonly appSecret: string;
  readonly botOpenId: string;
}

export interface FeishuRuntime {
  readonly client: lark.Client;
  readonly logger: FastifyBaseLogger;
  readonly isListenerReady?: () => boolean;
}

export interface OutboundPayload {
  readonly msgType: string;
  readonly content: string;
}

export interface SendMessageOptions {
  readonly replyToMessageId?: string | undefined;
}

export async function withRetry<T>(
  fn: () => Promise<T>,
  options: {
    maxAttempts?: number;
    baseDelayMs?: number;
    shouldRetry?: (error: unknown) => boolean;
  } = {},
): Promise<T> {
  const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
  const baseDelayMs = options.baseDelayMs ?? 1000;
  const shouldRetry = options.shouldRetry ?? (() => true);

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts || !shouldRetry(error)) {
        throw error;
      }
      const delay = baseDelayMs * 2 ** (attempt - 1);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }

  throw new Error("withRetry reached an unreachable state");
}

/** Raw `im.message.receive_v1` event payload. */
export interface MessageReceiveEvent {
  /** Feishu's SDK dispatcher flattens v2 headers onto the top-level payload. */
  readonly event_id?: string;
  readonly event_type?: string;
  readonly header?: { readonly event_id?: string; readonly event_type?: string };
  readonly message: {
    readonly message_id: string;
    readonly root_id?: string;
    readonly parent_id?: string;
    readonly thread_id?: string;
    readonly create_time?: string;
    readonly update_time?: string;
    readonly chat_id: string;
    readonly chat_type: string;
    readonly message_type: string;
    readonly content: string;
    readonly mentions?: ReadonlyArray<{
      readonly key: string;
      readonly id: { readonly open_id?: string };
      readonly name: string;
    }>;
  };
  readonly sender: {
    readonly sender_id: { readonly open_id?: string };
    readonly sender_type: string;
  };
}

/** Card action trigger event (button clicks, select changes). */
export interface CardActionEvent {
  readonly operator: { readonly open_id?: string };
  readonly action: {
    readonly value?: unknown;
    readonly tag?: string;
    readonly option?: string;
    readonly form_value?: Readonly<Record<string, unknown>>;
  };
  readonly context?: { readonly open_message_id?: string; readonly open_chat_id?: string };
  readonly token?: string;
}

/** A binding-ending Feishu event delivered to this application's bot. */
export interface FeishuBindingLifecycleEvent {
  readonly eventId: string;
  readonly chatId: string;
  readonly reason: "chat_dissolved" | "bot_removed";
}

interface ContactBasicUser {
  readonly name?: string | undefined;
  readonly i18n_name?: {
    readonly zh_cn?: string | undefined;
    readonly en_us?: string | undefined;
    readonly ja_jp?: string | undefined;
  } | undefined;
}

type ContactUserBasicBatchResponse = {
  readonly data?: {
    readonly users?: readonly ContactBasicUser[] | undefined;
  } | undefined;
} | null;

// --- Message helpers ---

export function buildOutboundPayload(content: string): OutboundPayload {
  if (containsMarkdownTable(content)) {
    return { msgType: "text", content: JSON.stringify({ text: content }) };
  }
  if (containsMarkdownSyntax(content)) {
    return {
      msgType: "post",
      content: JSON.stringify({ zh_cn: { content: _buildMarkdownPostRows(content) } }),
    };
  }
  return { msgType: "text", content: JSON.stringify({ text: content }) };
}

export function _buildMarkdownPostRows(content: string): unknown[][] {
  if (!content.includes("```")) {
    return [[{ tag: "md", text: content }]];
  }

  const rows: unknown[][] = [];
  const codeBlockPattern = /```[\s\S]*?```/g;
  let cursor = 0;
  let match: RegExpExecArray | null;
  while ((match = codeBlockPattern.exec(content)) !== null) {
    if (match.index > cursor) {
      rows.push([{ tag: "md", text: content.slice(cursor, match.index) }]);
    }
    rows.push([{ tag: "md", text: match[0] }]);
    cursor = match.index + match[0].length;
  }

  if (rows.length === 0) {
    return [[{ tag: "md", text: content }]];
  }
  if (cursor < content.length) {
    rows.push([{ tag: "md", text: content.slice(cursor) }]);
  }
  return rows;
}

export function _stripMarkdownToPlainText(text: string): string {
  return text
    .replace(/```[^\n`]*\n?([\s\S]*?)```/g, "$1")
    .replace(/`([^`\n]+)`/g, "$1")
    .replace(/!\[([^\]\n]*)\]\([^)]+\)/g, "$1")
    .replace(/\[([^\]\n]+)\]\([^)]+\)/g, "$1")
    .replace(/^ {0,3}#{1,6}\s+/gm, "")
    .replace(/^ {0,3}>\s?/gm, "")
    .replace(/^(\s*)[-*+]\s+/gm, "$1")
    .replace(/^(\s*)\d+[.)]\s+/gm, "$1")
    .replace(/\*\*([^*\n]+)\*\*/g, "$1")
    .replace(/__([^_\n]+)__/g, "$1")
    .replace(/~~([^~\n]+)~~/g, "$1")
    .replace(/(^|[^*])\*([^*\n]+)\*/g, "$1$2")
    .replace(/(^|[^_])_([^_\n]+)_/g, "$1$2");
}

export function parsePostMessage(content: string): { text: string; imageKeys: string[]; fileKeys: string[] } {
  const parsed = parseJsonObject(content);
  const root = parsed === null ? null : postRoot(parsed);
  const locale = root === null ? null : selectPostLocale(root);
  if (locale === null) {
    return { text: "", imageKeys: [], fileKeys: [] };
  }

  const imageKeys: string[] = [];
  const fileKeys: string[] = [];
  const rows = Array.isArray(locale["content"]) ? locale["content"] : [];
  const body = rows
    .map((row) => {
      const elements = Array.isArray(row) ? row : [row];
      return elements.map((element) => renderPostElement(element, imageKeys, fileKeys)).join("");
    })
    .join("\n");

  const title = stringField(locale, "title");
  const text = title === undefined || title === "" ? body : body === "" ? `# ${title}` : `# ${title}\n\n${body}`;
  return { text, imageKeys, fileKeys };
}

/** Send a text message using the best Feishu message type for the content. */
export async function sendTextMessage(
  rt: FeishuRuntime,
  chatId: string,
  text: string,
  options?: SendMessageOptions,
): Promise<string | null> {
  const payload = buildOutboundPayload(text);
  try {
    return await sendMessagePayload(rt, chatId, payload, options);
  } catch (e) {
    if (payload.msgType === "post" && isPostContentFormatError(e)) {
      try {
        return await sendMessagePayload(
          rt,
          chatId,
          { msgType: "text", content: JSON.stringify({ text: _stripMarkdownToPlainText(text) }) },
          options,
        );
      } catch {
        return null;
      }
    }
    return null;
  }
}

/** Send long text as multiple Feishu messages. Returns the last successful message_id. */
export async function sendLongText(
  rt: FeishuRuntime,
  chatId: string,
  text: string,
  options?: SendMessageOptions,
): Promise<string | null> {
  let lastMessageId: string | null = null;
  for (const chunk of splitAtBoundary(text, DEFAULT_MAX_MESSAGE_LENGTH)) {
    const messageId = await sendTextMessage(rt, chatId, chunk, options);
    if (messageId !== null) {
      lastMessageId = messageId;
    }
  }
  return lastMessageId;
}

/** Send a plain text message (fire-and-forget). */
export async function sendText(
  rt: FeishuRuntime,
  chatId: string,
  text: string,
  options?: SendMessageOptions,
): Promise<void> {
  await sendTextMessage(rt, chatId, text, options);
}

/** Send an interactive card message (always patchable). Used for streaming. */
export async function sendInteractiveCardMessage(
  rt: FeishuRuntime,
  chatId: string,
  text: string,
  options?: SendMessageOptions,
): Promise<string | null> {
  const payload = buildOutboundPayload(text);
  const card = buildInteractiveCardFromOutboundPayload(payload);
  try {
    return await sendMessagePayload(
      rt,
      chatId,
      { msgType: "interactive", content: JSON.stringify(card) },
      options,
    );
  } catch {
    return null;
  }
}

/** Patch a text-as-card message in-place (streaming updates). */
export async function patchTextMessage(rt: FeishuRuntime, messageId: string, text: string): Promise<void> {
  const payload = buildOutboundPayload(text);
  const card = buildInteractiveCardFromOutboundPayload(payload);
  const client = rt.client as unknown as {
    im: { v1: { message: { patch: (p: unknown) => Promise<unknown> } } };
  };
  try {
    await client.im.v1.message.patch({
      path: { message_id: messageId },
      params: { msg_type: "interactive" },
      data: { content: JSON.stringify(card) },
    });
  } catch (e) {
    rt.logger.warn({ messageId, err: e instanceof Error ? e.message : String(e) }, "patchTextMessage failed");
  }
}

/** Send a raw interactive card (arbitrary JSON). Returns message_id on success. */
export async function sendCard(
  rt: FeishuRuntime,
  chatId: string,
  card: Record<string, unknown>,
  options?: SendMessageOptions,
): Promise<string | null> {
  try {
    return await sendMessagePayload(
      rt,
      chatId,
      { msgType: "interactive", content: JSON.stringify(card) },
      options,
    );
  } catch (e) {
    rt.logger.warn({ chatId, err: errorText(e) }, "sendCard failed");
    return null;
  }
}

/** Patch an interactive card in-place with raw JSON. */
export async function patchCard(rt: FeishuRuntime, messageId: string, card: Record<string, unknown>): Promise<boolean> {
  const client = rt.client as unknown as {
    im: { v1: { message: { patch: (p: unknown) => Promise<unknown> } } };
  };
  try {
    await client.im.v1.message.patch({
      path: { message_id: messageId },
      params: { msg_type: "interactive" },
      data: { content: JSON.stringify(card) },
    });
    return true;
  } catch (e) {
    rt.logger.warn({ messageId, err: errorText(e) }, "patchCard failed");
    return false;
  }
}

/** Send an approval card with buttons. Returns message_id on success. */
export async function sendApprovalCard(
  rt: FeishuRuntime,
  chatId: string,
  title: string,
  body: string,
  buttons: ReadonlyArray<{ label: string; value: string; style?: "primary" | "default" | "danger" }>,
  options?: SendMessageOptions,
): Promise<string | null> {
  const card = {
    config: { wide_screen_mode: true },
    header: { title: { content: title, tag: "plain_text" }, template: "orange" },
    elements: [
      { tag: "markdown", content: body },
      {
        tag: "action",
        actions: buttons.map((button) => ({
          tag: "button",
          text: { tag: "plain_text", content: button.label },
          type: button.style ?? "default",
          value: { approval_action: button.value },
        })),
      },
    ],
  };
  try {
    return await sendMessagePayload(
      rt,
      chatId,
      { msgType: "interactive", content: JSON.stringify(card) },
      options,
    );
  } catch (e) {
    rt.logger.warn({ chatId, err: e instanceof Error ? e.message : String(e) }, "sendApprovalCard failed");
    return null;
  }
}

/** Update a card to show it has been resolved. */
export async function resolveCard(
  rt: FeishuRuntime,
  messageId: string,
  icon: string,
  label: string,
  template: "green" | "red",
  resolvedBy: string,
): Promise<void> {
  const card = {
    config: { wide_screen_mode: true },
    header: { title: { content: `${icon} ${label}`, tag: "plain_text" }, template },
    elements: [{ tag: "markdown", content: `${icon} **${label}** by ${resolvedBy}` }],
  };
  const client = rt.client as unknown as {
    im: { v1: { message: { patch: (p: unknown) => Promise<unknown> } } };
  };
  try {
    await client.im.v1.message.patch({
      path: { message_id: messageId },
      params: { msg_type: "interactive" },
      data: { content: JSON.stringify(card) },
    });
  } catch (e) {
    rt.logger.warn({ messageId, err: e instanceof Error ? e.message : String(e) }, "resolveCard failed");
  }
}

/** React to a message with an emoji. */
export async function reactToMessage(rt: FeishuRuntime, messageId: string, emoji: string): Promise<void> {
  try {
    await rt.client.request({
      method: "POST",
      url: `/open-apis/im/v1/messages/${messageId}/reactions`,
      data: { reaction_type: { emoji_type: emoji } },
    });
  } catch (e) {
    rt.logger.warn({ messageId, err: e instanceof Error ? e.message : String(e) }, "reactToMessage failed");
  }
}

/** Add a reaction to a message, returning the reaction_id (needed for later removal). */
export async function addReaction(rt: FeishuRuntime, messageId: string, emojiType: string): Promise<string | null> {
  try {
    const res = await rt.client.request({
      method: "POST",
      url: `/open-apis/im/v1/messages/${messageId}/reactions`,
      data: { reaction_type: { emoji_type: emojiType } },
    }) as ReactionCreateResponse;
    const reactionId = reactionIdFromResponse(res);
    if (reactionId === null) {
      rt.logger.warn({ messageId, emojiType }, "addReaction failed: response missing reaction_id");
    }
    return reactionId;
  } catch (e) {
    rt.logger.warn({ messageId, emojiType, err: e instanceof Error ? e.message : String(e) }, "addReaction failed");
    return null;
  }
}

/** Remove a previously added reaction by its reaction_id. */
export async function removeReaction(rt: FeishuRuntime, messageId: string, reactionId: string): Promise<boolean> {
  try {
    await rt.client.request({
      method: "DELETE",
      url: `/open-apis/im/v1/messages/${messageId}/reactions/${reactionId}`,
    });
    return true;
  } catch (e) {
    rt.logger.warn({ messageId, reactionId, err: e instanceof Error ? e.message : String(e) }, "removeReaction failed");
    return false;
  }
}

/** Resolve a Feishu sender display name from open_id, optionally using a cache. */
export async function resolveSenderName(
  rt: FeishuRuntime,
  openId: string,
  cache?: SenderNameCache,
): Promise<string | null> {
  if (openId === "") return null;

  const fetchName = async (id: string): Promise<string | null> => {
    try {
      const res = await rt.client.contact.v3.user.basicBatch({
        params: { user_id_type: "open_id" },
        data: { user_ids: [id] },
      }) as ContactUserBasicBatchResponse;
      const user = res?.data?.users?.[0];
      return user === undefined ? null : contactBasicUserName(user);
    } catch (e) {
      rt.logger.warn({ openId: id, err: e instanceof Error ? e.message : String(e) }, "resolveSenderName failed");
      return null;
    }
  };

  if (cache !== undefined) {
    return cache.getOrFetch(openId, fetchName);
  }
  return fetchName(openId);
}

function contactBasicUserName(user: ContactBasicUser): string | null {
  const candidates = [
    user.name,
    user.i18n_name?.zh_cn,
    user.i18n_name?.en_us,
    user.i18n_name?.ja_jp,
  ];
  for (const candidate of candidates) {
    if (typeof candidate === "string" && candidate.trim() !== "") return candidate.trim();
  }
  return null;
}

// --- File helpers ---

/** Download a file/image from a Feishu message to a local path. */
export async function downloadMessageFile(
  rt: FeishuRuntime,
  messageId: string,
  fileKey: string,
  workspaceRoot: string,
  workspaceDir: string,
  workspaceRelativePath: string,
  resourceType: "image" | "file",
  maxBytes?: number,
): Promise<string> {
  try {
    return await withRetry(async () => {
      const resource = await rt.client.im.v1.messageResource.get({
        params: { type: resourceType },
        path: { message_id: messageId, file_key: fileKey },
      });
      const readableResource = resource as unknown as { getReadableStream: () => Readable };
      const source = readableResource.getReadableStream();
      try {
        return await writeNewWorkspaceFileNoFollow(
          workspaceRoot,
          workspaceDir,
          workspaceRelativePath,
          source,
          maxBytes,
        );
      } catch (error) {
        source.destroy();
        throw error;
      }
    }, {
      shouldRetry: (error) => !(error instanceof WorkspaceFileBoundaryError),
    });
  } catch (e) {
    rt.logger.error(
      { err: e, messageId, fileKey, resourceType, workspaceRoot, workspaceDir, workspaceRelativePath },
      "downloadMessageFile failed",
    );
    throw e;
  }
}

/** Upload and send a file to a chat. */
export async function sendFile(
  rt: FeishuRuntime,
  chatId: string,
  filePath: string,
  fileName: string,
  options?: SendMessageOptions,
): Promise<string | null> {
  try {
    return await sendFileData(rt, chatId, await readFile(filePath), fileName, options);
  } catch (error) {
    // Legacy callers model delivery as nullable. Keep that API while retaining
    // the complete failure object logged by sendFileData (or log read failure).
    if (!(error instanceof FeishuFileDeliveryError)) {
      rt.logger.error({ chatId, filePath, fileName, err: error }, "sendFile failed before upload");
    }
    return null;
  }
}

export class FeishuFileDeliveryError extends Error {
  constructor(message: string, options?: ErrorOptions) {
    super(message, options);
    this.name = "FeishuFileDeliveryError";
  }
}

/** Upload and send an already-opened, no-follow file snapshot to a chat. */
export async function sendFileData(
  rt: FeishuRuntime,
  chatId: string,
  fileData: Buffer,
  fileName: string,
  options?: SendMessageOptions,
): Promise<string> {
  const client = rt.client as unknown as {
    im: { v1: {
      file: { create: (p: unknown) => Promise<FileCreateResponse> }
    } };
  };
  try {
    const ext = fileName.split(".").pop() ?? "";
    const fileType = ext === "pdf" ? "pdf" : ext === "doc" ? "doc" : ext === "xls" ? "xls" : ext === "ppt" ? "ppt" : "stream";
    const uploadRes = await withRetry(async () =>
      client.im.v1.file.create({ data: { file_type: fileType, file_name: fileName, file: fileData } }),
    );
    const fileKey = fileKeyFromResponse(uploadRes);
    if (fileKey === undefined) {
      throw new FeishuFileDeliveryError("Feishu file upload response is missing file_key");
    }
    const messageId = await withRetry(async () =>
      sendMessagePayload(rt, chatId, { msgType: "file", content: JSON.stringify({ file_key: fileKey }) }, options),
    );
    if (messageId === null) {
      throw new FeishuFileDeliveryError("Feishu file message response is missing message_id");
    }
    return messageId;
  } catch (error) {
    const failure = error instanceof FeishuFileDeliveryError
      ? error
      : new FeishuFileDeliveryError("Feishu file delivery failed", { cause: error });
    rt.logger.error({ chatId, fileName, err: failure }, "sendFileData failed");
    throw failure;
  }
}

async function sendMessagePayload(
  rt: FeishuRuntime,
  chatId: string,
  payload: OutboundPayload,
  options?: SendMessageOptions,
): Promise<string | null> {
  const client = rt.client as unknown as {
    im: { v1: { message: {
      create: (p: unknown) => Promise<MessageCreateResponse>;
      reply: (p: unknown) => Promise<MessageCreateResponse>;
    } } };
  };
  const replyToMessageId = normalizeReplyMessageId(options?.replyToMessageId);
  if (replyToMessageId !== null) {
    const res = await client.im.v1.message.reply({
      path: { message_id: replyToMessageId },
      data: {
        msg_type: payload.msgType,
        content: payload.content,
      },
    });
    return messageIdFromResponse(res);
  }

  const res = await client.im.v1.message.create({
    params: { receive_id_type: "chat_id" },
    data: { receive_id: chatId, msg_type: payload.msgType, content: payload.content },
  });
  return messageIdFromResponse(res);
}

function normalizeReplyMessageId(messageId: string | undefined): string | null {
  return messageId === undefined || messageId === "" ? null : messageId;
}

type MessageCreateResponse = {
  readonly message_id?: string | undefined;
  readonly data?: { readonly message_id?: string | undefined } | undefined;
} | null;

type FileCreateResponse = {
  readonly file_key?: string | undefined;
  readonly data?: { readonly file_key?: string | undefined } | undefined;
} | null;

type ReactionCreateResponse = {
  readonly reaction_id?: string | undefined;
  readonly data?: { readonly reaction_id?: string | undefined } | undefined;
} | null;

function messageIdFromResponse(res: MessageCreateResponse): string | null {
  return res?.data?.message_id ?? res?.message_id ?? null;
}

function fileKeyFromResponse(res: FileCreateResponse): string | undefined {
  return res?.file_key ?? res?.data?.file_key;
}

function reactionIdFromResponse(res: ReactionCreateResponse): string | null {
  return res?.data?.reaction_id ?? res?.reaction_id ?? null;
}

function parseJsonObject(content: string): Record<string, unknown> | null {
  try {
    const parsed = JSON.parse(content) as unknown;
    return isRecord(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

function postRoot(parsed: Record<string, unknown>): Record<string, unknown> {
  const post = parsed["post"];
  return isRecord(post) ? post : parsed;
}

function selectPostLocale(root: Record<string, unknown>): Record<string, unknown> | null {
  if (Array.isArray(root["content"])) return root;

  for (const locale of ["zh_cn", "en_us"]) {
    const candidate = root[locale];
    if (isPostLocale(candidate)) return candidate;
  }

  for (const candidate of Object.values(root)) {
    if (isPostLocale(candidate)) return candidate;
  }
  return null;
}

function isPostLocale(value: unknown): value is Record<string, unknown> {
  return isRecord(value) && (Array.isArray(value["content"]) || typeof value["title"] === "string");
}

function renderPostElement(element: unknown, imageKeys: string[], fileKeys: string[]): string {
  if (!isRecord(element)) return "";

  switch (stringField(element, "tag")) {
    case "text":
      return applyTextStyles(stringField(element, "text") ?? "", element);
    case "a":
      return renderLink(element);
    case "at":
      return renderMention(element);
    case "img":
    case "image":
      pushStringField(imageKeys, element, "image_key");
      return "[Image]";
    case "media":
    case "file":
    case "audio":
    case "video":
      pushStringField(fileKeys, element, "file_key");
      return renderAttachmentPlaceholder(element);
    case "code":
    case "code_block":
      return renderCodeBlock(element);
    case "br":
      return "\n";
    case "hr":
    case "divider":
      return "\n\n---\n\n";
    case "emotion":
    case "emoji":
      return renderEmoji(element);
    case "md":
      return stringField(element, "text") ?? "";
    default:
      return stringField(element, "text") ?? "";
  }
}

function renderLink(element: Record<string, unknown>): string {
  const text = stringField(element, "text") ?? "";
  const href = stringField(element, "href") ?? "";
  if (href === "") return text;
  return `[${text}](${href})`;
}

function renderMention(element: Record<string, unknown>): string {
  if (stringField(element, "user_id") === "@_all") return "@all";
  const name =
    stringField(element, "user_name") ??
    stringField(element, "name") ??
    stringField(element, "text") ??
    stringField(element, "user_id") ??
    "";
  return name === "" ? "" : `@${name.replace(/^@/, "")}`;
}

function renderAttachmentPlaceholder(element: Record<string, unknown>): string {
  const fileName = stringField(element, "file_name") ?? stringField(element, "name");
  return fileName === undefined || fileName === "" ? "[Attachment]" : `[Attachment: ${fileName}]`;
}

function renderCodeBlock(element: Record<string, unknown>): string {
  const language = stringField(element, "language") ?? "";
  const text = stringField(element, "text") ?? "";
  return `\`\`\`${language}\n${text}\n\`\`\``;
}

function renderEmoji(element: Record<string, unknown>): string {
  const emojiType =
    stringField(element, "emoji_type") ??
    stringField(element, "emotion_key") ??
    stringField(element, "name") ??
    stringField(element, "text");
  return emojiType === undefined || emojiType === "" ? "" : `:${emojiType}:`;
}

function applyTextStyles(text: string, element: Record<string, unknown>): string {
  let rendered = text;
  if (hasPostStyle(element, "bold")) rendered = `**${rendered}**`;
  if (hasPostStyle(element, "italic")) rendered = `*${rendered}*`;
  if (hasPostStyle(element, "strikethrough", "strike", "line_through", "lineThrough")) rendered = `~~${rendered}~~`;
  if (hasPostStyle(element, "code", "inline_code", "inlineCode")) rendered = `\`${rendered}\``;
  return rendered;
}

function hasPostStyle(element: Record<string, unknown>, ...names: string[]): boolean {
  const style = element["style"] ?? element["styles"];
  if (styleMatches(style, names)) return true;
  return names.some((name) => element[name] === true);
}

function styleMatches(style: unknown, names: readonly string[]): boolean {
  if (Array.isArray(style)) {
    return style.some((entry) => typeof entry === "string" && names.includes(entry));
  }
  if (typeof style === "string") {
    const tokens = style.split(/[\s,]+/).filter((token) => token !== "");
    return tokens.some((token) => names.includes(token));
  }
  if (isRecord(style)) {
    return names.some((name) => style[name] === true || style[name] === "true");
  }
  return false;
}

function pushStringField(target: string[], record: Record<string, unknown>, field: string): void {
  const value = stringField(record, field);
  if (value !== undefined && value !== "") target.push(value);
}

function stringField(record: Record<string, unknown>, field: string): string | undefined {
  const value = record[field];
  return typeof value === "string" ? value : undefined;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function containsMarkdownSyntax(content: string): boolean {
  const patterns = [
    /^ {0,3}#{1,6}\s+\S/m,
    /^ {0,3}>\s+\S/m,
    /^ {0,3}(?:[-*+]\s+\S|\d+[.)]\s+\S)/m,
    /```/,
    /`[^`\n]+`/,
    /\*\*[^*\n]+\*\*/,
    /__[^_\n]+__/,
    /~~[^~\n]+~~/,
    /(^|[^*])\*[^*\s][^*\n]*\*(?!\*)/,
    /(^|[^_])_[^_\s][^_\n]*_(?!_)/,
    /\[[^\]\n]+\]\([^)]+\)/,
  ];
  return patterns.some((pattern) => pattern.test(content));
}

function containsMarkdownTable(content: string): boolean {
  const lines = content.split(/\r?\n/);
  for (let i = 0; i < lines.length - 1; i += 1) {
    const current = lines[i];
    const next = lines[i + 1];
    if (current !== undefined && next !== undefined && isTableHeaderLine(current) && isTableSeparatorLine(next)) {
      return true;
    }
  }
  return false;
}

function isTableHeaderLine(line: string): boolean {
  const trimmed = line.trim();
  return trimmed.startsWith("|") && trimmed.includes("|", 1);
}

function isTableSeparatorLine(line: string): boolean {
  const trimmed = line.trim();
  if (!trimmed.startsWith("|")) return false;
  const cells = trimmed.replace(/^\|/, "").replace(/\|$/, "").split("|");
  return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
}

function buildInteractiveCardFromOutboundPayload(payload: OutboundPayload): { elements: Array<{ tag: "div"; text: { tag: "lark_md"; content: string } }> } {
  return {
    elements: rowsFromOutboundPayload(payload).map((row) => ({
      tag: "div",
      text: { tag: "lark_md", content: row.map(markdownTextFromPostElement).join("") },
    })),
  };
}

function rowsFromOutboundPayload(payload: OutboundPayload): unknown[][] {
  if (payload.msgType !== "post") {
    return [[{ tag: "md", text: textFromTextPayload(payload.content) }]];
  }
  try {
    const parsed = JSON.parse(payload.content) as { zh_cn?: { content?: unknown } };
    return Array.isArray(parsed.zh_cn?.content) ? parsed.zh_cn.content as unknown[][] : [[{ tag: "md", text: "" }]];
  } catch {
    return [[{ tag: "md", text: "" }]];
  }
}

function textFromTextPayload(content: string): string {
  try {
    const parsed = JSON.parse(content) as { text?: unknown };
    return typeof parsed.text === "string" ? parsed.text : content;
  } catch {
    return content;
  }
}

function markdownTextFromPostElement(element: unknown): string {
  if (typeof element !== "object" || element === null) return "";
  const text = (element as { text?: unknown }).text;
  return typeof text === "string" ? text : "";
}

function isPostContentFormatError(error: unknown): boolean {
  return errorText(error).includes("content format of the post type is incorrect");
}

function errorText(error: unknown): string {
  if (error instanceof Error) {
    return `${error.message} ${errorText((error as { response?: unknown }).response)}`;
  }
  if (typeof error === "string") return error;
  if (typeof error === "object" && error !== null) {
    try {
      return JSON.stringify(error);
    } catch {
      return String(error);
    }
  }
  return String(error);
}

// --- Lark client + ws listener ---

export function createLarkClient(config: FeishuConfig): lark.Client {
  return new lark.Client({
    appId: config.appId,
    appSecret: config.appSecret,
    appType: lark.AppType.SelfBuild,
  });
}

export async function startFeishuListenerWithClient(
  config: FeishuConfig,
  client: lark.Client,
  logger: FastifyBaseLogger,
  onMessage: (event: MessageReceiveEvent, rt: FeishuRuntime) => Promise<void>,
  onCardAction?: (event: CardActionEvent, rt: FeishuRuntime) => Promise<void>,
  onTerminalError?: (error: Error) => void,
  onBindingLifecycle?: (event: FeishuBindingLifecycleEvent) => Promise<void>,
): Promise<FeishuRuntime> {
  let state: "STARTING" | "READY" | "FAILED" = "STARTING";
  let resolveReady!: () => void;
  let rejectReady!: (error: Error) => void;
  const ready = new Promise<void>((resolve, reject) => {
    resolveReady = resolve;
    rejectReady = reject;
  });
  const wsClient = new lark.WSClient({
    appId: config.appId,
    appSecret: config.appSecret,
    domain: lark.Domain.Feishu,
    loggerLevel: lark.LoggerLevel.info,
    handshakeTimeoutMs: 10_000,
    onReady: () => {
      state = "READY";
      resolveReady();
      logger.info("Feishu listener ready");
    },
    onError: (error) => {
      const wasStarting = state === "STARTING";
      state = "FAILED";
      logger.error({ err: error }, "Feishu listener terminal failure");
      if (wasStarting) rejectReady(error);
      else onTerminalError?.(error);
    },
  });
  const rt: FeishuRuntime = { client, logger, isListenerReady: () => state === "READY" };
  const handlers: Record<string, (data: unknown) => Promise<void>> = {
    "im.message.receive_v1": async (data) => {
      try { await onMessage(data as MessageReceiveEvent, rt); }
      catch (e) { logger.error({ err: e }, "feishu message handler threw"); }
    },
  };
  if (onCardAction !== undefined) {
    handlers["card.action.trigger"] = async (data) => {
      void Promise.resolve()
        .then(() => onCardAction(data as CardActionEvent, rt))
        .catch((e) => { logger.error({ err: e }, "feishu card action handler threw"); });
    };
  }
  if (onBindingLifecycle !== undefined) {
    handlers["im.chat.disbanded_v1"] = async (data) => {
      await dispatchBindingLifecycleEvent(data, "chat_dissolved", onBindingLifecycle, logger);
    };
    handlers["im.chat.member.bot.deleted_v1"] = async (data) => {
      await dispatchBindingLifecycleEvent(data, "bot_removed", onBindingLifecycle, logger);
    };
  }
  await wsClient.start({ eventDispatcher: new lark.EventDispatcher({}).register(handlers) });
  let timeout: ReturnType<typeof setTimeout> | undefined;
  const startupTimeout = new Promise<never>((_resolve, reject) => {
    timeout = setTimeout(() => reject(new Error("Feishu listener did not become ready within 15 seconds")), 15_000);
    timeout.unref();
  });
  try {
    await Promise.race([ready, startupTimeout]);
  } finally {
    if (timeout !== undefined) clearTimeout(timeout);
  }
  return rt;
}

async function dispatchBindingLifecycleEvent(
  data: unknown,
  reason: FeishuBindingLifecycleEvent["reason"],
  onBindingLifecycle: (event: FeishuBindingLifecycleEvent) => Promise<void>,
  logger: FastifyBaseLogger,
): Promise<void> {
  const event = bindingLifecycleEventFrom(data, reason);
  if (event === null) {
    logger.warn({ reason }, "feishu binding lifecycle event missing chat id");
    return;
  }
  await onBindingLifecycle(event);
}

function bindingLifecycleEventFrom(
  data: unknown,
  reason: FeishuBindingLifecycleEvent["reason"],
): FeishuBindingLifecycleEvent | null {
  if (typeof data !== "object" || data === null) return null;
  const event = data as { readonly chat_id?: unknown; readonly event_id?: unknown; readonly header?: { readonly event_id?: unknown } };
  if (typeof event.chat_id !== "string" || event.chat_id.trim() === "") return null;
  const eventId = typeof event.event_id === "string" && event.event_id !== ""
    ? event.event_id
    : typeof event.header?.event_id === "string" && event.header.event_id !== ""
      ? event.header.event_id
      : undefined;
  if (eventId === undefined) return null;
  return { chatId: event.chat_id, reason, eventId };
}

export async function startFeishuListener(
  config: FeishuConfig,
  logger: FastifyBaseLogger,
  onMessage: (event: MessageReceiveEvent, rt: FeishuRuntime) => Promise<void>,
  onCardAction?: (event: CardActionEvent, rt: FeishuRuntime) => Promise<void>,
): Promise<FeishuRuntime> {
  return startFeishuListenerWithClient(config, createLarkClient(config), logger, onMessage, onCardAction);
}
