// ST-BME: 任务级 EJS / 世界书渲染引擎
// 仅用于世界书条目渲染，不开放给用户自定义 prompt 块。

import { getSTContextSnapshot } from "../host/st-context.js";

const DEFAULT_MAX_RECURSION = 10;
let ejsRuntimeStatePromise = null;

const EJS_RUNTIME_STATUS = {
  PRIMARY: "primary",
  FALLBACK: "fallback",
  FAILED: "failed",
};

const FALLBACK_LODASH = {
  get: getByPath,
  cloneDeep,
  escapeRegExp,
  sum(values = []) {
    return (Array.isArray(values) ? values : []).reduce(
      (total, value) => total + (Number(value) || 0),
      0,
    );
  },
};

function getUtilityLib() {
  return globalThis._ || FALLBACK_LODASH;
}

function getEjsRuntime() {
  return globalThis.ejs || null;
}

function buildEjsRuntimeState(runtime, status, error = null) {
  return {
    runtime: runtime || null,
    status,
    isAvailable: Boolean(runtime),
    isFallback: status === EJS_RUNTIME_STATUS.FALLBACK,
    error: error || null,
  };
}

function getCurrentEjsRuntimeState() {
  const runtime = getEjsRuntime();
  if (!runtime) {
    return buildEjsRuntimeState(null, EJS_RUNTIME_STATUS.FAILED);
  }
  return buildEjsRuntimeState(runtime, EJS_RUNTIME_STATUS.PRIMARY);
}

function createTaskEjsRuntimeUnavailableError(backend, content = "") {
  const error = new Error(
    `task-ejs runtime unavailable (${backend?.status || EJS_RUNTIME_STATUS.FAILED})`,
  );
  error.name = "TaskEjsRuntimeUnavailableError";
  error.code = "st_bme_task_ejs_runtime_unavailable";
  error.backend = backend || null;
  error.content = String(content || "");
  return error;
}

function createTaskEjsUnsupportedHelperError(helperName, args = []) {
  const error = new Error(`task-ejs unsupported helper: ${String(helperName || "unknown")}`);
  error.name = "TaskEjsUnsupportedHelperError";
  error.code = "st_bme_task_ejs_unsupported_helper";
  error.helperName = String(helperName || "unknown");
  error.args = Array.isArray(args) ? cloneDeep(args) : [];
  return error;
}

async function ensureEjsRuntime() {
  const currentState = getCurrentEjsRuntimeState();
  if (currentState.isAvailable) {
    return currentState;
  }
  if (ejsRuntimeStatePromise) {
    return await ejsRuntimeStatePromise;
  }

  ejsRuntimeStatePromise = (async () => {
    const hadWindow = Object.prototype.hasOwnProperty.call(globalThis, "window");
    const previousWindow = globalThis.window;
    let importError = null;

    if (!hadWindow) {
      globalThis.window = globalThis;
    }

    try {
      await import("../vendor/ejs.js");
    } catch (error) {
      importError = error;
      console.warn("[ST-BME] task-ejs 加载 ../vendor/ejs.js 失败:", error);
    } finally {
      if (!hadWindow) {
        delete globalThis.window;
      } else {
        globalThis.window = previousWindow;
      }
    }

    const runtime = getEjsRuntime();
    if (runtime) {
      return buildEjsRuntimeState(runtime, EJS_RUNTIME_STATUS.FALLBACK);
    }
    return buildEjsRuntimeState(null, EJS_RUNTIME_STATUS.FAILED, importError);
  })();

  return await ejsRuntimeStatePromise;
}

async function resolveTaskEjsBackend(options = {}) {
  if (options.ensureRuntime === false) {
    return getCurrentEjsRuntimeState();
  }
  return await ensureEjsRuntime();
}

function resolveHostSnapshot(injectedSnapshot) {
  if (injectedSnapshot?.snapshot) {
    return injectedSnapshot;
  }
  return getSTContextSnapshot();
}

function getStChat(injectedSnapshot) {
  return resolveHostSnapshot(injectedSnapshot).snapshot.chat.messages || [];
}

function buildTemplateContext(templateContext = {}, hostSnapshot) {
  const resolvedHost = resolveHostSnapshot(hostSnapshot);
  const snapshot = resolvedHost.snapshot;
  const promptAliases = resolvedHost.prompt || {};
  const lastUserMessage =
    typeof templateContext.user_input === "string"
      ? templateContext.user_input
      : snapshot.chat.lastUserMessage || "";

  return {
    userMessage: "",
    recentMessages: "",
    chatMessages: [],
    dialogueText: "",
    candidateText: "",
    candidateNodes: [],
    nodeContent: "",
    eventSummary: "",
    characterSummary: "",
    threadSummary: "",
    contradictionSummary: "",
    graphStats: "",
    schema: "",
    currentRange: "",
    worldInfoBefore: "",
    worldInfoAfter: "",
    worldInfoBeforeEntries: [],
    worldInfoAfterEntries: [],
    worldInfoAtDepthEntries: [],
    activatedWorldInfoNames: [],
    taskAdditionalMessages: [],
    user: snapshot.user.name,
    char: snapshot.character.name,
    userName: promptAliases.userName || snapshot.user.name,
    charName: promptAliases.charName || snapshot.character.name,
    assistantName: promptAliases.charName || snapshot.character.name,
    persona: promptAliases.userPersona || snapshot.persona.text,
    userPersona: promptAliases.userPersona || snapshot.persona.text,
    charDescription:
      promptAliases.charDescription || snapshot.character.description,
    currentTime: promptAliases.currentTime || snapshot.time.current,
    stSnapshot: snapshot,
    hostSnapshot: snapshot,
    lastUserMessage,
    last_user_message: lastUserMessage,
    userInput: lastUserMessage,
    user_input: lastUserMessage,
    original: "",
    input: "",
    lastMessage: "",
    lastMessageId: "",
    newline: "\n",
    trim: "",
    ...templateContext,
  };
}

function cloneDeep(value) {
  if (value == null) return value;
  try {
    if (typeof structuredClone === "function") {
      return structuredClone(value);
    }
  } catch {
    // ignore and fall back to JSON
  }

  try {
    return JSON.parse(JSON.stringify(value));
  } catch {
    return value;
  }
}

function isPlainObject(value) {
  if (!value || typeof value !== "object") {
    return false;
  }
  if (Array.isArray(value)) {
    return false;
  }
  const prototype = Object.getPrototypeOf(value);
  return prototype === Object.prototype || prototype === null;
}

function getByPath(target, path, defaultValue = undefined) {
  const result = String(path || "")
    .split(".")
    .filter(Boolean)
    .reduce((acc, key) => (acc == null ? undefined : acc[key]), target);
  return result === undefined ? defaultValue : result;
}

function escapeRegExp(value) {
  return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

function normalizeEntryKey(value) {
  return String(value ?? "").trim();
}

function isEntryIdentifier(value) {
  return (
    typeof value === "string" ||
    typeof value === "number" ||
    value instanceof RegExp
  );
}

function cloneRegExp(pattern) {
  return new RegExp(pattern.source, pattern.flags);
}

function matchesWorldbookIdentifier(worldbook, identifier) {
  if (!isEntryIdentifier(identifier)) {
    return false;
  }

  if (identifier instanceof RegExp) {
    return cloneRegExp(identifier).test(String(worldbook || ""));
  }

  return normalizeEntryKey(worldbook) === normalizeEntryKey(identifier);
}

function matchesEntryIdentifier(entry = {}, identifier) {
  if (!isEntryIdentifier(identifier)) {
    return false;
  }

  const entryName = normalizeEntryKey(entry.name);
  const entryComment = normalizeEntryKey(entry.comment);
  const entryUid = Number(entry.uid) || 0;

  if (identifier instanceof RegExp) {
    const pattern = cloneRegExp(identifier);
    return pattern.test(entryComment) || pattern.test(entryName);
  }

  if (typeof identifier === "number") {
    return entryUid === identifier;
  }

  const normalizedIdentifier = normalizeEntryKey(identifier);
  if (!normalizedIdentifier) {
    return false;
  }

  return (
    entryComment === normalizedIdentifier ||
    entryName === normalizedIdentifier ||
    String(entryUid) === normalizedIdentifier
  );
}

function normalizeIdentifier(value) {
  return String(value || "")
    .trim()
    .toLowerCase();
}

function normalizeRole(role) {
  const normalized = String(role || "system").trim().toLowerCase();
  return ["system", "user", "assistant"].includes(normalized)
    ? normalized
    : "system";
}

function processChatMessage(message) {
  return String(message?.mes ?? message?.message ?? message?.content ?? "");
}

export function substituteTaskEjsParams(
  text,
  templateContext = {},
  options = {},
) {
  if (!text || !String(text).includes("{{")) {
    return String(text || "");
  }

  const context = buildTemplateContext(
    templateContext,
    options.hostSnapshot || templateContext.hostSnapshot,
  );
  return String(text).replace(/\{\{\s*([a-zA-Z0-9_.$]+)\s*\}\}/g, (_, path) => {
    const value = getByPath(context, path);
    if (value == null) return "";
    if (typeof value === "object") {
      try {
        return JSON.stringify(value);
      } catch {
        return "";
      }
    }
    return String(value);
  });
}

function createReadOnlyVariableState(hostSnapshot) {
  const snapshot = resolveHostSnapshot(hostSnapshot).snapshot;
  const chat = snapshot.chat.messages || [];
  const lastMessage = chat[chat.length - 1] || {};
  const swipeId = Number(lastMessage?.swipe_id ?? 0);
  const messageVars =
    lastMessage?.variables && typeof lastMessage.variables === "object"
      ? cloneDeep(lastMessage.variables[swipeId] || {})
      : {};
  const globalVars = cloneDeep(snapshot.variables.global || {});
  const localVars = cloneDeep(snapshot.variables.local || {});

  return Object.freeze({
    globalVars,
    localVars,
    messageVars,
    cacheVars: {
      ...globalVars,
      ...localVars,
      ...messageVars,
    },
  });
}

function getVariable(state, path, options = {}) {
  const scope = normalizeIdentifier(options.scope);
  if (scope === "global") {
    return getByPath(state.globalVars, path, options.defaults);
  }
  if (scope === "local") {
    return getByPath(state.localVars, path, options.defaults);
  }
  if (scope === "message") {
    return getByPath(state.messageVars, path, options.defaults);
  }
  return getByPath(state.cacheVars, path, options.defaults);
}

function normalizeRenderEntry(entry = {}) {
  return {
    uid: Number(entry.uid) || 0,
    name: normalizeEntryKey(entry.name),
    comment: normalizeEntryKey(entry.comment),
    content: String(entry.content || ""),
    worldbook: normalizeEntryKey(entry.worldbook),
    role: normalizeRole(entry.role),
    position: Number(entry.position ?? 0),
    depth: Number(entry.depth ?? 0),
    order: Number(entry.order ?? 100),
    enabled: entry.enabled !== false,
    activationDebug:
      entry.activationDebug && typeof entry.activationDebug === "object"
        ? cloneDeep(entry.activationDebug)
        : null,
  };
}

function registerEntryLookup(lookup, key, entry) {
  const normalizedKey = normalizeEntryKey(key);
  if (!normalizedKey || lookup.has(normalizedKey)) return;
  lookup.set(normalizedKey, entry);
}

function registerEntries(renderCtx, entries = []) {
  for (const rawEntry of Array.isArray(entries) ? entries : []) {
    const entry = normalizeRenderEntry(rawEntry);
    renderCtx.entries.push(entry);
    registerEntryLookup(renderCtx.allEntries, entry.name, entry);
    registerEntryLookup(renderCtx.allEntries, entry.comment, entry);
    registerEntryLookup(renderCtx.allEntries, entry.uid, entry);

    if (!renderCtx.entriesByWorldbook.has(entry.worldbook)) {
      renderCtx.entriesByWorldbook.set(entry.worldbook, new Map());
    }
    const worldbookLookup = renderCtx.entriesByWorldbook.get(entry.worldbook);
    registerEntryLookup(worldbookLookup, entry.name, entry);
    registerEntryLookup(worldbookLookup, entry.comment, entry);
    registerEntryLookup(worldbookLookup, entry.uid, entry);
  }
}

function activationKey(entry) {
  return [entry.worldbook, entry.uid || entry.comment || entry.name].join("::");
}

function recordRenderWarning(renderCtx, warning) {
  const text = String(warning || "").trim();
  if (!text) return;
  if (!Array.isArray(renderCtx?.warnings)) {
    renderCtx.warnings = [];
  }
  if (!renderCtx.warnings.includes(text)) {
    renderCtx.warnings.push(text);
  }
}

async function ensureWorldbookEntriesLoaded(renderCtx, worldbookName) {
  const normalizedWorldbook = normalizeEntryKey(worldbookName);
  if (!normalizedWorldbook) {
    return false;
  }
  if (renderCtx.entriesByWorldbook.has(normalizedWorldbook)) {
    return true;
  }
  if (renderCtx.worldbookLoadAttempts.has(normalizedWorldbook)) {
    return renderCtx.entriesByWorldbook.has(normalizedWorldbook);
  }
  if (typeof renderCtx.loadWorldbookEntries !== "function") {
    return false;
  }

  renderCtx.worldbookLoadAttempts.add(normalizedWorldbook);
  try {
    const loadedEntries = await renderCtx.loadWorldbookEntries(normalizedWorldbook);
    registerEntries(renderCtx, loadedEntries);
    if ((Array.isArray(loadedEntries) ? loadedEntries : []).length > 0) {
      renderCtx.lazyLoadedWorldbooks.add(normalizedWorldbook);
      return true;
    }
  } catch (error) {
    recordRenderWarning(
      renderCtx,
      `lazy load worldbook failed: ${normalizedWorldbook}`,
    );
    console.warn(
      `[ST-BME] task-ejs 懒加载世界书失败: ${normalizedWorldbook}`,
      error,
    );
  }

  return renderCtx.entriesByWorldbook.has(normalizedWorldbook);
}

function lookupEntryInMap(lookup, identifier) {
  if (!(lookup instanceof Map) || !isEntryIdentifier(identifier)) {
    return undefined;
  }

  if (!(identifier instanceof RegExp)) {
    const direct = lookup.get(normalizeEntryKey(identifier));
    if (direct) {
      return direct;
    }
  }

  for (const entry of lookup.values()) {
    if (matchesEntryIdentifier(entry, identifier)) {
      return entry;
    }
  }

  return undefined;
}

function buildCandidateLookups(renderCtx, currentWorldbook, explicitWorldbook = null) {
  const candidates = [];
  const seen = new Set();
  const pushLookup = (lookup) => {
    if (!(lookup instanceof Map) || seen.has(lookup)) {
      return;
    }
    seen.add(lookup);
    candidates.push(lookup);
  };

  if (typeof explicitWorldbook === "string") {
    pushLookup(renderCtx.entriesByWorldbook.get(normalizeEntryKey(explicitWorldbook)));
  } else if (explicitWorldbook instanceof RegExp) {
    for (const [worldbookName, lookup] of renderCtx.entriesByWorldbook.entries()) {
      if (matchesWorldbookIdentifier(worldbookName, explicitWorldbook)) {
        pushLookup(lookup);
      }
    }
  }

  const fallbackWorldbook = normalizeEntryKey(currentWorldbook);
  if (fallbackWorldbook) {
    pushLookup(renderCtx.entriesByWorldbook.get(fallbackWorldbook));
  }

  pushLookup(renderCtx.allEntries);
  return candidates;
}

async function resolveEntry(renderCtx, currentWorldbook, worldbookOrEntry, entryNameOrData) {
  const hasExplicitWorldbook = isEntryIdentifier(entryNameOrData);
  const explicitWorldbook = hasExplicitWorldbook ? worldbookOrEntry : null;
  const fallbackWorldbook = normalizeEntryKey(currentWorldbook);
  const identifier = hasExplicitWorldbook ? entryNameOrData : worldbookOrEntry;

  if (!isEntryIdentifier(identifier)) {
    return undefined;
  }

  const directLookups = buildCandidateLookups(
    renderCtx,
    fallbackWorldbook,
    explicitWorldbook,
  );
  for (const lookup of directLookups) {
    const matched = lookupEntryInMap(lookup, identifier);
    if (matched) {
      return matched;
    }
  }

  if (typeof explicitWorldbook === "string" && normalizeEntryKey(explicitWorldbook)) {
    await ensureWorldbookEntriesLoaded(renderCtx, explicitWorldbook);
    const loadedLookups = buildCandidateLookups(
      renderCtx,
      fallbackWorldbook,
      explicitWorldbook,
    );
    for (const lookup of loadedLookups) {
      const matched = lookupEntryInMap(lookup, identifier);
      if (matched) {
        return matched;
      }
    }
  }

  if (!renderCtx.resolveIgnoredEntry || identifier instanceof RegExp) {
    return undefined;
  }

  const normalizedIdentifier = normalizeEntryKey(identifier);
  const explicitWorldbookName =
    typeof explicitWorldbook === "string" ? normalizeEntryKey(explicitWorldbook) : "";
  const ignoredEntry =
    renderCtx.resolveIgnoredEntry(
      explicitWorldbookName || fallbackWorldbook,
      normalizedIdentifier,
    ) || renderCtx.resolveIgnoredEntry("", normalizedIdentifier);
  if (ignoredEntry) {
    const descriptor = ignoredEntry.sourceName || ignoredEntry.name || normalizedIdentifier;
    recordRenderWarning(
      renderCtx,
      `mvu filtered world info blocked: ${ignoredEntry.worldbook ? `${ignoredEntry.worldbook}/` : ""}${descriptor}`,
    );
  }

  return undefined;
}

function parseActivateWorldInfoArgs(world, entryOrForce, maybeForce) {
  const hasExplicitWorldbook = isEntryIdentifier(entryOrForce);
  return {
    explicitWorldbook: hasExplicitWorldbook ? world : null,
    identifier: hasExplicitWorldbook ? entryOrForce : world,
    force:
      typeof maybeForce === "boolean"
        ? maybeForce
        : typeof entryOrForce === "boolean",
  };
}

function parseGetwiArgs(worldbookOrEntry, entryNameOrData, dataOrUndefined) {
  const hasExplicitWorldbook = isEntryIdentifier(entryNameOrData);
  return {
    explicitWorldbook: hasExplicitWorldbook ? worldbookOrEntry : null,
    identifier: hasExplicitWorldbook ? entryNameOrData : worldbookOrEntry,
    data: isPlainObject(hasExplicitWorldbook ? dataOrUndefined : entryNameOrData)
      ? cloneDeep(hasExplicitWorldbook ? dataOrUndefined : entryNameOrData)
      : {},
  };
}

function mergeEjsExtraEnv(...values) {
  const utilityLib = getUtilityLib();
  const merge = typeof utilityLib?.merge === "function" ? utilityLib.merge : null;
  const plainValues = values.filter((value) => isPlainObject(value));
  if (plainValues.length === 0) {
    return {};
  }
  if (merge) {
    return merge({}, ...plainValues.map((value) => cloneDeep(value)));
  }
  return Object.assign({}, ...plainValues.map((value) => ({ ...value })));
}

async function activateWorldInfoInContext(
  renderCtx,
  currentWorldbook,
  world,
  entryOrForce,
  maybeForce,
) {
  const parsed = parseActivateWorldInfoArgs(world, entryOrForce, maybeForce);
  const identifierLabel =
    parsed.identifier instanceof RegExp
      ? parsed.identifier.toString()
      : normalizeEntryKey(parsed.identifier);
  const explicitWorldbookLabel =
    typeof parsed.explicitWorldbook === "string"
      ? normalizeEntryKey(parsed.explicitWorldbook)
      : parsed.explicitWorldbook instanceof RegExp
        ? parsed.explicitWorldbook.toString()
        : "";
  const entry = await resolveEntry(
    renderCtx,
    currentWorldbook,
    parsed.explicitWorldbook,
    parsed.identifier,
  );

  if (!entry) {
    recordRenderWarning(
      renderCtx,
      `activewi target not found: ${explicitWorldbookLabel ? `${explicitWorldbookLabel}/` : ""}${identifierLabel}`,
    );
    return null;
  }

  const normalizedEntry = normalizeRenderEntry({
    ...entry,
    content: String(entry.content || "").replaceAll("@@dont_activate", ""),
  });

  renderCtx.forcedActivatedEntries.set(activationKey(normalizedEntry), normalizedEntry);
  return {
    world: normalizedEntry.worldbook,
    comment: normalizedEntry.comment || normalizedEntry.name,
    content: normalizedEntry.content,
    forced: parsed.force,
  };
}

async function getwi(
  renderCtx,
  currentWorldbook,
  worldbookOrEntry,
  entryNameOrData,
  dataOrUndefined,
) {
  const parsed = parseGetwiArgs(
    worldbookOrEntry,
    entryNameOrData,
    dataOrUndefined,
  );
  const entry = await resolveEntry(
    renderCtx,
    currentWorldbook,
    parsed.explicitWorldbook,
    parsed.identifier,
  );
  if (!entry) {
    return "";
  }

  const entryKey = activationKey(entry);
  if (renderCtx.renderStack.has(entryKey)) {
    recordRenderWarning(
      renderCtx,
      `recursive getwi blocked: ${entry.comment || entry.name}`,
    );
    console.warn(
      `[ST-BME] task-ejs 检测到循环 getwi: ${entry.comment || entry.name}`,
    );
    return "";
  }

  if (renderCtx.renderStack.size >= renderCtx.maxRecursion) {
    recordRenderWarning(
      renderCtx,
      `getwi recursion limit reached: ${entry.comment || entry.name}`,
    );
    console.warn(
      `[ST-BME] task-ejs 超过最大递归深度: ${renderCtx.maxRecursion}`,
    );
    return "";
  }

  const processed = substituteTaskEjsParams(entry.content, renderCtx.templateContext, {
    hostSnapshot: renderCtx.hostSnapshot,
  });
  let finalContent = processed;

  if (processed.includes("<%")) {
    renderCtx.renderStack.add(entryKey);
    try {
      finalContent = await evalTaskEjsTemplate(processed, renderCtx, {
        ...mergeEjsExtraEnv(parsed.data),
        world_info: {
          comment: entry.comment || entry.name,
          name: entry.name,
          world: entry.worldbook,
        },
      });
    } finally {
      renderCtx.renderStack.delete(entryKey);
    }
  }

  renderCtx.inlinePulledEntries.set(entryKey, {
    name: entry.name,
    comment: entry.comment,
    content: finalContent,
    worldbook: entry.worldbook,
  });

  return String(finalContent || "");
}

function getChatMessageCompat(renderCtx, index, role) {
  const chat = getStChat(renderCtx?.hostSnapshot)
    .filter((message) => {
      if (!role) return true;
      if (role === "user") return Boolean(message?.is_user);
      if (role === "system") return Boolean(message?.is_system);
      return !message?.is_user && !message?.is_system;
    })
    .map(processChatMessage);

  const resolvedIndex = index >= 0 ? index : chat.length + index;
  return chat[resolvedIndex] || "";
}

function getChatMessagesCompat(renderCtx, startOrCount, endOrRole, role) {
  const chat = getStChat(renderCtx?.hostSnapshot);
  const allMessages = chat.map((message, index) => ({
    raw: message,
    id: index,
    text: processChatMessage(message),
  }));

  const filterByRole = (items, currentRole) => {
    if (!currentRole) return items;
    return items.filter((item) => {
      if (currentRole === "user") return Boolean(item.raw?.is_user);
      if (currentRole === "system") return Boolean(item.raw?.is_system);
      return !item.raw?.is_user && !item.raw?.is_system;
    });
  };

  if (endOrRole == null) {
    return (
      startOrCount > 0
        ? allMessages.slice(0, startOrCount)
        : allMessages.slice(startOrCount)
    ).map((item) => item.text);
  }

  if (typeof endOrRole === "string") {
    const filtered = filterByRole(allMessages, endOrRole);
    return (
      startOrCount > 0
        ? filtered.slice(0, startOrCount)
        : filtered.slice(startOrCount)
    ).map((item) => item.text);
  }

  return filterByRole(allMessages, role)
    .slice(startOrCount, endOrRole)
    .map((item) => item.text);
}

function matchChatMessagesCompat(renderCtx, pattern) {
  const regex = typeof pattern === "string" ? new RegExp(pattern, "i") : pattern;
  return getStChat(renderCtx?.hostSnapshot).some((message) =>
    regex.test(processChatMessage(message)),
  );
}

function rethrow(err, str, filename, lineNumber, esc) {
  const lines = String(str || "").split("\n");
  const start = Math.max(lineNumber - 3, 0);
  const end = Math.min(lines.length, lineNumber + 3);
  const escapedFileName =
    typeof esc === "function" ? esc(filename) : filename || "ejs";
  const context = lines
    .slice(start, end)
    .map((line, index) => {
      const currentLine = index + start + 1;
      return `${currentLine === lineNumber ? " >> " : "    "}${currentLine}| ${line}`;
    })
    .join("\n");

  err.message = `${escapedFileName}:${lineNumber}\n${context}\n\n${err.message}`;
  throw err;
}

function makeUnsupportedHelper(helperName) {
  return (...args) => {
    throw createTaskEjsUnsupportedHelperError(helperName, args);
  };
}

function getCurrentActivatedEntries(renderCtx) {
  return Array.isArray(renderCtx?.currentActivatedEntries)
    ? renderCtx.currentActivatedEntries
    : [];
}

export function createTaskEjsRenderContext(entries = [], options = {}) {
  const hostSnapshot = resolveHostSnapshot(options.hostSnapshot);
  const renderCtx = {
    entries: [],
    allEntries: new Map(),
    entriesByWorldbook: new Map(),
    renderStack: new Set(),
    worldbookLoadAttempts: new Set(),
    lazyLoadedWorldbooks: new Set(),
    warnings: [],
    maxRecursion:
      Number.isFinite(Number(options.maxRecursion)) && Number(options.maxRecursion) > 0
        ? Number(options.maxRecursion)
        : DEFAULT_MAX_RECURSION,
    hostSnapshot,
    variableState: createReadOnlyVariableState(hostSnapshot),
    currentActivatedEntries: Array.isArray(options.currentActivatedEntries)
      ? options.currentActivatedEntries.map((entry) => normalizeRenderEntry(entry))
      : [],
    forcedActivatedEntries: new Map(),
    inlinePulledEntries: new Map(),
    ejsRuntimeStatus: EJS_RUNTIME_STATUS.FAILED,
    ejsRuntimeFallback: false,
    ejsLastError: null,
    loadWorldbookEntries:
      typeof options.loadWorldbookEntries === "function"
        ? options.loadWorldbookEntries
        : null,
    resolveIgnoredEntry:
      typeof options.resolveIgnoredEntry === "function"
        ? options.resolveIgnoredEntry
        : null,
    templateContext: {
      ...(options.templateContext || {}),
      hostSnapshot: hostSnapshot.snapshot,
      stSnapshot: hostSnapshot.snapshot,
    },
  };

  registerEntries(renderCtx, entries);
  return renderCtx;
}

export async function evalTaskEjsTemplate(content, renderCtx, extraEnv = {}) {
  const backend = await resolveTaskEjsBackend();
  const runtime = backend.runtime;
  if (renderCtx && typeof renderCtx === "object") {
    renderCtx.ejsRuntimeStatus = backend.status;
    renderCtx.ejsRuntimeFallback = Boolean(backend.isFallback);
    renderCtx.ejsLastError = backend.error
      ? backend.error instanceof Error
        ? backend.error.message
        : String(backend.error)
      : null;
  }

  const hostSnapshot = resolveHostSnapshot(renderCtx?.hostSnapshot);
  const snapshot = hostSnapshot.snapshot;
  const templateAliases = buildTemplateContext(renderCtx?.templateContext || {}, hostSnapshot);
  const processed = substituteTaskEjsParams(content, renderCtx?.templateContext, {
    hostSnapshot,
  });

  if (!runtime) {
    if (processed.includes("<%")) {
      throw createTaskEjsRuntimeUnavailableError(backend, processed);
    }
    return processed;
  }

  if (!processed.includes("<%")) {
    return processed;
  }

  const stCtx = snapshot.raw || {};
  const chat = snapshot.chat.messages || [];
  const utilityLib = getUtilityLib();
  const templateRuntimeEnv = mergeEjsExtraEnv(templateAliases);
  const workflowUserInput =
    typeof renderCtx?.templateContext?.user_input === "string"
      ? renderCtx.templateContext.user_input
      : snapshot.chat.lastUserMessage || "";

  const unsupported = {
    setvar: makeUnsupportedHelper("setvar"),
    setLocalVar: makeUnsupportedHelper("setLocalVar"),
    setGlobalVar: makeUnsupportedHelper("setGlobalVar"),
    setMessageVar: makeUnsupportedHelper("setMessageVar"),
    incvar: makeUnsupportedHelper("incvar"),
    decvar: makeUnsupportedHelper("decvar"),
    delvar: makeUnsupportedHelper("delvar"),
    insvar: makeUnsupportedHelper("insvar"),
    incLocalVar: makeUnsupportedHelper("incLocalVar"),
    incGlobalVar: makeUnsupportedHelper("incGlobalVar"),
    incMessageVar: makeUnsupportedHelper("incMessageVar"),
    decLocalVar: makeUnsupportedHelper("decLocalVar"),
    decGlobalVar: makeUnsupportedHelper("decGlobalVar"),
    decMessageVar: makeUnsupportedHelper("decMessageVar"),
    patchVariables: makeUnsupportedHelper("patchVariables"),
    getprp: makeUnsupportedHelper("getprp"),
    getpreset: makeUnsupportedHelper("getpreset"),
    getPresetPrompt: makeUnsupportedHelper("getPresetPrompt"),
    execute: makeUnsupportedHelper("execute"),
    define: makeUnsupportedHelper("define"),
    getqr: makeUnsupportedHelper("getqr"),
    getQuickReply: makeUnsupportedHelper("getQuickReply"),
    selectActivatedEntries: makeUnsupportedHelper("selectActivatedEntries"),
    activateWorldInfoByKeywords: makeUnsupportedHelper("activateWorldInfoByKeywords"),
    activateRegex: makeUnsupportedHelper("activateRegex"),
    injectPrompt: makeUnsupportedHelper("injectPrompt"),
    getPromptsInjected: makeUnsupportedHelper("getPromptsInjected"),
    hasPromptsInjected: makeUnsupportedHelper("hasPromptsInjected"),
    jsonPatch: makeUnsupportedHelper("jsonPatch"),
  };

  const context = {
    _: utilityLib,
    console,
    ...templateRuntimeEnv,
    stat_data: renderCtx.variableState?.cacheVars?.stat_data,
    user: templateAliases.user,
    char: templateAliases.char,
    persona:
      templateAliases.persona || templateAliases.userPersona || snapshot.persona.text || "",
    userName: templateAliases.userName || snapshot.user.name,
    charName: templateAliases.charName || snapshot.character.name,
    assistantName:
      templateAliases.assistantName ||
      templateAliases.charName ||
      snapshot.character.name,
    charDescription:
      templateAliases.charDescription || snapshot.character.description || "",
    userPersona: templateAliases.userPersona || snapshot.persona.text || "",
    currentTime: templateAliases.currentTime || snapshot.time.current || "",
    characterId: snapshot.character.id,
    hostSnapshot: snapshot,
    stSnapshot: snapshot,
    get chatId() {
      return snapshot.chat.id || "";
    },
    get variables() {
      return renderCtx.variableState.cacheVars;
    },
    get stat_data() {
      return renderCtx.variableState?.cacheVars?.stat_data;
    },
    get lastUserMessageId() {
      if (typeof chat.findLastIndex === "function") {
        return chat.findLastIndex((message) => message?.is_user);
      }
      const reversedIndex = [...chat].reverse().findIndex((message) => message?.is_user);
      return reversedIndex < 0 ? -1 : chat.length - 1 - reversedIndex;
    },
    get lastUserMessage() {
      return (
        workflowUserInput ||
        chat.findLast?.((message) => message?.is_user)?.mes ||
        [...chat].reverse().find((message) => message?.is_user)?.mes ||
        ""
      );
    },
    get last_user_message() {
      return this.lastUserMessage;
    },
    get userInput() {
      return workflowUserInput;
    },
    get user_input() {
      return workflowUserInput;
    },
    get lastCharMessageId() {
      if (typeof chat.findLastIndex === "function") {
        return chat.findLastIndex(
          (message) => !message?.is_user && !message?.is_system,
        );
      }
      const reversedIndex = [...chat]
        .reverse()
        .findIndex((message) => !message?.is_user && !message?.is_system);
      return reversedIndex < 0 ? -1 : chat.length - 1 - reversedIndex;
    },
    get lastCharMessage() {
      return (
        chat.findLast?.((message) => !message?.is_user && !message?.is_system)?.mes ||
        [...chat]
          .reverse()
          .find((message) => !message?.is_user && !message?.is_system)?.mes ||
        ""
      );
    },
    get lastMessageId() {
      return chat.length - 1;
    },
    get charLoreBook() {
      return snapshot.worldbook.character || "";
    },
    get userLoreBook() {
      return snapshot.worldbook.persona || "";
    },
    get chatLoreBook() {
      return snapshot.worldbook.chat || "";
    },
    get charAvatar() {
      return snapshot.character.avatar || "";
    },
    userAvatar: snapshot.user.avatar || "",
    groups: stCtx.groups || [],
    groupId: snapshot.host.meta.selectedGroupId,
    get model() {
      return snapshot.host.meta.onlineStatus || "";
    },
    get SillyTavern() {
      return stCtx;
    },
    getwi: (worldbookOrEntry, entryNameOrData, dataOrUndefined) =>
      getwi(
        renderCtx,
        String(context.world_info?.world || ""),
        worldbookOrEntry,
        entryNameOrData,
        dataOrUndefined,
      ),
    getWorldInfo: (worldbookOrEntry, entryNameOrData, dataOrUndefined) =>
      getwi(
        renderCtx,
        String(context.world_info?.world || ""),
        worldbookOrEntry,
        entryNameOrData,
        dataOrUndefined,
      ),
    getvar: (path, options) => getVariable(renderCtx.variableState, path, options),
    getLocalVar: (path, options = {}) =>
      getVariable(renderCtx.variableState, path, {
        ...options,
        scope: "local",
      }),
    getGlobalVar: (path, options = {}) =>
      getVariable(renderCtx.variableState, path, {
        ...options,
        scope: "global",
      }),
    getMessageVar: (path, options = {}) =>
      getVariable(renderCtx.variableState, path, {
        ...options,
        scope: "message",
      }),
    getChatMessage: (id, role) => getChatMessageCompat(renderCtx, id, role),
    getChatMessages: (startOrCount = getStChat(hostSnapshot).length, endOrRole, role) =>
      getChatMessagesCompat(renderCtx, startOrCount, endOrRole, role),
    matchChatMessages: (pattern) => matchChatMessagesCompat(renderCtx, pattern),
    getchr: () => snapshot.character.description || "",
    evalTemplate: async (innerContent, data = {}) =>
      evalTaskEjsTemplate(innerContent, renderCtx, data),
    getWorldInfoData: async () =>
      renderCtx.entries.map((entry) => ({
        comment: entry.comment || entry.name,
        content: entry.content,
        world: entry.worldbook,
      })),
    getWorldInfoActivatedData: async () =>
      getCurrentActivatedEntries(renderCtx).map((entry) => ({
        comment: entry.comment || entry.name,
        content: entry.content,
        world: entry.worldbook,
      })),
    getEnabledWorldInfoEntries: async () =>
      renderCtx.entries.map((entry) => ({
        comment: entry.comment || entry.name,
        content: entry.content,
        world: entry.worldbook,
      })),
    getEnabledLoreBooks: () => [...new Set(renderCtx.entries.map((entry) => entry.worldbook))],
    activewi: async (world, entryOrForce, maybeForce) =>
      activateWorldInfoInContext(
        renderCtx,
        String(context.world_info?.world || ""),
        world,
        entryOrForce,
        maybeForce,
      ),
    activateWorldInfo: async (world, entryOrForce, maybeForce) =>
      activateWorldInfoInContext(
        renderCtx,
        String(context.world_info?.world || ""),
        world,
        entryOrForce,
        maybeForce,
      ),
    parseJSON: (raw) => {
      try {
        return JSON.parse(raw);
      } catch {
        return null;
      }
    },
    print: (...parts) =>
      parts.filter((part) => part !== undefined && part !== null).join(""),
    ...unsupported,
    ...extraEnv,
  };

  context.getchar = context.getchr;
  context.getChara = context.getchr;

  try {
    const compiled = runtime.compile(processed, {
      async: true,
      outputFunctionName: "print",
      _with: true,
      localsName: "locals",
      client: true,
    });
    const result = await compiled.call(
      context,
      context,
      (value) => value,
      () => ({ filename: "", template: "" }),
      rethrow,
    );
    return result ?? "";
  } catch (error) {
    if (renderCtx && typeof renderCtx === "object") {
      renderCtx.ejsLastError =
        error instanceof Error ? error.message : String(error);
    }
    if (error?.code === "st_bme_task_ejs_unsupported_helper") {
      throw error;
    }
    console.warn("[ST-BME] task-ejs 渲染失败:", error);
    throw error;
  }
}

export async function renderTaskEjsContent(content, templateContext = {}) {
  const hostSnapshot = resolveHostSnapshot(templateContext.hostSnapshot);
  const processed = substituteTaskEjsParams(content, templateContext, {
    hostSnapshot,
  });
  if (!processed.includes("<%")) {
    return processed;
  }

  const renderCtx = createTaskEjsRenderContext([], {
    templateContext,
    hostSnapshot,
  });
  return await evalTaskEjsTemplate(processed, renderCtx);
}

export async function checkTaskEjsSyntax(content) {
  const backend = await resolveTaskEjsBackend();
  const runtime = backend.runtime;
  if (!runtime || !String(content || "").includes("<%")) {
    return null;
  }

  try {
    runtime.compile(content, {
      async: true,
      client: true,
      _with: true,
      localsName: "locals",
    });
    return null;
  } catch (error) {
    return error instanceof Error ? error.message : String(error);
  }
}

export async function inspectTaskEjsRuntimeBackend(options = {}) {
  return await resolveTaskEjsBackend(options);
}
