import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import {
  onBeforeCombinePromptsController,
  onGenerationAfterCommandsController,
  onGenerationStartedController,
  onMessageReceivedController,
  onMessageSentController,
} from "../../host/event-binding.js";
import { isSystemMessageForExtraction } from "../../maintenance/chat-history.js";
import { resolveAutoExtractionPlanController } from "../../maintenance/extraction-controller.js";
import {
  GRAPH_LOAD_STATES,
  GRAPH_METADATA_KEY,
  GRAPH_PERSISTENCE_META_KEY,
  MODULE_NAME,
} from "../../graph/graph-persistence.js";
import { getSmartTriggerDecision } from "../../maintenance/smart-trigger.js";
import {
  buildPersistedRecallRecord,
  bumpPersistedRecallGenerationCount,
  readPersistedRecallFromUserMessage,
  resolveFinalRecallInjectionSource,
  writePersistedRecallToUserMessage,
} from "../../retrieval/recall-persistence.js";
import {
  createGraphPersistenceState,
  createRecallInputRecord,
  createRecallRunResult,
      createUiStatus,
      getGenerationRecallHookStateFromResult,
      getRecallHookLabel,
      getStageNoticeDuration,
      getStageNoticeTitle,
  hashRecallInput,
  isFreshRecallInputRecord,
  isTerminalGenerationRecallHookState,
  isTrivialUserInput,
  normalizeRecallInputText,
  normalizeStageNoticeLevel,
  shouldRunRecallForTransaction,
} from "../../ui/ui-status.js";
import {
  defaultSettings,
  mergePersistedSettings,
} from "../../runtime/settings-defaults.js";
import {
  createDefaultAuthorityCapabilityState,
  normalizeAuthoritySettings,
  normalizeAuthorityCapabilityState,
  probeAuthorityCapabilities,
} from "../../runtime/authority-capabilities.js";
import {
  createAuthorityBrowserState,
  getAuthorityBrowserStateSnapshot,
  normalizeAuthorityBrowserState,
  recordAuthorityAcceptedRevision,
} from "../../sync/authority-browser-state.js";
import {
  consumeRerollRecallReuseMarker,
  createRerollRecallReuseMarker,
} from "../../runtime/reroll-transaction-boundary.js";

const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const indexPath = path.resolve(moduleDir, "../../index.js");

export function createGenerationRecallHarness(options = {}) {
  const { realApplyFinal = false } = options;
  return fs.readFile(indexPath, "utf8").then((source) => {
    const start = source.indexOf("const RECALL_INPUT_RECORD_TTL_MS = 60000;");
    const end = source.indexOf(
      'function onMessageReceived(messageId = null, type = "") {',
    );
    const endFallback = source.indexOf("async function runExtraction()");
    const resolvedEnd = end >= 0 ? end : endFallback;
    if (start < 0 || resolvedEnd < 0 || resolvedEnd <= start) {
      throw new Error("无法从 index.js 提取生成召回事务定义");
    }
    const snippet = source
      .slice(start, resolvedEnd)
      .replace(/^export\s+/gm, "");
    const context = {
      console,
      Date,
      Map,
      setTimeout,
      clearTimeout,
      __sendTextareaValue: "",
      document: {
        getElementById(id) {
          if (
            id === "send_textarea" &&
            typeof context.__sendTextareaValue === "string" &&
            context.__sendTextareaValue
          ) {
            return { value: context.__sendTextareaValue };
          }
          return null;
        },
      },
      result: null,
      currentGraph: {},
      _panelModule: null,
      defaultSettings,
      mergePersistedSettings,
      createDefaultAuthorityCapabilityState,
      normalizeAuthoritySettings,
      normalizeAuthorityCapabilityState,
      probeAuthorityCapabilities,
      createAuthorityBrowserState,
      getAuthorityBrowserStateSnapshot,
      normalizeAuthorityBrowserState,
      recordAuthorityAcceptedRevision,
      consumeRerollRecallReuseMarker,
      createRerollRecallReuseMarker,
      settings: {},
      graphPersistenceState: createGraphPersistenceState(),
      extension_settings: { [MODULE_NAME]: {} },
      extension_prompt_types: {
        NONE: 0,
        BEFORE_PROMPT: 1,
        IN_PROMPT: 2,
        IN_CHAT: 3,
      },
      extension_prompt_roles: {
        SYSTEM: 0,
        USER: 1,
        ASSISTANT: 2,
      },
      clampInt: (value, fallback = 0, min = 0, max = 9999) => {
        const numeric = Number(value);
        if (!Number.isFinite(numeric)) return fallback;
        return Math.min(max, Math.max(min, Math.trunc(numeric)));
      },
      getHostAdapter: () => null,
      migrateLegacyTaskProfiles: (settings = {}) => ({
        taskProfilesVersion: settings?.taskProfilesVersion || 0,
        taskProfiles: settings?.taskProfiles || {},
      }),
      migratePerTaskRegexToGlobal: (settings = {}) => ({
        changed: false,
        settings,
      }),
      refreshPanelLiveStateController: () => {
        context.refreshPanelCalls += 1;
      },
      isRecalling: false,
      getCurrentChatId: () => "chat-main",
      normalizeChatIdCandidate: (value = "") => String(value ?? "").trim(),
      normalizeRecallInputText: (text = "") => String(text || "").trim(),
      isTrivialUserInput,
      getAssistantTurns: (chat = []) =>
        chat.flatMap((message, index) =>
          !message?.is_user &&
          !isSystemMessageForExtraction(message, { index, chat })
            ? [index]
            : [],
        ),
      isSystemMessageForExtraction,
      getLatestUserChatMessage: (chat = []) =>
        [...chat].reverse().find((message) => message?.is_user) || null,
      getLastNonSystemChatMessage: (chat = []) =>
        [...chat]
          .map((message, index) => ({ message, index }))
          .reverse()
          .find(
            ({ message, index }) =>
              !isSystemMessageForExtraction(message, { index, chat }),
          )?.message || null,
      getSmartTriggerDecision,
      getSendTextareaValue: () => context.__sendTextareaValue,
      getRecallUserMessageSourceLabel: (source = "") => source,
      getRecallUserMessageSourceLabelController: (source = "") => source,
      buildRecallRecentMessages: (
        chat = [],
        _limit,
        syntheticUserMessage = "",
      ) =>
        syntheticUserMessage
          ? [...chat, { is_user: true, mes: syntheticUserMessage }]
          : [...chat],
      getContext: () => ({
        chatId: "chat-main",
        chat: context.chat,
      }),
      chat: [],
      runRecallCalls: [],
      runExtractionCalls: [],
      extractionIssues: [],
      applyFinalCalls: [],
      moduleInjectionCalls: [],
      recordedInjectionSnapshots: [],
      refreshPanelCalls: 0,
      hideScheduleCalls: [],
      isExtracting: false,
      isRecoveringHistory: false,
      isRestoreLockActive: () => false,
      isAssistantChatMessage: (message) =>
        Boolean(message) && !message.is_user && !message.is_system,
      createRecallInputRecord,
      createRecallRunResult,
      hashRecallInput,
      isFreshRecallInputRecord,
      isTerminalGenerationRecallHookState,
      shouldRunRecallForTransaction,
      getGenerationRecallHookStateFromResult,
      createUiStatus,
      createGraphPersistenceState,
      getRecallHookLabel,
      getStageNoticeTitle,
      getStageNoticeDuration,
      normalizeStageNoticeLevel,
      MODULE_NAME,
      GRAPH_LOAD_STATES,
      GRAPH_METADATA_KEY,
      GRAPH_PERSISTENCE_META_KEY,
      resolveAutoExtractionPlanController,
      onBeforeCombinePromptsController,
      onGenerationAfterCommandsController,
      onGenerationStartedController,
      readPersistedRecallFromUserMessage,
      writePersistedRecallToUserMessage,
      buildPersistedRecallRecord,
      resolveFinalRecallInjectionSource,
      bumpPersistedRecallGenerationCount,
      applyModuleInjectionPrompt: (text = "") => {
        const normalizedText = String(text || "");
        context.moduleInjectionCalls.push(normalizedText);
        return {
          applied: Boolean(normalizedText.trim()),
          source: normalizedText.trim() ? "module-injection" : "rewrite-clear",
          mode: normalizedText.trim() ? "module-injection" : "rewrite-clear",
        };
      },
      getSettings: () => context.settings,
      $: () => ({}),
      triggerChatMetadataSave: () => {
        context.metadataSaveCalls += 1;
        return "debounced";
      },
      refreshPanelLiveState: () => {
        context.refreshPanelCalls += 1;
      },
      recordInjectionSnapshot: (_kind, snapshot = {}) => {
        context.recordedInjectionSnapshots.push({ ...snapshot });
      },
      schedulePersistedRecallMessageUiRefresh: () => {
        context.recallUiRefreshCalls += 1;
      },
      getMessageHideSettings: () => ({}),
      getHideRuntimeAdapters: () => ({}),
      scheduleHideSettingsApply: (...args) => {
        context.hideScheduleCalls.push(args);
      },
      estimateTokens: (text = "") =>
        normalizeRecallInputText(text)
          .split(/\s+/)
          .filter(Boolean).length || (normalizeRecallInputText(text) ? 1 : 0),
      resolveGenerationTargetUserMessageIndex: (
        chat = [],
        { generationType } = {},
      ) => {
        const normalized = String(generationType || "normal");
        if (!Array.isArray(chat) || chat.length === 0) return null;
        if (normalized === "normal")
          return chat[chat.length - 1]?.is_user ? chat.length - 1 : null;
        for (let index = chat.length - 1; index >= 0; index--)
          if (chat[index]?.is_user) return index;
        return null;
      },
      metadataSaveCalls: 0,
      recallUiRefreshCalls: 0,
    };
    vm.createContext(context);
    vm.runInContext(
      `${snippet}\nresult = { hashRecallInput, buildPreGenerationRecallKey, buildGenerationAfterCommandsRecallInput, buildNormalGenerationRecallInput, cleanupGenerationRecallTransactions, buildGenerationRecallTransactionId, beginGenerationRecallTransaction, markGenerationRecallTransactionHookState, shouldRunRecallForTransaction, createGenerationRecallContext, onGenerationStarted, onGenerationEnded, onGenerationAfterCommands, onBeforeCombinePrompts, applyFinalRecallInjectionForGeneration, persistRecallInjectionRecord, ensurePersistedRecallRecordForGeneration, findRecentGenerationRecallTransactionForChat, getGenerationRecallTransactionResult, generationRecallTransactions, freezeHostGenerationInputSnapshot, consumeHostGenerationInputSnapshot, getPendingHostGenerationInputSnapshot, clearPendingHostGenerationInputSnapshot, prepareRerollRecallReuse, getPendingRerollRecallReuse, clearPendingRerollRecallReuse, recordRecallSendIntent, clearPendingRecallSendIntent, recordRecallSentUserMessage, getPendingRecallSendIntent: () => pendingRecallSendIntent, getLastRecallSentUserMessage: () => lastRecallSentUserMessage, getCurrentGenerationTrivialSkip, markCurrentGenerationTrivialSkip, clearCurrentGenerationTrivialSkip, consumeCurrentGenerationTrivialSkip, deferAutoExtraction, maybeResumePendingAutoExtraction, clearPendingAutoExtraction, getPendingAutoExtraction: () => ({ ...pendingAutoExtraction }), getIsHostGenerationRunning: () => isHostGenerationRunning, preparePlannerRecallHandoff, runPlannerRecallForEna, getGraphPersistenceState: () => graphPersistenceState, setGraphPersistenceState: (value = {}) => { graphPersistenceState = { ...graphPersistenceState, ...(value || {}) }; return graphPersistenceState; } };`,
      context,
      { filename: indexPath },
    );

    Object.defineProperties(context, {
      pendingRecallSendIntent: {
        get() {
          return context.result.getPendingRecallSendIntent();
        },
        set(value) {
          if (value?.text) {
            context.result.recordRecallSendIntent(
              value?.text || "",
              value?.source,
            );
            return;
          }
          context.result.clearPendingRecallSendIntent();
        },
        configurable: true,
      },
      lastRecallSentUserMessage: {
        get() {
          return context.result.getLastRecallSentUserMessage();
        },
        set(value) {
          context.result.recordRecallSentUserMessage(
            value?.messageId,
            value?.text || "",
            value?.source,
          );
        },
        configurable: true,
      },
    });
    const originalApplyFinalRecallInjectionForGeneration =
      context.result.applyFinalRecallInjectionForGeneration;
    context.applyFinalRecallInjectionForGeneration = (payload = {}) => {
      context.applyFinalCalls.push({ ...payload });
      if (realApplyFinal) {
        return originalApplyFinalRecallInjectionForGeneration(payload);
      }
      return {
        source: "fresh",
        targetUserMessageIndex: null,
      };
    };
    context.runRecall = async (options = {}) => {
      context.runRecallCalls.push({ ...options });
      const overrideUserMessage = String(
        options.overrideUserMessage || options.userMessage || "",
      );
      return {
        status: "completed",
        didRecall: true,
        ok: true,
        injectionText: `注入:${overrideUserMessage}`,
        deliveryMode: String(options.deliveryMode || "immediate"),
        source: options.overrideSource,
        sourceLabel: options.overrideSourceLabel,
        reason: options.overrideReason,
        sourceCandidates: Array.isArray(options.sourceCandidates)
          ? options.sourceCandidates.map((candidate) => ({ ...candidate }))
          : [],
        selectedNodeIds: ["node-test-1"],
        retrievalMeta: {
          vectorHits: 1,
          vectorMergedHits: 0,
          diffusionHits: 0,
          candidatePoolAfterDpp: 1,
        },
        llmMeta: {
          status: "disabled",
          reason: "test-disabled",
          candidatePool: 0,
        },
        stats: {
          coreCount: 1,
          recallCount: 1,
        },
      };
    };
    context.runExtraction = async (...args) => {
      context.runExtractionCalls.push(args);
      return {
        ok: true,
      };
    };
    context.invokeOnMessageSent = (messageId = null) =>
      onMessageSentController(
        {
          getContext: context.getContext,
          isTrivialUserInput,
          recordRecallSentUserMessage: context.result.recordRecallSentUserMessage,
          refreshPersistedRecallMessageUi: () => {
            context.recallUiRefreshCalls += 1;
          },
        },
        messageId,
      );
    context.invokeOnMessageReceived = (messageId = null, type = "") =>
      onMessageReceivedController(
        {
          console,
          consumeCurrentGenerationTrivialSkip:
            context.result.consumeCurrentGenerationTrivialSkip,
          createRecallInputRecord,
          deferAutoExtraction: context.result.deferAutoExtraction,
          getContext: context.getContext,
          getCurrentGraph: () => context.currentGraph,
          getGraphPersistenceState: () => context.result.getGraphPersistenceState(),
          getIsHostGenerationRunning: () =>
            context.result.getIsHostGenerationRunning(),
          getPendingHostGenerationInputSnapshot:
            context.result.getPendingHostGenerationInputSnapshot,
          getPendingRecallSendIntent: () =>
            context.result.getPendingRecallSendIntent(),
          getLastProcessedAssistantFloor: () => -1,
          getSettings: () => context.settings,
          isAssistantChatMessage: (message) =>
            Boolean(message) && !message.is_user && !message.is_system,
          isFreshRecallInputRecord,
          isGraphMetadataWriteAllowed: () => true,
          syncGraphLoadFromLiveContext: () => {},
          maybeCaptureGraphShadowSnapshot: () => {},
          maybeFlushQueuedGraphPersist: () => {},
          notifyExtractionIssue: (message) => {
            context.extractionIssues.push(String(message || ""));
          },
          queueMicrotask: (task) => task(),
          resolveAutoExtractionPlan: (options = {}) =>
            resolveAutoExtractionPlanController(
              {
                getAssistantTurns(chat = []) {
                  return chat.flatMap((message, index) =>
                    !message?.is_user && !message?.is_system ? [index] : [],
                  );
                },
                getLastProcessedAssistantFloor: () => -1,
                getSettings: () => context.settings,
                getSmartTriggerDecision: () => ({
                  triggered: false,
                  score: 0,
                  reasons: [],
                }),
              },
              options,
            ),
          runExtraction: context.runExtraction,
          refreshPersistedRecallMessageUi: () => {
            context.recallUiRefreshCalls += 1;
          },
          setPendingHostGenerationInputSnapshot: () => {},
          setPendingRecallSendIntent: (record) => {
            if (record?.text) {
              context.result.recordRecallSendIntent(
                record.text || "",
                record.source,
              );
              return;
            }
            context.result.clearPendingRecallSendIntent();
          },
        },
        messageId,
        type,
      );
    return context;
  });
}
