// ST-BME: old-message hide engine
// Uses the host's native /hide and /unhide slash commands instead of
// mutating chat messages into is_system=true.

const hideState = {
  managedChatRef: null,
  managedChatKey: null,
  managedSystemIndices: new Set(),
  hiddenRangeEnd: -1,
  lastProcessedLength: 0,
  scheduledTimer: null,
  operationVersion: 0,
};

const BME_HIDE_HASH_MARKER = "__st_bme_hide_managed";

function getTimerApi(runtime = {}) {
  const rawSetTimeout =
    typeof runtime.setTimeout === "function"
      ? runtime.setTimeout
      : globalThis.setTimeout;
  const rawClearTimeout =
    typeof runtime.clearTimeout === "function"
      ? runtime.clearTimeout
      : globalThis.clearTimeout;

  return {
    setTimeout(...args) {
      return Reflect.apply(rawSetTimeout, globalThis, args);
    },
    clearTimeout(...args) {
      return Reflect.apply(rawClearTimeout, globalThis, args);
    },
  };
}

function notifyRuntimeHideStateChanged(runtime = {}) {
  try {
    runtime.refreshPanelLiveState?.();
  } catch {
  }
}

function getCurrentContext(runtime = {}) {
  try {
    return typeof runtime.getContext === "function" ? runtime.getContext() : null;
  } catch {
    return null;
  }
}

function getCurrentChatInfo(runtime = {}) {
  const context = getCurrentContext(runtime);
  return {
    chat: Array.isArray(context?.chat) ? context.chat : null,
    chatId:
      context?.chatId != null && context.chatId !== ""
        ? String(context.chatId)
        : null,
  };
}

function getCurrentChatKey(runtime = {}) {
  const { chat, chatId } = getCurrentChatInfo(runtime);
  if (chatId) return chatId;
  return Array.isArray(chat) ? chat : null;
}

function getSlashExecutor(runtime = {}) {
  if (typeof runtime.executeSlashCommands === "function") {
    return runtime.executeSlashCommands.bind(runtime);
  }

  const context = getCurrentContext(runtime);
  if (typeof context?.executeSlashCommands === "function") {
    return context.executeSlashCommands.bind(context);
  }

  if (typeof globalThis.executeSlashCommands === "function") {
    return globalThis.executeSlashCommands.bind(globalThis);
  }

  if (typeof globalThis.executeSlashCommandsOnChatInput === "function") {
    return globalThis.executeSlashCommandsOnChatInput.bind(globalThis);
  }

  return null;
}

async function executeSlashCommand(command, runtime = {}) {
  const executor = getSlashExecutor(runtime);
  if (!executor) {
    throw new Error("executeSlashCommands is not available");
  }

  return await executor(command, true);
}

function normalizeHideSettings(settings = {}) {
  return {
    enabled: Boolean(settings.enabled),
    hideLastN: Math.max(
      0,
      Math.trunc(
        Number(
          settings.hideLastN ??
            settings.hide_last_n ??
            settings.keepLastN ??
            settings.keep_last_n ??
            0,
        ) || 0,
      ),
    ),
  };
}

function getJquery(runtime = {}) {
  if (typeof runtime.$ === "function") return runtime.$;
  if (typeof globalThis.$ === "function") return globalThis.$;
  return null;
}

function syncSystemAttribute(chat, indices = [], value = "true", runtime = {}) {
  if (!Array.isArray(chat) || !Array.isArray(indices) || indices.length === 0) {
    return;
  }

  const currentChat = getCurrentChatInfo(runtime).chat;
  if (currentChat !== chat) return;

  const jq = getJquery(runtime);
  if (!jq) return;

  const selector = indices.map((index) => `.mes[mesid="${index}"]`).join(",");
  if (!selector) return;
  jq(selector).attr("is_system", value);
}

function calcHideRange(chatLength, hideLastN) {
  if (!Number.isFinite(chatLength) || chatLength <= 0 || hideLastN <= 0) {
    return null;
  }

  const visibleStart =
    hideLastN >= chatLength ? 0 : Math.max(0, chatLength - hideLastN);
  const hideEnd = visibleStart - 1;
  if (hideEnd < 0) return null;

  return {
    start: 0,
    end: hideEnd,
  };
}

function beginOperation() {
  hideState.operationVersion += 1;
  return hideState.operationVersion;
}

function isOperationCurrent(version) {
  return version === hideState.operationVersion;
}

function clearScheduledTimer(runtime = {}) {
  const timers = getTimerApi(runtime);
  if (hideState.scheduledTimer) {
    timers.clearTimeout(hideState.scheduledTimer);
    hideState.scheduledTimer = null;
  }
}

function clearManagedState() {
  hideState.managedChatRef = null;
  hideState.managedChatKey = null;
  hideState.managedSystemIndices.clear();
  hideState.hiddenRangeEnd = -1;
  hideState.lastProcessedLength = 0;
}

function isManagedSystemMessage(message) {
  return Boolean(
    message?.is_system === true &&
      message?.extra &&
      typeof message.extra === "object" &&
      message.extra[BME_HIDE_HASH_MARKER] === true,
  );
}

function collectManagedSystemIndices(chat) {
  if (!Array.isArray(chat) || chat.length === 0) return [];
  const indices = [];
  for (let index = 0; index < chat.length; index++) {
    if (isManagedSystemMessage(chat[index])) {
      indices.push(index);
    }
  }
  return indices;
}

function hydrateManagedStateFromChat(
  chat,
  chatKey = getCurrentChatKey(),
  { bootstrapLength = false } = {},
) {
  if (!Array.isArray(chat)) {
    hideState.managedSystemIndices.clear();
    hideState.hiddenRangeEnd = -1;
    return { managedCount: 0, hiddenRangeEnd: -1 };
  }

  const managedIndices = collectManagedSystemIndices(chat);
  hideState.managedSystemIndices.clear();
  for (const index of managedIndices) {
    hideState.managedSystemIndices.add(index);
  }

  hideState.managedChatRef = chat;
  hideState.managedChatKey = chatKey;
  hideState.hiddenRangeEnd =
    managedIndices.length > 0 ? managedIndices[managedIndices.length - 1] : -1;
  if (managedIndices.length > 0 && bootstrapLength) {
    hideState.lastProcessedLength = chat.length;
  }

  return {
    managedCount: managedIndices.length,
    hiddenRangeEnd: hideState.hiddenRangeEnd,
  };
}

function restoreManagedSystemFlags(chat, runtime = {}) {
  if (!Array.isArray(chat)) {
    hideState.managedSystemIndices.clear();
    return 0;
  }

  if (hideState.managedSystemIndices.size === 0) {
    hydrateManagedStateFromChat(chat, getCurrentChatKey(runtime), {
      bootstrapLength: false,
    });
  }
  if (hideState.managedSystemIndices.size === 0) {
    return 0;
  }

  const restored = [];
  for (const index of hideState.managedSystemIndices) {
    const message = chat[index];
    if (!message || message.is_system !== true) continue;
    message.is_system = false;
    if (message.extra && typeof message.extra === "object") {
      delete message.extra[BME_HIDE_HASH_MARKER];
      if (Object.keys(message.extra).length === 0) {
        delete message.extra;
      }
    }
    restored.push(index);
  }

  syncSystemAttribute(chat, restored, "false", runtime);
  hideState.managedSystemIndices.clear();
  return restored.length;
}

function markManagedSystemRange(chat, start, end, runtime = {}) {
  if (!Array.isArray(chat) || start > end) return 0;

  const marked = [];
  for (let index = start; index <= end && index < chat.length; index++) {
    const message = chat[index];
    if (!message || message.is_system === true) continue;
    message.is_system = true;
    const extra =
      message.extra && typeof message.extra === "object" ? message.extra : {};
    extra[BME_HIDE_HASH_MARKER] = true;
    message.extra = extra;
    hideState.managedSystemIndices.add(index);
    marked.push(index);
  }

  syncSystemAttribute(chat, marked, "true", runtime);
  return marked.length;
}

function adoptManagedChat(chat, chatKey, runtime = {}) {
  const previousChat = hideState.managedChatRef;
  if (previousChat && previousChat !== chat) {
    restoreManagedSystemFlags(previousChat, runtime);
    hideState.hiddenRangeEnd = -1;
    hideState.lastProcessedLength = 0;
  }

  hideState.managedChatRef = chat;
  hideState.managedChatKey = chatKey;
}

function buildResult({
  active = false,
  hiddenCount = 0,
  shownCount = 0,
  chatLength = 0,
  incremental = false,
  stale = false,
} = {}) {
  return {
    active,
    hiddenCount,
    shownCount,
    managedCount: active ? hiddenCount : 0,
    chatLength,
    incremental,
    stale,
  };
}

async function unhideCurrentRange(runtime = {}, version = null, options = {}) {
  const { chat } = getCurrentChatInfo(runtime);
  const chatLength = Array.isArray(chat) ? chat.length : 0;
  const full = Boolean(options.full);
  const previousEnd = full
    ? Math.max(-1, chatLength - 1)
    : Math.min(hideState.hiddenRangeEnd, chatLength - 1);
  if (previousEnd < 0) {
    return { shownCount: 0, chatLength };
  }

  await executeSlashCommand(`/unhide 0-${previousEnd}`, runtime);
  if (!isOperationCurrent(version ?? hideState.operationVersion)) {
    return { shownCount: 0, chatLength, stale: true };
  }

  return { shownCount: previousEnd + 1, chatLength };
}

async function runHideApply(settings = {}, runtime = {}, options = {}) {
  const normalized = normalizeHideSettings(settings);
  const { incrementalPreferred = false, version = beginOperation() } = options;
  const chatInfo = getCurrentChatInfo(runtime);
  const chat = chatInfo.chat;
  const chatLength = Array.isArray(chat) ? chat.length : 0;

  if (!chat || chatLength === 0) {
    clearManagedState();
    return buildResult();
  }

  const chatKey = getCurrentChatKey(runtime);
  const previousChatKey = hideState.managedChatKey;
  const hadTrackedState =
    hideState.managedSystemIndices.size > 0 ||
    hideState.hiddenRangeEnd >= 0 ||
    (Number.isFinite(hideState.lastProcessedLength) &&
      hideState.lastProcessedLength > 0);
  adoptManagedChat(chat, chatKey, runtime);
  hydrateManagedStateFromChat(chat, chatKey, {
    bootstrapLength: !hadTrackedState,
  });
  const sameChat =
    previousChatKey !== null && chatKey !== null && previousChatKey === chatKey;
  const previousHiddenEnd = hideState.hiddenRangeEnd;
  const previousLength =
    sameChat && Number.isFinite(hideState.lastProcessedLength)
      ? hideState.lastProcessedLength
      : 0;
  hideState.lastProcessedLength = chatLength;

  if (!normalized.enabled || normalized.hideLastN <= 0) {
    if (previousHiddenEnd >= 0) {
      const { shownCount } = await unhideCurrentRange(runtime, version);
      if (!isOperationCurrent(version)) {
        return buildResult({ chatLength, shownCount, stale: true });
      }
      restoreManagedSystemFlags(chat, runtime);
      hideState.hiddenRangeEnd = -1;
      return buildResult({ chatLength, shownCount });
    }

    restoreManagedSystemFlags(chat, runtime);
    hideState.hiddenRangeEnd = -1;
    return buildResult({ chatLength });
  }

  const nextRange = calcHideRange(chatLength, normalized.hideLastN);
  if (!nextRange) {
    if (previousHiddenEnd >= 0) {
      const { shownCount } = await unhideCurrentRange(runtime, version);
      if (!isOperationCurrent(version)) {
        return buildResult({ chatLength, shownCount, stale: true });
      }
      restoreManagedSystemFlags(chat, runtime);
      hideState.hiddenRangeEnd = -1;
      return buildResult({ chatLength, shownCount });
    }

    restoreManagedSystemFlags(chat, runtime);
    hideState.hiddenRangeEnd = -1;
    return buildResult({
      active: true,
      hiddenCount: 0,
      chatLength,
    });
  }

  if (
    incrementalPreferred &&
    sameChat &&
    previousHiddenEnd >= 0 &&
    chatLength > previousLength &&
    previousLength > 0
  ) {
    const previousRange = calcHideRange(previousLength, normalized.hideLastN);
    const canExtendOnly =
      previousRange &&
      previousRange.end === previousHiddenEnd &&
      nextRange.end >= previousHiddenEnd;
    if (canExtendOnly && nextRange.end > previousHiddenEnd) {
      const start = previousHiddenEnd + 1;
      const end = nextRange.end;
      await executeSlashCommand(`/hide ${start}-${end}`, runtime);
      if (!isOperationCurrent(version)) {
        return buildResult({ chatLength, stale: true });
      }

      markManagedSystemRange(chat, start, end, runtime);
      hideState.hiddenRangeEnd = end;
      return buildResult({
        active: true,
        hiddenCount: end + 1,
        shownCount: 0,
        chatLength,
        incremental: true,
      });
    }
  }

  let shownCount = 0;
  if (previousHiddenEnd >= 0) {
    const unhideResult = await unhideCurrentRange(runtime, version);
    if (!isOperationCurrent(version)) {
      return buildResult({
        chatLength,
        shownCount: unhideResult.shownCount ?? 0,
        stale: true,
      });
    }
    shownCount = unhideResult.shownCount ?? 0;
  }
  restoreManagedSystemFlags(chat, runtime);

  await executeSlashCommand(`/hide ${nextRange.start}-${nextRange.end}`, runtime);
  if (!isOperationCurrent(version)) {
    return buildResult({ chatLength, shownCount, stale: true });
  }

  markManagedSystemRange(chat, nextRange.start, nextRange.end, runtime);
  hideState.hiddenRangeEnd = nextRange.end;
  hideState.lastProcessedLength = chatLength;

  return buildResult({
    active: true,
    hiddenCount: nextRange.end + 1,
    shownCount,
    chatLength,
    incremental: false,
  });
}

export async function runFullHideCheck(settings = {}, runtime = {}) {
  return await runHideApply(settings, runtime, {
    incrementalPreferred: false,
    version: beginOperation(),
  });
}

export async function runIncrementalHideCheck(settings = {}, runtime = {}) {
  return await runHideApply(settings, runtime, {
    incrementalPreferred: true,
    version: beginOperation(),
  });
}

export async function applyHideSettings(settings = {}, runtime = {}) {
  return await runFullHideCheck(settings, runtime);
}

export function scheduleHideSettingsApply(
  settings = {},
  runtime = {},
  delayMs = 120,
) {
  clearScheduledTimer(runtime);

  const timers = getTimerApi(runtime);
  const snapshot = normalizeHideSettings(settings);
  hideState.scheduledTimer = timers.setTimeout(() => {
    hideState.scheduledTimer = null;
    void applyHideSettings(snapshot, runtime)
      .then(() => {
        notifyRuntimeHideStateChanged(runtime);
      })
      .catch((error) => {
        console.warn?.("[ST-BME] scheduled hide apply failed", error);
      });
  }, Math.max(0, Math.trunc(Number(delayMs) || 0)));
  notifyRuntimeHideStateChanged(runtime);
}

export async function unhideAll(runtime = {}) {
  clearScheduledTimer(runtime);
  const version = beginOperation();
  const chatInfo = getCurrentChatInfo(runtime);
  const chatLength = Array.isArray(chatInfo.chat) ? chatInfo.chat.length : 0;

  if (chatLength === 0) {
    hideState.lastProcessedLength = chatLength;
    hideState.hiddenRangeEnd = -1;
    hideState.managedChatKey = getCurrentChatKey(runtime);
    return buildResult({ chatLength });
  }

  hydrateManagedStateFromChat(chatInfo.chat, getCurrentChatKey(runtime), {
    bootstrapLength: false,
  });
  const { shownCount } = await unhideCurrentRange(runtime, version, {
    full: true,
  });
  if (!isOperationCurrent(version)) {
    return buildResult({ chatLength, shownCount, stale: true });
  }

  restoreManagedSystemFlags(chatInfo.chat, runtime);
  hideState.hiddenRangeEnd = -1;
  hideState.lastProcessedLength = chatLength;
  hideState.managedChatRef = chatInfo.chat;
  hideState.managedChatKey = getCurrentChatKey(runtime);

  return buildResult({ chatLength, shownCount });
}

export function resetHideState(runtime = {}) {
  clearScheduledTimer(runtime);
  beginOperation();
  const chatInfo = getCurrentChatInfo(runtime);
  if (Array.isArray(chatInfo.chat)) {
    hydrateManagedStateFromChat(chatInfo.chat, chatInfo.chatId || null, {
      bootstrapLength: false,
    });
  }
  restoreManagedSystemFlags(hideState.managedChatRef, runtime);
  clearManagedState();
}

export function getHideStateSnapshot() {
  return {
    hasManagedChat: hideState.managedChatRef !== null,
    managedHiddenCount: hideState.hiddenRangeEnd >= 0 ? hideState.hiddenRangeEnd + 1 : 0,
    lastProcessedLength: hideState.lastProcessedLength,
    scheduled: Boolean(hideState.scheduledTimer),
  };
}

export function isInManagedHideRange(index, chat = null) {
  if (!Number.isFinite(index) || index < 0) return false;
  if (!hideState.managedChatRef) return false;
  if (Array.isArray(chat) && chat !== hideState.managedChatRef) return false;

  return hideState.managedSystemIndices.has(index);
}
