export function createGenerationRecallTransactions(deps = {}) {
  const generationRecallTransactions = new Map();

  const normalizeChatIdCandidate = (value = "") =>
    deps.normalizeChatIdCandidate?.(value) ?? String(value ?? "").trim();
  const normalizeRecallInputText = (value = "") =>
    deps.normalizeRecallInputText?.(value) ?? String(value || "").trim();
  const getCurrentChatId = (...args) => deps.getCurrentChatId?.(...args);
  const getContext = (...args) => deps.getContext?.(...args);
  const getActiveGenerationId = () =>
    String(deps.getActiveGenerationId?.() || "").trim();
  const getGenerationRecallTransactionTtlMs = () =>
    Number.isFinite(Number(deps.GENERATION_RECALL_TRANSACTION_TTL_MS))
      ? Number(deps.GENERATION_RECALL_TRANSACTION_TTL_MS)
      : 15000;
  const getGenerationRecallHookBridgeMs = () =>
    Number.isFinite(Number(deps.GENERATION_RECALL_HOOK_BRIDGE_MS))
      ? Number(deps.GENERATION_RECALL_HOOK_BRIDGE_MS)
      : 1200;

  function buildPreGenerationRecallKey(type, options = {}) {
    const targetUserMessageIndex = Number.isFinite(options.targetUserMessageIndex)
      ? options.targetUserMessageIndex
      : "none";
    const seedText =
      options.overrideUserMessage ||
      options.userMessage ||
      `@target:${targetUserMessageIndex}`;

    const normalizedChatId = normalizeChatIdCandidate(
      options.chatId || getCurrentChatId(),
    );

    return [
      normalizedChatId,
      String(type || "normal").trim() || "normal",
      deps.hashRecallInput(seedText || ""),
    ].join(":");
  }

  function cleanupGenerationRecallTransactions(now = Date.now()) {
    for (const [
      transactionId,
      transaction,
    ] of generationRecallTransactions.entries()) {
      if (
        !transaction ||
        now - (transaction.updatedAt || 0) > getGenerationRecallTransactionTtlMs()
      ) {
        generationRecallTransactions.delete(transactionId);
      }
    }
  }

  function getGenerationRecallPeerHookName(hookName = "") {
    const normalized = String(hookName || "").trim();
    if (normalized === "GENERATION_AFTER_COMMANDS") {
      return "GENERATE_BEFORE_COMBINE_PROMPTS";
    }
    if (normalized === "GENERATE_BEFORE_COMBINE_PROMPTS") {
      return "GENERATION_AFTER_COMMANDS";
    }
    return "";
  }

  function isGenerationRecallTransactionWithinBridgeWindow(
    transaction,
    now = Date.now(),
  ) {
    if (!transaction) return false;
    return (
      now - Number(transaction.updatedAt || transaction.createdAt || 0) <=
      getGenerationRecallHookBridgeMs()
    );
  }

  function normalizeGenerationRecallTransactionType(generationType = "normal") {
    const normalized = String(generationType || "normal").trim() || "normal";
    return normalized === "normal" ? "normal" : "history";
  }

  function resolveGenerationRecallDeliveryMode(
    hookName,
    generationType = "normal",
    recallOptions = {},
  ) {
    if (recallOptions?.forceImmediateDelivery === true) {
      return "immediate";
    }

    const normalizedType = normalizeGenerationRecallTransactionType(
      recallOptions?.generationType || generationType,
    );
    if (normalizedType !== "normal") {
      return "immediate";
    }

    // GENERATION_AFTER_COMMANDS: immediate —— await 完召回后直接通过
    // setExtensionPrompt 注入记忆，与 shujuku 参考实现一致。
    // GENERATE_BEFORE_COMBINE_PROMPTS: deferred —— 作为兜底，通过 promptData
    // rewrite 补救注入。
    if (hookName === "GENERATE_BEFORE_COMBINE_PROMPTS") {
      return "deferred";
    }
    return "immediate";
  }

  function shouldUseAuthoritativeGenerationRecallInput(recallOptions = {}) {
    const normalizedGenerationType = normalizeGenerationRecallTransactionType(
      recallOptions?.generationType || "normal",
    );
    if (normalizedGenerationType !== "normal") {
      return false;
    }
    return Boolean(deps.getSettings?.()?.recallUseAuthoritativeGenerationInput);
  }

  function shouldPreserveAuthoritativeGenerationRecallText(
    source,
    overrideUserMessage,
    targetUserMessageText,
    recallOptions = {},
  ) {
    if (!shouldUseAuthoritativeGenerationRecallInput(recallOptions)) {
      return false;
    }
    const normalizedOverride = normalizeRecallInputText(overrideUserMessage);
    const normalizedTarget = normalizeRecallInputText(targetUserMessageText);
    if (!normalizedOverride || !normalizedTarget || normalizedOverride === normalizedTarget) {
      return false;
    }
    const normalizedSource = String(source || "").trim();
    return [
      "send-intent",
      "generation-started-send-intent",
      "generation-started-textarea",
      "host-generation-lifecycle",
      "textarea-live",
      "planner-handoff",
    ].includes(normalizedSource);
  }

  function freezeGenerationRecallOptionsForTransaction(
    chat,
    generationType = "normal",
    recallOptions = {},
  ) {
    if (!Array.isArray(chat)) return null;

    const optionGenerationType =
      String(
        recallOptions?.generationType || generationType || "normal",
      ).trim() || "normal";
    const normalizedGenerationType = optionGenerationType;

    const overrideUserMessage = normalizeRecallInputText(
      recallOptions?.overrideUserMessage || recallOptions?.userMessage || "",
    );

    const source =
      String(
        recallOptions?.overrideSource || recallOptions?.source || "",
      ).trim() ||
      (normalizeGenerationRecallTransactionType(normalizedGenerationType) ===
      "normal"
        ? "chat-tail-user"
        : "chat-last-user");
    const sourceLabel =
      String(
        recallOptions?.overrideSourceLabel ||
          recallOptions?.sourceLabel ||
          deps.getRecallUserMessageSourceLabel(source),
      ).trim() || deps.getRecallUserMessageSourceLabel(source);
    const sourceReason =
      String(
        recallOptions?.overrideReason || recallOptions?.reason || "",
      ).trim() || "transaction-source-frozen";
    const sourceCandidates = Array.isArray(recallOptions?.sourceCandidates)
      ? recallOptions.sourceCandidates
          .map((candidate) => ({
            text: normalizeRecallInputText(candidate?.text || ""),
            source: String(candidate?.source || "").trim(),
            sourceLabel: String(candidate?.sourceLabel || "").trim(),
            reason: String(candidate?.reason || "").trim(),
            includeSyntheticUserMessage: Boolean(
              candidate?.includeSyntheticUserMessage,
            ),
          }))
          .filter((candidate) => candidate.text && candidate.source)
      : [];

    let targetUserMessageIndex = Number.isFinite(
      recallOptions?.targetUserMessageIndex,
    )
      ? Math.floor(Number(recallOptions.targetUserMessageIndex))
      : deps.resolveGenerationTargetUserMessageIndex(chat, {
          generationType: normalizedGenerationType,
        });

    if (!Number.isFinite(targetUserMessageIndex)) {
      if (
        normalizeGenerationRecallTransactionType(normalizedGenerationType) ===
          "normal" &&
        overrideUserMessage
      ) {
        return {
          generationType: normalizedGenerationType,
          targetUserMessageIndex: null,
          overrideUserMessage,
          overrideSource: source,
          overrideSourceLabel: sourceLabel,
          overrideReason: sourceReason,
          sourceCandidates,
          lockedSource: source,
          lockedSourceLabel: sourceLabel,
          lockedReason: sourceReason,
          authoritativeInputUsed: false,
          boundUserFloorText: "",
          includeSyntheticUserMessage: Boolean(
            recallOptions?.includeSyntheticUserMessage,
          ),
        };
      }
      return null;
    }
    targetUserMessageIndex = Math.floor(targetUserMessageIndex);

    const targetUserMessage = chat[targetUserMessageIndex];
    if (!targetUserMessage?.is_user) {
      return null;
    }

    const targetUserMessageText = normalizeRecallInputText(targetUserMessage?.mes || "");
    const preserveAuthoritativeText = shouldPreserveAuthoritativeGenerationRecallText(
      source,
      overrideUserMessage,
      targetUserMessageText,
      recallOptions,
    );
    const frozenUserMessage = preserveAuthoritativeText
      ? normalizeRecallInputText(overrideUserMessage)
      : normalizeRecallInputText(
          targetUserMessage?.mes ||
            recallOptions?.overrideUserMessage ||
            recallOptions?.userMessage ||
            "",
        );
    if (!frozenUserMessage) {
      return null;
    }

    return {
      generationType: normalizedGenerationType,
      targetUserMessageIndex,
      overrideUserMessage: frozenUserMessage,
      overrideSource: source,
      overrideSourceLabel: sourceLabel,
      overrideReason:
        sourceReason ||
        (frozenUserMessage === overrideUserMessage
          ? "transaction-source-frozen"
          : "transaction-bound-to-chat-user-floor"),
      sourceCandidates,
      lockedSource: source,
      lockedSourceLabel: sourceLabel,
      lockedReason:
        sourceReason ||
        (frozenUserMessage === overrideUserMessage
          ? "transaction-source-frozen"
          : "transaction-bound-to-chat-user-floor"),
      authoritativeInputUsed: preserveAuthoritativeText,
      boundUserFloorText: targetUserMessageText,
      includeSyntheticUserMessage: preserveAuthoritativeText,
    };
  }

  function buildGenerationRecallTransactionId(chatId, generationType, recallKey) {
    return [
      String(chatId || ""),
      String(generationType || "normal").trim() || "normal",
      String(recallKey || ""),
    ].join(":");
  }

  function beginGenerationRecallTransaction({
    chatId,
    generationType = "normal",
    recallKey = "",
    forceNew = false,
  } = {}) {
    const normalizedChatId = String(chatId || "");
    const normalizedGenerationType =
      String(generationType || "normal").trim() || "normal";
    const normalizedRecallKey = String(recallKey || "");
    if (!normalizedChatId || !normalizedRecallKey) return null;

    cleanupGenerationRecallTransactions();
    const transactionId = buildGenerationRecallTransactionId(
      normalizedChatId,
      normalizedGenerationType,
      normalizedRecallKey,
    );

    const now = Date.now();
    const existingTransaction =
      generationRecallTransactions.get(transactionId) || null;
    if (
      existingTransaction &&
      isGenerationRecallTransactionWithinBridgeWindow(existingTransaction, now) &&
      !forceNew
    ) {
      existingTransaction.updatedAt = now;
      generationRecallTransactions.set(transactionId, existingTransaction);
      return existingTransaction;
    }

    const transaction = {
      id: transactionId,
      chatId: normalizedChatId,
      generationType: normalizedGenerationType,
      recallKey: normalizedRecallKey,
      generationId: getActiveGenerationId(),
      hookStates: {},
      createdAt: now,
      frozenRecallOptions: null,
    };
    transaction.updatedAt = now;
    generationRecallTransactions.set(transactionId, transaction);
    return transaction;
  }

  function findRecentGenerationRecallTransactionForChat(
    chatId = getCurrentChatId(),
    now = Date.now(),
  ) {
    const normalizedChatId = normalizeChatIdCandidate(chatId);
    if (!normalizedChatId) return null;

    // 跨代际隔离：当宿主提供了当前生成代际 id 时，只桥接“同一次生成”的事务。
    // 这阻止上一轮 normal 生成遗留的事务被本轮 reroll 复用，从而保证
    // reroll 真正进入 runRecall → 持久召回复用门禁，而不是继承旧的 fresh 结果。
    const activeGenerationId = getActiveGenerationId();

    let latestTransaction = null;
    for (const transaction of generationRecallTransactions.values()) {
      if (!transaction || String(transaction.chatId || "") !== normalizedChatId)
        continue;
      if (!isGenerationRecallTransactionWithinBridgeWindow(transaction, now))
        continue;
      if (activeGenerationId) {
        const transactionGenerationId = String(transaction.generationId || "").trim();
        if (transactionGenerationId && transactionGenerationId !== activeGenerationId) {
          continue;
        }
      }
      if (
        !latestTransaction ||
        Number(transaction.updatedAt || 0) >
          Number(latestTransaction.updatedAt || 0)
      ) {
        latestTransaction = transaction;
      }
    }

    return latestTransaction;
  }

  function shouldReuseRecentGenerationRecallTransaction(
    transaction,
    hookName,
    recallKey = "",
    now = Date.now(),
  ) {
    if (!transaction || !hookName) return false;
    if (!isGenerationRecallTransactionWithinBridgeWindow(transaction, now)) {
      return false;
    }

    const hookStates = transaction.hookStates || {};
    const normalizedRecallKey = String(recallKey || "");
    const transactionRecallKey = String(transaction.recallKey || "");

    if (Object.values(hookStates).includes("running")) {
      return true;
    }

    const peerHookName = getGenerationRecallPeerHookName(hookName);
    const peerHookState = peerHookName ? hookStates[peerHookName] : "";
    if (peerHookState) {
      return true;
    }

    const ownState = hookStates[hookName];
    if (ownState) {
      return ownState === "running";
    }

    if (!Object.keys(hookStates).length) {
      if (!transactionRecallKey) {
        return true;
      }
      if (!normalizedRecallKey) {
        return false;
      }
      if (normalizedRecallKey !== transactionRecallKey) {
        return false;
      }
      return true;
    }

    return false;
  }

  function markGenerationRecallTransactionHookState(
    transaction,
    hookName,
    state = "completed",
  ) {
    if (!transaction?.id || !hookName) return transaction;
    transaction.hookStates ||= {};
    transaction.hookStates[hookName] = state;
    transaction.updatedAt = Date.now();
    generationRecallTransactions.set(transaction.id, transaction);
    return transaction;
  }

  function getGenerationRecallTransactionResult(transaction) {
    return transaction?.lastRecallResult || null;
  }

  function storeGenerationRecallTransactionResult(
    transaction,
    recallResult = null,
    meta = {},
  ) {
    if (!transaction?.id) return transaction;
    transaction.lastRecallResult = recallResult ? { ...recallResult } : null;
    transaction.lastRecallMeta =
      meta && typeof meta === "object" ? { ...meta } : {};
    transaction.lastDeliveryMode =
      String(meta?.deliveryMode || recallResult?.deliveryMode || "").trim() ||
      transaction.lastDeliveryMode ||
      "";
    transaction.finalResolution = null;
    transaction.updatedAt = Date.now();
    generationRecallTransactions.set(transaction.id, transaction);
    return transaction;
  }

  function readGenerationRecallTransactionFinalResolution(transaction) {
    return transaction?.finalResolution || null;
  }

  function storeGenerationRecallTransactionFinalResolution(
    transaction,
    finalResolution = null,
  ) {
    if (!transaction?.id) return transaction;
    transaction.finalResolution = finalResolution ? { ...finalResolution } : null;
    transaction.updatedAt = Date.now();
    generationRecallTransactions.set(transaction.id, transaction);
    return transaction;
  }

  function clearGenerationRecallTransactionsForChat(
    chatId = getCurrentChatId(),
    { clearAll = false } = {},
  ) {
    let removed = 0;
    const normalizedChatId = String(chatId || "");
    if (clearAll || !normalizedChatId) {
      removed = generationRecallTransactions.size;
      generationRecallTransactions.clear();
      return removed;
    }

    for (const [
      transactionId,
      transaction,
    ] of generationRecallTransactions.entries()) {
      if (String(transaction?.chatId || "") !== normalizedChatId) continue;
      generationRecallTransactions.delete(transactionId);
      removed += 1;
    }

    return removed;
  }

  function createGenerationRecallContext({
    hookName,
    generationType = "normal",
    recallOptions = {},
    chatId = getCurrentChatId(),
  } = {}) {
    const context = getContext();
    const chat = context?.chat;
    const normalizedChatId = normalizeChatIdCandidate(
      chatId || context?.chatId || getCurrentChatId(),
    );
    const effectiveGenerationType = normalizeGenerationRecallTransactionType(
      recallOptions?.generationType || generationType,
    );
    const plannerRecallHandoff =
      effectiveGenerationType === "normal"
        ? deps.peekPlannerRecallHandoff(normalizedChatId)
        : null;
    const effectiveRecallOptions = plannerRecallHandoff
      ? {
          ...(recallOptions || {}),
          overrideUserMessage: plannerRecallHandoff.rawUserInput,
          overrideSource: plannerRecallHandoff.source || "planner-handoff",
          overrideSourceLabel:
            plannerRecallHandoff.sourceLabel || "Planner handoff",
          overrideReason: "planner-handoff-reuse",
          sourceCandidates: [
            {
              text: plannerRecallHandoff.rawUserInput,
              source: plannerRecallHandoff.source || "planner-handoff",
              sourceLabel:
                plannerRecallHandoff.sourceLabel || "Planner handoff",
              reason: "planner-handoff-reuse",
              includeSyntheticUserMessage: false,
            },
          ],
          includeSyntheticUserMessage: false,
        }
      : recallOptions;

    const frozenRecallOptions = freezeGenerationRecallOptionsForTransaction(
      chat,
      generationType,
      effectiveRecallOptions,
    );
    if (!frozenRecallOptions) {
      return {
        hookName,
        generationType,
        recallKey: "",
        transaction: null,
        recallOptions: null,
        shouldRun: false,
        guardReason: "missing-frozen-recall-options",
      };
    }

    const transactionGenerationType = normalizeGenerationRecallTransactionType(
      frozenRecallOptions.generationType || generationType,
    );
    const fallbackRecallKey =
      effectiveRecallOptions?.recallKey ||
      buildPreGenerationRecallKey(transactionGenerationType, {
        ...frozenRecallOptions,
        chatId: normalizedChatId,
        userMessage: frozenRecallOptions.overrideUserMessage,
      });

    if (!normalizedChatId || !String(fallbackRecallKey || "").trim()) {
      return {
        hookName,
        generationType: transactionGenerationType,
        recallKey: "",
        transaction: null,
        recallOptions: null,
        shouldRun: false,
        guardReason: !normalizedChatId ? "missing-chat-id" : "missing-recall-key",
      };
    }

    const now = Date.now();
    const recentTransaction = findRecentGenerationRecallTransactionForChat(
      normalizedChatId,
      now,
    );
    let transaction = recentTransaction;
    if (
      !shouldReuseRecentGenerationRecallTransaction(
        transaction,
        hookName,
        fallbackRecallKey,
        now,
      )
    ) {
      transaction = beginGenerationRecallTransaction({
        chatId: normalizedChatId,
        generationType: transactionGenerationType,
        recallKey: fallbackRecallKey,
        forceNew: true,
      });
    }

    if (!transaction) {
      return {
        hookName,
        generationType: transactionGenerationType,
        recallKey: "",
        transaction: null,
        recallOptions: null,
        shouldRun: false,
        guardReason: "transaction-unavailable",
      };
    }

    const normalizedTransactionChatId = normalizeChatIdCandidate(
      transaction.chatId,
    );
    const transactionRecallKey = String(transaction.recallKey || "").trim();
    const peerHookName = getGenerationRecallPeerHookName(hookName);
    const hasPeerHookState = Boolean(
      peerHookName && transaction.hookStates?.[peerHookName],
    );
    if (
      normalizedTransactionChatId !== normalizedChatId ||
      !transactionRecallKey ||
      (!hasPeerHookState && transactionRecallKey !== String(fallbackRecallKey))
    ) {
      return {
        hookName,
        generationType: transactionGenerationType,
        recallKey: String(fallbackRecallKey || ""),
        transaction,
        recallOptions: null,
        shouldRun: false,
        guardReason: "transaction-mismatch",
      };
    }

    if (
      !transaction.frozenRecallOptions ||
      typeof transaction.frozenRecallOptions !== "object"
    ) {
      transaction.frozenRecallOptions = {
        ...frozenRecallOptions,
        lockedSource:
          frozenRecallOptions?.lockedSource ||
          frozenRecallOptions?.overrideSource ||
          frozenRecallOptions?.source ||
          "",
        lockedSourceLabel:
          frozenRecallOptions?.lockedSourceLabel ||
          frozenRecallOptions?.overrideSourceLabel ||
          frozenRecallOptions?.sourceLabel ||
          "",
        lockedReason:
          frozenRecallOptions?.lockedReason ||
          frozenRecallOptions?.overrideReason ||
          frozenRecallOptions?.reason ||
          "",
        lockedAt: now,
      };
    }
    if (!String(transaction.generationType || "").trim()) {
      transaction.generationType = transactionGenerationType;
    }
    transaction.updatedAt = now;
    generationRecallTransactions.set(transaction.id, transaction);

    const boundRecallOptions = {
      ...(transaction.frozenRecallOptions || frozenRecallOptions),
      recallKey: transaction.recallKey,
      generationType:
        transaction.frozenRecallOptions?.generationType || generationType,
    };
    if (plannerRecallHandoff?.result) {
      boundRecallOptions.cachedRecallPayload = {
        handoffId: plannerRecallHandoff.id,
        chatId: plannerRecallHandoff.chatId,
        result: plannerRecallHandoff.result,
        recentMessages: Array.isArray(plannerRecallHandoff.recentMessages)
          ? plannerRecallHandoff.recentMessages.map((item) => String(item || ""))
          : [],
        injectionText: String(plannerRecallHandoff.injectionText || ""),
        source: plannerRecallHandoff.source || "planner-handoff",
        sourceLabel: plannerRecallHandoff.sourceLabel || "Planner handoff",
        reason: "planner-handoff-reuse",
      };
    }

    const recallKey = transactionRecallKey;
    const shouldRun = deps.shouldRunRecallForTransaction(transaction, hookName);

    return {
      hookName,
      generationType: boundRecallOptions.generationType,
      recallKey,
      transaction,
      recallOptions: boundRecallOptions,
      shouldRun,
      guardReason: shouldRun ? "" : "transaction-not-runnable",
    };
  }

  return {
    generationRecallTransactions,
    buildPreGenerationRecallKey,
    cleanupGenerationRecallTransactions,
    getGenerationRecallPeerHookName,
    isGenerationRecallTransactionWithinBridgeWindow,
    normalizeGenerationRecallTransactionType,
    resolveGenerationRecallDeliveryMode,
    shouldUseAuthoritativeGenerationRecallInput,
    shouldPreserveAuthoritativeGenerationRecallText,
    freezeGenerationRecallOptionsForTransaction,
    buildGenerationRecallTransactionId,
    beginGenerationRecallTransaction,
    findRecentGenerationRecallTransactionForChat,
    shouldReuseRecentGenerationRecallTransaction,
    markGenerationRecallTransactionHookState,
    getGenerationRecallTransactionResult,
    storeGenerationRecallTransactionResult,
    readGenerationRecallTransactionFinalResolution,
    storeGenerationRecallTransactionFinalResolution,
    clearGenerationRecallTransactionsForChat,
    createGenerationRecallContext,
  };
}
