const { getSetting } = require("./settings");

const badgeCatalogs = new Map();
const userProfiles = new Map();
const betterTtvCatalogs = new Map();
const CACHE_MS = 30 * 60 * 1000;
const USER_CACHE_MS = 6 * 60 * 60 * 1000;
const USER_FAILURE_CACHE_MS = 5 * 60 * 1000;
const EXTERNAL_FAILURE_CACHE_MS = 5 * 60 * 1000;

function fallbackBadges(badges = {}) {
  return Object.entries(badges).map(([label]) => ({ label, image: null }));
}

async function helixRequest(path) {
  const clientId = String(getSetting("twitch_client_id", "") || "").trim();
  const token = String(getSetting("twitch_bot_oauth", "") || "").trim().replace(/^oauth:/i, "");
  if (!clientId || !token) throw new Error("Twitch API credentials are unavailable.");
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 2500);
  try {
    const response = await fetch(`https://api.twitch.tv/helix/${path}`, {
      headers: { "Client-Id": clientId, Authorization: `Bearer ${token}` },
      signal: controller.signal
    });
    if (!response.ok) throw new Error(`Twitch badge request returned HTTP ${response.status}.`);
    return response.json();
  } finally {
    clearTimeout(timeout);
  }
}

function badgeRequest(path) {
  return helixRequest(`chat/badges${path}`);
}

function indexCatalog(payload) {
  const catalog = new Map();
  for (const set of payload?.data || []) {
    for (const version of set.versions || []) {
      catalog.set(`${set.set_id}/${version.id}`, {
        label: version.title || set.set_id,
        image: version.image_url_2x || version.image_url_1x || version.image_url_4x || null
      });
    }
  }
  return catalog;
}

async function loadCatalog(roomId) {
  const key = String(roomId || "global");
  const cached = badgeCatalogs.get(key);
  if (cached && cached.expiresAt > Date.now()) return cached.promise;
  const promise = Promise.allSettled([
    badgeRequest("/global"),
    roomId ? badgeRequest(`?broadcaster_id=${encodeURIComponent(roomId)}`) : Promise.resolve({ data: [] })
  ]).then((results) => {
    const catalog = new Map();
    for (const result of results) {
      if (result.status !== "fulfilled") continue;
      for (const [id, badge] of indexCatalog(result.value)) catalog.set(id, badge);
    }
    return catalog;
  });
  badgeCatalogs.set(key, { expiresAt: Date.now() + CACHE_MS, promise });
  return promise;
}

async function resolveTwitchBadges(roomId, badges = {}) {
  const entries = Object.entries(badges || {});
  if (!entries.length) return [];
  try {
    const catalog = await loadCatalog(roomId);
    return entries.map(([setId, version]) => catalog.get(`${setId}/${version}`) || { label: setId, image: null });
  } catch {
    return fallbackBadges(badges);
  }
}

async function resolveTwitchAvatar(userId, username) {
  const id = String(userId || "").trim();
  const login = String(username || "").trim().toLowerCase();
  if (!id && !login) return null;
  const key = id ? `id:${id}` : `login:${login}`;
  const cached = userProfiles.get(key);
  if (cached && cached.expiresAt > Date.now()) return cached.promise;
  const query = id ? `id=${encodeURIComponent(id)}` : `login=${encodeURIComponent(login)}`;
  const promise = helixRequest(`users?${query}`)
    .then((payload) => payload?.data?.[0]?.profile_image_url || null)
    .catch(() => null);
  const result = { expiresAt: Date.now() + USER_CACHE_MS, promise };
  userProfiles.set(key, result);
  promise.then((avatar) => {
    if (!avatar) result.expiresAt = Date.now() + USER_FAILURE_CACHE_MS;
  });
  return promise;
}

async function betterTtvRequest(path) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 2500);
  try {
    const response = await fetch(`https://api.betterttv.net/3/${path}`, {
      headers: { Accept: "application/json", "User-Agent": "Lumi-Bot" },
      redirect: "error",
      signal: controller.signal
    });
    if (response.status === 404) return null;
    if (!response.ok) throw new Error(`BetterTTV request returned HTTP ${response.status}.`);
    return response.json();
  } finally {
    clearTimeout(timeout);
  }
}

function indexBetterTtvCatalog(emotes = []) {
  const catalog = new Map();
  for (const emote of Array.isArray(emotes) ? emotes : []) {
    const id = String(emote?.id || "").trim();
    const code = String(emote?.code || "").trim();
    if (!id || !code || /\s/u.test(code)) continue;
    catalog.set(code, {
      label: code,
      image: `https://cdn.betterttv.net/emote/${encodeURIComponent(id)}/3x`
    });
  }
  return catalog;
}

function mergeBetterTtvCatalog(target, emotes) {
  for (const [code, emote] of indexBetterTtvCatalog(emotes)) target.set(code, emote);
  return target;
}

async function loadBetterTtvCatalog(roomId) {
  if (!roomId) return loadGlobalBetterTtvCatalog();
  const key = `room:${String(roomId)}`;
  const cached = betterTtvCatalogs.get(key);
  if (cached && cached.expiresAt > Date.now()) return cached.promise;
  const promise = Promise.allSettled([
    loadGlobalBetterTtvCatalog(),
    betterTtvRequest(`cached/users/twitch/${encodeURIComponent(roomId)}`)
  ]).then((results) => {
    const catalog = results[0].status === "fulfilled" ? new Map(results[0].value) : new Map();
    const channelPayload = results[1].status === "fulfilled" ? results[1].value : null;
    mergeBetterTtvCatalog(catalog, channelPayload?.channelEmotes);
    mergeBetterTtvCatalog(catalog, channelPayload?.sharedEmotes);
    return catalog;
  });
  const result = { expiresAt: Date.now() + CACHE_MS, promise };
  betterTtvCatalogs.set(key, result);
  promise.then((catalog) => {
    if (!catalog.size) result.expiresAt = Date.now() + EXTERNAL_FAILURE_CACHE_MS;
  });
  return promise;
}

async function loadGlobalBetterTtvCatalog() {
  const key = "global-emotes";
  const cached = betterTtvCatalogs.get(key);
  if (cached && cached.expiresAt > Date.now()) return cached.promise;
  const promise = betterTtvRequest("cached/emotes/global")
    .then((payload) => indexBetterTtvCatalog(payload))
    .catch(() => new Map());
  const result = { expiresAt: Date.now() + CACHE_MS, promise };
  betterTtvCatalogs.set(key, result);
  promise.then((catalog) => {
    if (!catalog.size) result.expiresAt = Date.now() + EXTERNAL_FAILURE_CACHE_MS;
  });
  return promise;
}

function findBetterTtvEmotes(message, catalog) {
  const text = String(message || "");
  if (!text || !(catalog instanceof Map) || !catalog.size) return [];
  const matches = [];
  for (const token of text.matchAll(/\S+/gu)) {
    const emote = catalog.get(token[0]);
    if (!emote) continue;
    matches.push({
      start: token.index,
      end: token.index + token[0].length - 1,
      image: emote.image,
      label: emote.label
    });
  }
  return matches;
}

async function resolveBetterTtvEmotes(roomId, message) {
  try {
    return findBetterTtvEmotes(message, await loadBetterTtvCatalog(roomId));
  } catch {
    return [];
  }
}

module.exports = {
  findBetterTtvEmotes,
  indexBetterTtvCatalog,
  resolveBetterTtvEmotes,
  resolveTwitchAvatar,
  resolveTwitchBadges
};
