// ST-BME: 任务正则兼容层（Phase 1）
// 目标：在任务预设中复用 Tavern 正则来源（global/preset/character），
// 同时叠加任务本地规则，并按任务阶段执行。

import { extension_settings, getContext } from "../../../../extensions.js";
import { debugDebug } from "../runtime/debug-logging.js";
import { getHostAdapter } from "../host/adapter/index.js";
import {
  getActiveTaskProfile,
  isTaskRegexStageEnabled,
  normalizeGlobalTaskRegex,
  normalizeTaskRegexStages,
} from "./prompt-profiles.js";

const HTML_TAG_PATTERN = /<\/?[a-z][\w:-]*\b/i;
const HTML_ATTR_PATTERN = /\b(?:style|class|id|href|src|data-)\s*=/i;
const TAVERN_REGEX_PLACEMENT = Object.freeze({
  USER_INPUT: 1,
  AI_OUTPUT: 2,
  SLASH_COMMAND: 3,
  WORLD_INFO: 5,
  REASONING: 6,
});
const TAVERN_REGEX_PLACEMENT_LABELS = Object.freeze({
  [TAVERN_REGEX_PLACEMENT.USER_INPUT]: "用户输入",
  [TAVERN_REGEX_PLACEMENT.AI_OUTPUT]: "AI 输出",
  [TAVERN_REGEX_PLACEMENT.SLASH_COMMAND]: "斜杠命令",
  [TAVERN_REGEX_PLACEMENT.WORLD_INFO]: "世界书",
  [TAVERN_REGEX_PLACEMENT.REASONING]: "推理/思维",
});

const PROMPT_STAGES = new Set([
  "finalPrompt",
  "input.userMessage",
  "input.recentMessages",
  "input.candidateText",
  "input.finalPrompt",
]);

const OUTPUT_STAGES = new Set([
  "rawResponse",
  "beforeParse",
  "output.rawResponse",
  "output.beforeParse",
]);

function isBeautificationReplace(text = "") {
  const normalized = String(text || "");
  return (
    HTML_TAG_PATTERN.test(normalized) || HTML_ATTR_PATTERN.test(normalized)
  );
}

function parseRegexFromString(regexStr = "") {
  const input = String(regexStr || "").trim();
  if (!input) return null;

  const slashFormat = input.match(/^\/([\s\S]+)\/([gimsuy]*)$/);
  if (slashFormat) {
    try {
      return new RegExp(slashFormat[1], slashFormat[2]);
    } catch {
      return null;
    }
  }

  try {
    return new RegExp(input, "g");
  } catch {
    return null;
  }
}

function normalizeTrimStrings(rawTrim) {
  if (Array.isArray(rawTrim)) {
    return rawTrim.map((item) => String(item || "")).filter(Boolean);
  }
  if (typeof rawTrim === "string") {
    return rawTrim
      .split("\n")
      .map((item) => item.trim())
      .filter(Boolean);
  }
  return [];
}

function normalizeRulePlacement(rawPlacement) {
  const placement = Array.isArray(rawPlacement) ? rawPlacement : [];
  return placement
    .map((item) => Number(item))
    .filter((item) => Number.isFinite(item));
}

function derivePlacementLabelsFromSourceFlags(sourceFlags = {}) {
  const labels = [];
  if (sourceFlags.user) {
    labels.push(TAVERN_REGEX_PLACEMENT_LABELS[TAVERN_REGEX_PLACEMENT.USER_INPUT]);
  }
  if (sourceFlags.assistant) {
    labels.push(TAVERN_REGEX_PLACEMENT_LABELS[TAVERN_REGEX_PLACEMENT.AI_OUTPUT]);
  }
  if (sourceFlags.worldInfo) {
    labels.push(TAVERN_REGEX_PLACEMENT_LABELS[TAVERN_REGEX_PLACEMENT.WORLD_INFO]);
  }
  if (sourceFlags.reasoning) {
    labels.push(TAVERN_REGEX_PLACEMENT_LABELS[TAVERN_REGEX_PLACEMENT.REASONING]);
  }
  if (
    labels.length === 0 &&
    sourceFlags.system &&
    !sourceFlags.assistant &&
    !sourceFlags.worldInfo &&
    !sourceFlags.reasoning
  ) {
    labels.push("系统/世界书");
  }
  return labels;
}

function isTavernRuleShape(raw = {}) {
  return (
    Array.isArray(raw?.placement) ||
    Object.prototype.hasOwnProperty.call(raw || {}, "promptOnly") ||
    Object.prototype.hasOwnProperty.call(raw || {}, "markdownOnly") ||
    Object.prototype.hasOwnProperty.call(raw || {}, "scriptName") ||
    Object.prototype.hasOwnProperty.call(raw || {}, "findRegex") ||
    Object.prototype.hasOwnProperty.call(raw || {}, "replaceString")
  );
}

function buildRuleSourceFlags(source, placement, isTavernRule) {
  if (source && typeof source === "object") {
    const user = Boolean(source.user_input);
    const assistant = Boolean(source.ai_output);
    const worldInfo = Boolean(source.world_info);
    const reasoning = Boolean(source.reasoning);
    return {
      user,
      assistant,
      worldInfo,
      reasoning,
      system: assistant || worldInfo || reasoning,
    };
  }

  if (isTavernRule && placement.length > 0) {
    const user = placement.includes(TAVERN_REGEX_PLACEMENT.USER_INPUT);
    const assistant = placement.includes(TAVERN_REGEX_PLACEMENT.AI_OUTPUT);
    const worldInfo = placement.includes(TAVERN_REGEX_PLACEMENT.WORLD_INFO);
    const reasoning = placement.includes(TAVERN_REGEX_PLACEMENT.REASONING);
    return {
      user,
      assistant,
      worldInfo,
      reasoning,
      system: assistant || worldInfo || reasoning,
    };
  }

  return {
    user: true,
    assistant: true,
    worldInfo: true,
    reasoning: true,
    system: true,
  };
}

function normalizeRule(raw = {}, fallbackSource = "local", index = 0) {
  const destination =
    raw?.destination && typeof raw.destination === "object"
      ? raw.destination
      : null;
  const source =
    raw?.source && typeof raw.source === "object" ? raw.source : null;
  const placement = normalizeRulePlacement(raw?.placement);
  const isTavernRule = isTavernRuleShape(raw);
  const replaceString = String(
    raw.replace_string ?? raw.replaceString ?? raw.replace ?? "",
  );
  const beautificationReplace = isBeautificationReplace(replaceString);
  const sourceFlags = buildRuleSourceFlags(source, placement, isTavernRule);

  return {
    id: String(raw.id || `${fallbackSource}-${index + 1}`),
    scriptName: String(raw.script_name || raw.scriptName || ""),
    enabled: raw.enabled !== false && raw.disabled !== true,
    findRegex: String(raw.find_regex || raw.findRegex || raw.find || "").trim(),
    replaceString,
    trimStrings: normalizeTrimStrings(raw.trim_strings ?? raw.trimStrings),
    sourceFlags,
    destinationFlags: {
      prompt: destination
        ? Boolean(destination.prompt)
        : raw.markdownOnly === true
          ? false
          : true,
      display: destination
        ? Boolean(destination.display)
        : Boolean(raw.markdownOnly),
    },
    beautificationReplace,
    promptOnly: Boolean(raw.promptOnly),
    markdownOnly: Boolean(raw.markdownOnly),
    placement,
    minDepth: Number.isFinite(Number(raw.min_depth ?? raw.minDepth))
      ? Number(raw.min_depth ?? raw.minDepth)
      : null,
    maxDepth: Number.isFinite(Number(raw.max_depth ?? raw.maxDepth))
      ? Number(raw.max_depth ?? raw.maxDepth)
      : null,
    isTavernRule,
    sourceType: fallbackSource,
    raw,
  };
}

function readArrayPath(root, paths = []) {
  for (const path of paths) {
    let current = root;
    let valid = true;
    for (const segment of path) {
      if (!current || typeof current !== "object") {
        valid = false;
        break;
      }
      current = current[segment];
    }
    if (valid && Array.isArray(current)) {
      return current;
    }
  }
  return [];
}

function getLegacyRegexApi(name) {
  const fn = globalThis?.[name];
  return typeof fn === "function" ? fn : null;
}

function getRegexHost() {
  const legacyGetTavernRegexes = getLegacyRegexApi("getTavernRegexes");
  const legacyIsCharacterTavernRegexesEnabled = getLegacyRegexApi(
    "isCharacterTavernRegexesEnabled",
  );
  const legacyFormatAsTavernRegexedString = getLegacyRegexApi(
    "formatAsTavernRegexedString",
  );

  try {
    const regexHost = getHostAdapter?.()?.regex || null;
    if (
      typeof regexHost?.getTavernRegexes === "function" ||
      typeof regexHost?.formatAsTavernRegexedString === "function"
    ) {
      const capabilitySupport = regexHost.readCapabilitySupport?.() || {};
      const supplementedCapabilities = [];
      const missingCapabilities = [];
      const resolvedGetter =
        typeof regexHost.getTavernRegexes === "function"
          ? regexHost.getTavernRegexes
          : legacyGetTavernRegexes;
      const resolvedCharacterToggle =
        typeof regexHost.isCharacterTavernRegexesEnabled === "function"
          ? regexHost.isCharacterTavernRegexesEnabled
          : legacyIsCharacterTavernRegexesEnabled;
      const resolvedFormatter =
        typeof regexHost.formatAsTavernRegexedString === "function"
          ? regexHost.formatAsTavernRegexedString
          : legacyFormatAsTavernRegexedString;

      if (typeof regexHost.getTavernRegexes !== "function") {
        if (resolvedGetter) {
          supplementedCapabilities.push("getTavernRegexes");
        } else {
          missingCapabilities.push("getTavernRegexes");
        }
      }

      if (typeof regexHost.isCharacterTavernRegexesEnabled !== "function") {
        if (resolvedCharacterToggle) {
          supplementedCapabilities.push("isCharacterTavernRegexesEnabled");
        } else {
          missingCapabilities.push("isCharacterTavernRegexesEnabled");
        }
      }

      if (typeof regexHost.formatAsTavernRegexedString !== "function") {
        if (resolvedFormatter) {
          supplementedCapabilities.push("formatAsTavernRegexedString");
        } else {
          missingCapabilities.push("formatAsTavernRegexedString");
        }
      }

      return {
        getTavernRegexes: resolvedGetter,
        isCharacterTavernRegexesEnabled: resolvedCharacterToggle,
        formatAsTavernRegexedString: resolvedFormatter,
        sourceLabel:
          capabilitySupport.sourceLabel || regexHost?.sourceLabel || "host-adapter.regex",
        fallback:
          Boolean(capabilitySupport.fallback) ||
          typeof regexHost.getTavernRegexes !== "function" ||
          typeof regexHost.isCharacterTavernRegexesEnabled !== "function" ||
          typeof regexHost.formatAsTavernRegexedString !== "function" ||
          supplementedCapabilities.length > 0,
        fallbackReason: String(
          regexHost?.fallbackReason || capabilitySupport.fallbackReason || "",
        ).trim(),
        capabilityStatus: Object.freeze({
          mode: capabilitySupport.mode || regexHost?.mode || "unknown",
          bridgeTier:
            capabilitySupport.bridgeTier || capabilitySupport.mode || regexHost?.mode || "unknown",
          supplementedCapabilities: Object.freeze(supplementedCapabilities),
          missingCapabilities: Object.freeze(missingCapabilities),
        }),
      };
    }
  } catch (error) {
    debugDebug(
      "[ST-BME] task-regex 读取 regex bridge 失败，回退到 legacy 宿主接口",
      error,
    );
  }

  const missingCapabilities = [];
  if (typeof legacyGetTavernRegexes !== "function") {
    missingCapabilities.push("getTavernRegexes");
  }
  if (typeof legacyIsCharacterTavernRegexesEnabled !== "function") {
    missingCapabilities.push("isCharacterTavernRegexesEnabled");
  }
  if (typeof legacyFormatAsTavernRegexedString !== "function") {
    missingCapabilities.push("formatAsTavernRegexedString");
  }

  return {
    getTavernRegexes: legacyGetTavernRegexes,
    isCharacterTavernRegexesEnabled: legacyIsCharacterTavernRegexesEnabled,
    formatAsTavernRegexedString: legacyFormatAsTavernRegexedString,
    sourceLabel: "legacy.globalThis",
    fallback: true,
    fallbackReason:
      typeof legacyGetTavernRegexes === "function"
        ? "当前通过 legacy globalThis 回退提供 Tavern Regex 能力"
        : "未检测到 Tavern Regex 宿主接口",
    capabilityStatus: Object.freeze({
      mode: "legacy",
      supplementedCapabilities: Object.freeze([]),
      missingCapabilities: Object.freeze(missingCapabilities),
    }),
  };
}

function getPresetManagerFromContext(context = {}) {
  if (typeof context?.getPresetManager !== "function") {
    return null;
  }

  try {
    const manager = context.getPresetManager();
    return manager && typeof manager === "object" ? manager : null;
  } catch {
    return null;
  }
}

function getCurrentPresetInfo(context = {}) {
  const presetManager = getPresetManagerFromContext(context);
  const apiId = String(presetManager?.apiId || "").trim();
  const presetName =
    typeof presetManager?.getSelectedPresetName === "function"
      ? String(presetManager.getSelectedPresetName() || "").trim()
      : "";

  return {
    presetManager,
    apiId,
    presetName,
  };
}

function isPresetRegexAllowed(extSettings = {}, apiId = "", presetName = "") {
  if (!apiId || !presetName) {
    return false;
  }
  return Boolean(extSettings?.preset_allowed_regex?.[apiId]?.includes?.(presetName));
}

function getCurrentCharacterInfo(context = {}) {
  const rawCharacterId = context?.characterId;
  const characterId = Number(rawCharacterId);
  if (!Number.isFinite(characterId) || characterId < 0) {
    return {
      characterId: null,
      character: null,
      avatar: "",
    };
  }

  const characters = Array.isArray(context?.characters) ? context.characters : [];
  const character = characters[characterId] || null;

  return {
    characterId,
    character,
    avatar: String(character?.avatar || ""),
  };
}

function isCharacterRegexAllowed(extSettings = {}, avatar = "") {
  if (!avatar) {
    return false;
  }
  return Boolean(extSettings?.character_allowed_regex?.includes?.(avatar));
}

function readGlobalFallbackRules(extSettings = {}) {
  return readArrayPath(extSettings, [
    ["regex"],
    ["regex_scripts"],
    ["regex", "regex_scripts"],
  ]);
}

function readPresetFallbackRules(context = {}, oaiSettings = {}) {
  const { presetManager } = getCurrentPresetInfo(context);
  if (typeof presetManager?.readPresetExtensionField === "function") {
    try {
      const scripts = presetManager.readPresetExtensionField({
        path: "regex_scripts",
      });
      if (Array.isArray(scripts)) {
        return scripts;
      }
    } catch {
      // ignore and continue to legacy paths
    }
  }

  return readArrayPath(oaiSettings, [
    ["regex_scripts"],
    ["extensions", "regex_scripts"],
  ]);
}

function readCharacterFallbackRules(context = {}) {
  const { character } = getCurrentCharacterInfo(context);
  if (!character) {
    return [];
  }

  return readArrayPath(character, [
    ["data", "extensions", "regex_scripts"],
    ["extensions", "regex_scripts"],
  ]);
}

function getPlacementLabels(placement = []) {
  return (Array.isArray(placement) ? placement : []).map(
    (item) => TAVERN_REGEX_PLACEMENT_LABELS[item] || `#${item}`,
  );
}

function summarizeRule(rule, reason = "") {
  const normalized = rule && typeof rule === "object" ? rule : {};
  const sourceFlags =
    normalized.sourceFlags && typeof normalized.sourceFlags === "object"
      ? normalized.sourceFlags
      : {};
  const placementLabels = getPlacementLabels(normalized.placement);
  const effectivePlacementLabels =
    placementLabels.length > 0
      ? placementLabels
      : derivePlacementLabelsFromSourceFlags(sourceFlags);
  return {
    id: String(normalized.id || ""),
    name: String(normalized.scriptName || normalized.id || ""),
    findRegex: String(normalized.findRegex || ""),
    replaceString: String(normalized.replaceString || ""),
    effectivePromptReplaceString: String(normalized.replaceString || ""),
    promptReplaceAsEmpty: false,
    sourceType: String(normalized.sourceType || ""),
    promptOnly: Boolean(normalized.promptOnly),
    markdownOnly: Boolean(normalized.markdownOnly),
    beautificationReplace: Boolean(normalized.beautificationReplace),
    sourceFlags: {
      user: sourceFlags.user !== false,
      assistant: sourceFlags.assistant !== false,
      worldInfo: sourceFlags.worldInfo !== false,
      reasoning: sourceFlags.reasoning !== false,
      system: sourceFlags.system !== false,
    },
    placement: Array.isArray(normalized.placement) ? [...normalized.placement] : [],
    placementLabels: effectivePlacementLabels,
    minDepth:
      normalized.minDepth == null ? null : Number(normalized.minDepth),
    maxDepth:
      normalized.maxDepth == null ? null : Number(normalized.maxDepth),
    reason: String(reason || ""),
  };
}

function summarizeRuleForPromptPreview(rule, stageConfig = {}, reason = "") {
  const summary = summarizeRule(rule, reason);
  const regexHost = getRegexHost();
  const executionState = buildHostRegexExecutionState(regexHost);
  const promptStageApplies =
    summary.sourceType === "local"
      ? shouldApplyRuleForStage(rule, "input.finalPrompt", stageConfig)
      : shouldReuseTavernRuleForPrompt(rule, executionState.mode);
  const promptSemanticApplies =
    summary.sourceType === "local"
      ? summary.sourceFlags.system !== false &&
        rule?.destinationFlags?.prompt !== false
      : shouldReuseTavernRuleForPrompt(rule, executionState.mode);
  let promptStageMode = "skip";
  if (summary.sourceType === "local") {
    promptStageMode = promptSemanticApplies ? "replace" : "skip";
  } else if (rule?.destinationFlags?.prompt === false || summary.markdownOnly) {
    promptStageMode = "display-only";
  } else if (
    summary.beautificationReplace &&
    !["host-real", "host-helper"].includes(executionState.mode)
  ) {
    promptStageMode = "fallback-skip-beautify";
  } else if (executionState.mode === "host-real") {
    promptStageMode = "host-real";
  } else if (executionState.mode === "host-helper") {
    promptStageMode = "host-helper";
  } else if (executionState.mode === "host-fallback") {
    promptStageMode = "host-fallback";
  }
  return {
    ...summary,
    promptSemanticApplies,
    promptStageApplies,
    promptStageEnabled: isTaskRegexStageEnabled(stageConfig, "input.finalPrompt"),
    promptStageMode,
    executionMode:
      summary.sourceType === "local" ? "local-final" : executionState.mode,
    formatterAvailable: executionState.formatterAvailable,
  };
}

function collectViaApi(sourceType, regexHost = null) {
  const getter = regexHost?.getTavernRegexes;
  if (typeof getter !== "function") {
    return { supported: false, items: [] };
  }

  const success = (items) => ({
    supported: true,
    items: Array.isArray(items) ? items : [],
  });

  const unsupported = () => ({ supported: false, items: [] });

  try {
    if (sourceType === "global") {
      return success(getter({ type: "global" }));
    }
    if (sourceType === "preset") {
      return success(getter({ type: "preset", name: "in_use" }));
    }
    if (sourceType === "character") {
      const checkEnabled = regexHost?.isCharacterTavernRegexesEnabled;
      if (
        typeof checkEnabled !== "function" &&
        regexHost?.capabilityStatus?.mode === "partial"
      ) {
        return unsupported();
      }
      if (typeof checkEnabled === "function" && !checkEnabled()) {
        return success([]);
      }
      return success(getter({ type: "character", name: "current" }));
    }
  } catch {
    return unsupported();
  }
  return unsupported();
}

function collectTavernRulesDetailed(regexConfig = {}) {
  const shouldReuse = regexConfig.inheritStRegex !== false;
  const sourceConfig = regexConfig.sources || {};
  const enabledSources = {
    global: shouldReuse && sourceConfig.global !== false,
    preset: shouldReuse && sourceConfig.preset !== false,
    character: shouldReuse && sourceConfig.character !== false,
  };

  const context = getContext?.() || {};
  const extSettings = context?.extensionSettings || extension_settings || {};
  const oaiSettings =
    context?.chatCompletionSettings || globalThis?.oai_settings || {};
  const regexHost = getRegexHost();
  const collected = [];
  const seen = new Set();
  const sources = [];

  const appendSourceSnapshot = ({
    type,
    label,
    enabled,
    supported,
    resolvedVia,
    allowed = true,
    reason = "",
    rawItems = [],
  }) => {
    const effectiveItems =
      enabled && allowed ? (Array.isArray(rawItems) ? rawItems : []) : [];
    const activeRules = [];
    const ignoredRules = [];
    const ignoredPreviewRules = [];
    const previewRules = [];

    if (!enabled) {
      sources.push({
        type,
        label,
        enabled,
        supported,
        resolvedVia,
        allowed,
        reason:
          reason || (shouldReuse ? "当前任务已关闭该来源" : "当前任务未启用复用酒馆正则"),
        rawRuleCount: Array.isArray(rawItems) ? rawItems.length : 0,
        activeRuleCount: 0,
        previewRules: Array.isArray(rawItems)
          ? rawItems.map((item, index) => normalizeRule(item, type, index))
          : [],
        ignoredPreviewRules: [],
        rules: [],
        ignoredRules: [],
      });
      return;
    }

    if (!allowed && Array.isArray(rawItems)) {
      for (let index = 0; index < rawItems.length; index++) {
        const normalized = normalizeRule(rawItems[index], type, index);
        previewRules.push(normalized);
        ignoredPreviewRules.push({ ...normalized, reason: "not-allowed" });
        ignoredRules.push(
          summarizeRule(normalized, "not-allowed"),
        );
      }
    }

    for (let index = 0; index < effectiveItems.length; index++) {
      const normalized = normalizeRule(effectiveItems[index], type, index);
      previewRules.push(normalized);
      if (!normalized.enabled) {
        ignoredPreviewRules.push({ ...normalized, reason: "disabled" });
        ignoredRules.push(summarizeRule(normalized, "disabled"));
        continue;
      }
      if (!normalized.findRegex) {
        ignoredPreviewRules.push({ ...normalized, reason: "missing-find-regex" });
        ignoredRules.push(summarizeRule(normalized, "missing-find-regex"));
        continue;
      }
      const key = `${type}:${normalized.id}:${normalized.findRegex}`;
      if (seen.has(key)) {
        ignoredPreviewRules.push({ ...normalized, reason: "duplicate" });
        ignoredRules.push(summarizeRule(normalized, "duplicate"));
        continue;
      }
      seen.add(key);
      collected.push(normalized);
      activeRules.push(summarizeRule(normalized));
    }

    sources.push({
      type,
      label,
      enabled,
      supported,
      resolvedVia,
      allowed,
      reason,
      rawRuleCount: Array.isArray(rawItems) ? rawItems.length : 0,
      activeRuleCount: activeRules.length,
      previewRules,
      ignoredPreviewRules,
      rules: activeRules,
      ignoredRules,
    });
  };

  const globalViaApi = collectViaApi("global", regexHost);
  appendSourceSnapshot({
    type: "global",
    label: "全局",
    enabled: enabledSources.global,
    supported: true,
    resolvedVia: globalViaApi.supported ? "bridge" : "fallback",
    rawItems: globalViaApi.supported
      ? globalViaApi.items
      : readGlobalFallbackRules(extSettings),
  });

  const presetViaApi = collectViaApi("preset", regexHost);
  if (presetViaApi.supported) {
    appendSourceSnapshot({
      type: "preset",
      label: "当前预设",
      enabled: enabledSources.preset,
      supported: true,
      resolvedVia: "bridge",
      rawItems: presetViaApi.items,
    });
  } else {
    const { apiId, presetName } = getCurrentPresetInfo(context);
    const rawItems = readPresetFallbackRules(context, oaiSettings);
    const allowed = isPresetRegexAllowed(extSettings, apiId, presetName);
    appendSourceSnapshot({
      type: "preset",
      label: "当前预设",
      enabled: enabledSources.preset,
      supported: true,
      resolvedVia: "fallback",
      allowed,
      reason: allowed
        ? ""
        : apiId && presetName
          ? `酒馆当前未允许预设 "${presetName}" 的正则参与运行`
          : "未识别到酒馆当前生效的预设",
      rawItems,
    });
  }

  const characterViaApi = collectViaApi("character", regexHost);
  if (characterViaApi.supported) {
    appendSourceSnapshot({
      type: "character",
      label: "角色卡",
      enabled: enabledSources.character,
      supported: true,
      resolvedVia: "bridge",
      rawItems: characterViaApi.items,
    });
  } else {
    const { avatar } = getCurrentCharacterInfo(context);
    const rawItems = readCharacterFallbackRules(context);
    const allowed = isCharacterRegexAllowed(extSettings, avatar);
    appendSourceSnapshot({
      type: "character",
      label: "角色卡",
      enabled: enabledSources.character,
      supported: true,
      resolvedVia: "fallback",
      allowed,
      reason: allowed
        ? ""
        : avatar
          ? "酒馆当前未允许该角色卡的 scoped regex 参与运行"
          : "当前没有可用的角色卡上下文",
      rawItems,
    });
  }

  return {
    shouldReuse,
    host: {
      sourceLabel: regexHost.sourceLabel,
      fallback: Boolean(regexHost.fallback),
      fallbackReason: String(regexHost.fallbackReason || ""),
      formatterAvailable:
        typeof regexHost.formatAsTavernRegexedString === "function",
      executionMode: buildHostRegexExecutionState(regexHost).mode,
      bridgeTier:
        regexHost?.capabilityStatus?.bridgeTier ||
        regexHost?.capabilityStatus?.mode ||
        "unknown",
      capabilityStatus: regexHost.capabilityStatus || null,
    },
    sources,
    rules: collected,
  };
}

function collectTavernRules(regexConfig = {}) {
  return collectTavernRulesDetailed(regexConfig).rules;
}

function collectLocalRules(regexConfig = {}) {
  const localRules = Array.isArray(regexConfig.localRules)
    ? regexConfig.localRules
    : [];
  return localRules
    .map((rule, index) => normalizeRule(rule, "local", index))
    .filter((rule) => rule.enabled && rule.findRegex);
}

function normalizeHostRegexSourceType(sourceType = "") {
  const normalized = String(sourceType || "").trim().toLowerCase();
  if (
    ["user_input", "ai_output", "world_info", "reasoning"].includes(normalized)
  ) {
    return normalized;
  }
  return "";
}

function normalizeHostFormatterOptions(formatterOptions = null) {
  const normalized =
    formatterOptions && typeof formatterOptions === "object"
      ? { ...formatterOptions }
      : {};
  if (normalized.isPrompt == null) {
    normalized.isPrompt = true;
  }
  if (normalized.isMarkdown == null) {
    normalized.isMarkdown = false;
  }
  if (!Number.isFinite(Number(normalized.depth))) {
    delete normalized.depth;
  } else {
    normalized.depth = Number(normalized.depth);
  }
  return normalized;
}

function ruleMatchesFormatterDepth(rule, formatterOptions = null) {
  const depth = Number(formatterOptions?.depth);
  if (!Number.isFinite(depth)) {
    return true;
  }
  if (
    rule?.minDepth != null &&
    Number.isFinite(Number(rule.minDepth)) &&
    Number(rule.minDepth) >= -1 &&
    depth < Number(rule.minDepth)
  ) {
    return false;
  }
  if (
    rule?.maxDepth != null &&
    Number.isFinite(Number(rule.maxDepth)) &&
    Number(rule.maxDepth) >= 0 &&
    depth > Number(rule.maxDepth)
  ) {
    return false;
  }
  return true;
}

function buildHostRegexExecutionState(regexHost = null) {
  const bridgeTier =
    String(
      regexHost?.capabilityStatus?.bridgeTier ||
        regexHost?.capabilityStatus?.mode ||
        "",
    ).trim() || "unknown";
  const formatterAvailable =
    typeof regexHost?.formatAsTavernRegexedString === "function";
  const rulesAvailable = typeof regexHost?.getTavernRegexes === "function";

  if (formatterAvailable && bridgeTier === "core-real") {
    return {
      mode: "host-real",
      bridgeTier,
      formatterAvailable: true,
      fallbackReason: "",
    };
  }

  if (formatterAvailable) {
    return {
      mode: "host-helper",
      bridgeTier,
      formatterAvailable: true,
      fallbackReason:
        String(regexHost?.fallbackReason || "").trim() ||
        "当前通过 helper bridge 提供 Tavern Regex formatter",
    };
  }

  if (rulesAvailable) {
    return {
      mode: "host-fallback",
      bridgeTier,
      formatterAvailable: false,
      fallbackReason:
        String(regexHost?.fallbackReason || "").trim() ||
        "宿主 formatter 不可用，已回退插件侧兼容执行",
    };
  }

  return {
    mode: "host-unavailable",
    bridgeTier,
    formatterAvailable: false,
    fallbackReason:
      String(regexHost?.fallbackReason || "").trim() ||
      "未检测到可用的 Tavern Regex 宿主接口",
  };
}

function shouldReuseTavernRuleForPrompt(rule, executionMode = "host-fallback") {
  if (!rule?.isTavernRule) {
    return false;
  }
  if (rule?.destinationFlags?.prompt === false) {
    return false;
  }
  if (rule?.markdownOnly) {
    return false;
  }
  if (
    !["host-real", "host-helper"].includes(executionMode) &&
    Boolean(rule?.beautificationReplace)
  ) {
    return false;
  }
  return true;
}

function shouldReuseTavernRuleForSourceType(rule, sourceType = "", role = "system") {
  const normalizedSourceType = normalizeHostRegexSourceType(sourceType);
  if (!normalizedSourceType || !rule?.sourceFlags) {
    return false;
  }

  if (normalizedSourceType === "user_input") {
    if (role === "mixed") {
      return rule.sourceFlags.user !== false || rule.sourceFlags.assistant !== false;
    }
    return rule.sourceFlags.user !== false;
  }
  if (normalizedSourceType === "ai_output") {
    if (role === "mixed") {
      return rule.sourceFlags.user !== false || rule.sourceFlags.assistant !== false;
    }
    if (role === "user") {
      return rule.sourceFlags.user !== false;
    }
    return rule.sourceFlags.assistant !== false;
  }
  if (normalizedSourceType === "world_info") {
    return rule.sourceFlags.worldInfo !== false;
  }
  if (normalizedSourceType === "reasoning") {
    return rule.sourceFlags.reasoning !== false;
  }
  return false;
}

function shouldApplyRuleForTaskContext(rule, stage = "") {
  if (!rule?.isTavernRule) {
    return true;
  }

  const normalizedStage = String(stage || "").trim();
  const isPromptStage = PROMPT_STAGES.has(normalizedStage);
  const isFinalPromptStage =
    normalizedStage === "finalPrompt" || normalizedStage === "input.finalPrompt";
  const isOutputStage = OUTPUT_STAGES.has(normalizedStage);

  if (rule.markdownOnly || rule.beautificationReplace) {
    return isPromptStage;
  }

  if (isFinalPromptStage) {
    return rule.promptOnly === true;
  }

  if (isOutputStage) {
    return rule.promptOnly !== true;
  }

  return rule.promptOnly !== true;
}

function shouldApplyRuleForStage(rule, stage = "", stagesConfig = {}) {
  const normalizedStage = String(stage || "").trim();
  if (rule.destinationFlags.prompt === false) {
    return false;
  }
  if (!shouldApplyRuleForTaskContext(rule, normalizedStage)) {
    return false;
  }

  if (!normalizedStage) {
    return isTaskRegexStageEnabled(stagesConfig, "input");
  }

  if (PROMPT_STAGES.has(normalizedStage) || OUTPUT_STAGES.has(normalizedStage)) {
    return isTaskRegexStageEnabled(stagesConfig, normalizedStage);
  }

  return isTaskRegexStageEnabled(stagesConfig, normalizedStage);
}

function shouldApplyRuleForRole(rule, role = "system") {
  if (role === "mixed") {
    return rule.sourceFlags.user !== false || rule.sourceFlags.assistant !== false;
  }
  if (role === "user") return rule.sourceFlags.user !== false;
  if (role === "assistant") return rule.sourceFlags.assistant !== false;
  return rule.sourceFlags.system !== false;
}

function applyOneRule(input, rule, stage = "") {
  const regex = parseRegexFromString(rule.findRegex);
  if (!regex) return { output: input, changed: false, error: "invalid_regex" };

  let output = input.replace(regex, rule.replaceString || "");
  if (rule.trimStrings.length > 0) {
    for (const trimText of rule.trimStrings) {
      if (!trimText) continue;
      output = output.split(trimText).join("");
    }
  }

  return { output, changed: output !== input, error: "" };
}

function pushDebug(collector, entry) {
  if (collector && Array.isArray(collector.entries)) {
    collector.entries.push(entry);
  }
}

function applyHostRegexReuseFallback(
  input,
  tavernRules = [],
  {
    sourceType = "",
    role = "system",
    formatterOptions = null,
  } = {},
) {
  let output = String(input || "");
  const appliedRules = [];
  const normalizedSourceType = normalizeHostRegexSourceType(sourceType);
  const normalizedFormatterOptions = normalizeHostFormatterOptions(formatterOptions);

  for (const rule of Array.isArray(tavernRules) ? tavernRules : []) {
    if (!shouldReuseTavernRuleForPrompt(rule, "host-fallback")) {
      continue;
    }
    if (!shouldReuseTavernRuleForSourceType(rule, normalizedSourceType, role)) {
      continue;
    }
    if (!ruleMatchesFormatterDepth(rule, normalizedFormatterOptions)) {
      continue;
    }

    const result = applyOneRule(output, rule, "");
    if (result.error) {
      appliedRules.push({
        id: rule.id,
        source: rule.sourceType,
        error: result.error,
      });
      continue;
    }
    if (result.changed) {
      appliedRules.push({
        id: rule.id,
        source: rule.sourceType,
      });
      output = result.output;
    }
  }

  return {
    output,
    appliedRules,
  };
}

function resolveTaskRegexConfig(settings = {}, taskType = "") {
  const hasGlobalRegex =
    settings?.globalTaskRegex &&
    typeof settings.globalTaskRegex === "object" &&
    !Array.isArray(settings.globalTaskRegex);

  if (hasGlobalRegex) {
    return {
      profile: null,
      regexConfig: normalizeGlobalTaskRegex(
        settings.globalTaskRegex || {},
        "global",
      ),
    };
  }

  const profile = getActiveTaskProfile(settings, taskType);
  return {
    profile,
    regexConfig: normalizeGlobalTaskRegex(profile?.regex || {}, taskType || "task"),
  };
}

export function applyHostRegexReuse(
  settings = {},
  taskType,
  text,
  {
    sourceType = "",
    role = "system",
    debugCollector = null,
    formatterOptions = null,
  } = {},
) {
  const input = typeof text === "string" ? text : "";
  const normalizedTaskType = String(taskType || "").trim();
  const normalizedSourceType = normalizeHostRegexSourceType(sourceType);
  const normalizedFormatterOptions = normalizeHostFormatterOptions(formatterOptions);
  const { regexConfig } = resolveTaskRegexConfig(settings, taskType);
  const regexHost = getRegexHost();
  const executionState = buildHostRegexExecutionState(regexHost);

  if (!regexConfig.enabled || regexConfig.inheritStRegex === false) {
    pushDebug(debugCollector, {
      kind: "host-reuse",
      taskType: normalizedTaskType,
      stage: `host:${normalizedSourceType || "unknown"}`,
      enabled: false,
      executionMode: executionState.mode,
      formatterAvailable: executionState.formatterAvailable,
      appliedRules: [],
      sourceCount: { tavern: 0, local: 0 },
      fallbackReason: executionState.fallbackReason,
      hostFormatterSource: String(regexHost?.sourceLabel || ""),
      skippedDisplayOnlyRuleCount: 0,
    });
    return {
      text: input,
      changed: false,
      executionMode: executionState.mode,
      formatterAvailable: executionState.formatterAvailable,
      formatterSource: String(regexHost?.sourceLabel || ""),
      fallbackReason: executionState.fallbackReason,
      skippedDisplayOnlyRuleCount: 0,
    };
  }

  const detailed = collectTavernRulesDetailed(regexConfig);
  const tavernRules = Array.isArray(detailed.rules) ? detailed.rules : [];
  const skippedDisplayOnlyRuleCount = tavernRules.filter(
    (rule) =>
      rule?.isTavernRule &&
      (!shouldReuseTavernRuleForPrompt(rule, executionState.mode) ||
        rule?.destinationFlags?.prompt === false ||
        rule?.markdownOnly === true),
  ).length;

  if (
    !normalizedSourceType ||
    (
      tavernRules.length === 0 &&
      !["host-real", "host-helper"].includes(executionState.mode)
    )
  ) {
    pushDebug(debugCollector, {
      kind: "host-reuse",
      taskType: normalizedTaskType,
      stage: `host:${normalizedSourceType || "unknown"}`,
      enabled: true,
      executionMode: executionState.mode,
      formatterAvailable: executionState.formatterAvailable,
      appliedRules: [],
      sourceCount: { tavern: tavernRules.length, local: 0 },
      fallbackReason: executionState.fallbackReason,
      hostFormatterSource: String(regexHost?.sourceLabel || ""),
      skippedDisplayOnlyRuleCount,
    });
    return {
      text: input,
      changed: false,
      executionMode: executionState.mode,
      formatterAvailable: executionState.formatterAvailable,
      formatterSource: String(regexHost?.sourceLabel || ""),
      fallbackReason: executionState.fallbackReason,
      skippedDisplayOnlyRuleCount,
    };
  }

  if (
    ["host-real", "host-helper"].includes(executionState.mode) &&
    typeof regexHost?.formatAsTavernRegexedString === "function"
  ) {
    try {
      const output = String(
        regexHost.formatAsTavernRegexedString(
          input,
          normalizedSourceType,
          "prompt",
          normalizedFormatterOptions,
        ) ?? input,
      );
      pushDebug(debugCollector, {
        kind: "host-reuse",
        taskType: normalizedTaskType,
        stage: `host:${normalizedSourceType}`,
        enabled: true,
        executionMode: executionState.mode,
        formatterAvailable: true,
        appliedRules: output !== input
          ? [{ id: "__host_formatter__", source: executionState.mode }]
          : [],
        sourceCount: { tavern: tavernRules.length, local: 0 },
        fallbackReason:
          executionState.mode === "host-real"
            ? ""
            : executionState.fallbackReason,
        hostFormatterSource: String(regexHost?.sourceLabel || ""),
        skippedDisplayOnlyRuleCount,
      });
      return {
        text: output,
        changed: output !== input,
        executionMode: executionState.mode,
        formatterAvailable: true,
        formatterSource: String(regexHost?.sourceLabel || ""),
        fallbackReason:
          executionState.mode === "host-real"
            ? ""
            : executionState.fallbackReason,
        skippedDisplayOnlyRuleCount,
      };
    } catch (error) {
      debugDebug("[ST-BME] 宿主 formatter 执行失败，回退插件兼容执行", error);
    }
  }

  const fallback = applyHostRegexReuseFallback(input, tavernRules, {
    sourceType: normalizedSourceType,
    role,
    formatterOptions: normalizedFormatterOptions,
  });
  const fallbackReason =
    executionState.mode === "host-unavailable"
      ? executionState.fallbackReason
      : executionState.fallbackReason ||
        "宿主 formatter 不可用，已回退插件侧兼容执行";
  pushDebug(debugCollector, {
    kind: "host-reuse",
    taskType: normalizedTaskType,
    stage: `host:${normalizedSourceType}`,
    enabled: true,
    executionMode: "host-fallback",
    formatterAvailable: false,
    appliedRules: fallback.appliedRules,
    sourceCount: { tavern: tavernRules.length, local: 0 },
    fallbackReason,
    hostFormatterSource: String(regexHost?.sourceLabel || ""),
    skippedDisplayOnlyRuleCount,
  });
  return {
    text: fallback.output,
    changed: fallback.output !== input,
    executionMode: "host-fallback",
    formatterAvailable: false,
    formatterSource: String(regexHost?.sourceLabel || ""),
    fallbackReason,
    skippedDisplayOnlyRuleCount,
  };
}

export function applyTaskRegex(
  settings = {},
  taskType,
  stage,
  text,
  debugCollector = null,
  role = "system",
) {
  const { regexConfig } = resolveTaskRegexConfig(settings, taskType);
  const input = typeof text === "string" ? text : "";

  if (!regexConfig.enabled) {
    pushDebug(debugCollector, {
      taskType,
      stage,
      enabled: false,
      appliedRules: [],
      sourceCount: { tavern: 0, local: 0 },
    });
    return input;
  }

  // 阶段检查已移到 shouldApplyRuleForStage 中，无需单独 gate
  const stagesConfig = normalizeTaskRegexStages(regexConfig?.stages || {});

  const localRules = collectLocalRules(regexConfig);
  const appliedRules = [];
  let output = input;

  for (const rule of localRules) {
    if (!shouldApplyRuleForStage(rule, stage, stagesConfig)) continue;
    if (!shouldApplyRuleForRole(rule, role)) continue;

    const result = applyOneRule(output, rule, stage);
    if (result.error) {
      appliedRules.push({
        id: rule.id,
        source: rule.sourceType,
        error: result.error,
      });
      continue;
    }
    if (result.changed) {
      appliedRules.push({
        id: rule.id,
        source: rule.sourceType,
      });
      output = result.output;
    }
  }

  pushDebug(debugCollector, {
    taskType,
    stage,
    enabled: true,
    appliedRules,
    sourceCount: {
      tavern: 0,
      local: localRules.length,
    },
  });

  return output;
}

export function inspectTaskRegexReuse(settings = {}, taskType = "") {
  const { profile, regexConfig } = resolveTaskRegexConfig(settings, taskType);
  const detailed = collectTavernRulesDetailed(regexConfig);
  const stageConfig = normalizeTaskRegexStages(regexConfig.stages || {});
  const localRules = collectLocalRules(regexConfig);

  const mapPreviewRules = (rules = []) =>
    (Array.isArray(rules) ? rules : []).map((rule) =>
      summarizeRuleForPromptPreview(rule, stageConfig, rule?.reason || ""),
    );

  return {
    taskType: String(taskType || ""),
    profileId: String(profile?.id || ""),
    profileName: String(profile?.name || ""),
    regexEnabled: regexConfig.enabled !== false,
    inheritStRegex: regexConfig.inheritStRegex !== false,
    stageConfig: normalizeTaskRegexStages(regexConfig.stages || {}),
    sourceConfig: {
      global: regexConfig.sources?.global !== false,
      preset: regexConfig.sources?.preset !== false,
      character: regexConfig.sources?.character !== false,
    },
    localRuleCount: Array.isArray(regexConfig.localRules)
      ? regexConfig.localRules.length
      : 0,
    localRules: mapPreviewRules(localRules),
    sources: detailed.sources.map((source) => ({
      ...source,
      previewRules: mapPreviewRules(source.previewRules),
      rules: mapPreviewRules(source.previewRules),
      ignoredRules: mapPreviewRules(source.ignoredPreviewRules),
    })),
    host: detailed.host,
    activeRuleCount: detailed.rules.length,
    activeRules: detailed.rules.map((rule) =>
      summarizeRuleForPromptPreview(rule, stageConfig),
    ),
  };
}



