// Service worker: dedups boost requests coming from the content script,
// fires desktop notification + sound + badge for new ones, stores them.
"use strict";

importScripts("pricing.js"); // EldoradoPricing (for price in notifications)

const MAX_STORED = 200;
const NOTIF_PREFIX = "boost:";

// Only these categories have a confirmed offer body (Rank Boost, Placements).
// Net Wins (7) needs a win-count quantity and Custom (4) has no price → excluded.
const OFFERABLE_CATEGORIES = ["5", "6"];

const DEFAULT_OFFER = {
  autoEnabled: false,
  deliveryTime: "Day28",
  maxPerHour: 30,
  minGapSec: 6,
  priceCap: 200, // auto skips offers above this; manual still allowed
  categories: ["5", "6"], // which categories auto-offers
  regions: [], // empty = all; else region codes/names (e.g. ["EUW","Turkey"])
};
const DEFAULT_MESSAGE = {
  autoStartConversation: false,
  sendOpener: false, // text sending not wired yet (needs TalkJS outbound capture)
  template:
    "Hi {buyer}! I can do your {currentRank}→{targetRank} ({region}) boost. I just sent an offer — fast & safe.",
};
// Phone alerts via Telegram Bot API (one-way; the extension POSTs directly).
const DEFAULT_TELEGRAM = { enabled: false, token: "", chatId: "" };

// Licensing (30-day subscription) — validated against the Worker.
const WORKER_URL = "https://boostwatcher-relay.boostwatcher.workers.dev";
const LICENSE_GRACE_MS = 3 * 86400000; // offline tolerance after a successful server check
const DEFAULT_LICENSE = { key: "", status: "none", expiresAt: 0, lastCheck: 0, deviceId: "" };

// Each game has its OWN alerts + filters + pricing. Poll interval and the Phase 2
// automation (master/offer/message) stay GLOBAL.
const GAME_DEFAULTS = () => ({
  notifyDesktop: true, notifySound: true, notifyBadge: true, soundVolume: 0.7,
  minPrice: 0, filterTypes: [], filterRegions: [], pricing: {},
});

function normalizeSettings(s) {
  s = s || {};
  const global = (x) => ({
    pollInterval: x.pollInterval || 8000,
    master: !!x.master,
    offer: Object.assign({}, DEFAULT_OFFER, x.offer || {}),
    message: Object.assign({}, DEFAULT_MESSAGE, x.message || {}),
    telegram: Object.assign({}, DEFAULT_TELEGRAM, x.telegram || {}),
    license: Object.assign({}, DEFAULT_LICENSE, x.license || {}),
  });
  if (s.games && s.games.lol) {
    return Object.assign(global(s), {
      games: { lol: Object.assign(GAME_DEFAULTS(), s.games.lol), valorant: Object.assign(GAME_DEFAULTS(), s.games.valorant) },
    });
  }
  // migrate old flat settings → per-game (alerts/pricing per game; automation global)
  const mk = () => ({
    notifyDesktop: s.notifyDesktop !== false, notifySound: s.notifySound !== false, notifyBadge: s.notifyBadge !== false,
    soundVolume: s.soundVolume != null ? s.soundVolume : 0.7, minPrice: s.minPrice || 0,
    filterTypes: s.filterTypes || [], filterRegions: s.filterRegions || [], pricing: s.pricing || {},
  });
  return Object.assign(global(s), { games: { lol: mk(), valorant: mk() } });
}

function gameSettings(s, game) {
  const g = (s.games && s.games[game]) || (s.games && s.games.lol) || {};
  return Object.assign(GAME_DEFAULTS(), g);
}

// ---- storage helpers ----
function getLocal(keys) {
  return new Promise((r) => chrome.storage.local.get(keys, r));
}
function setLocal(obj) {
  return new Promise((r) => chrome.storage.local.set(obj, r));
}
// settings WITHOUT the license gate (used when we read/write the license itself,
// so reading never persists a master=false side effect).
async function getRawSettings() {
  const { settings } = await getLocal("settings");
  return normalizeSettings(settings);
}
async function getSettings() {
  const s = await getRawSettings();
  if (!licenseAllowsMaster(s.license)) s.master = false; // expired/invalid = nothing is sent
  return s;
}

// Cached-license check — allows the master switch, and works OFFLINE until expiry.
function licenseAllowsMaster(lic) {
  lic = lic || {};
  if (lic.status === "revoked") return false;
  if (lic.status === "none" || !lic.key) return false;
  const now = Date.now();
  if (!lic.expiresAt || now >= lic.expiresAt) return false;        // expired
  if (lic.lastCheck && now < lic.lastCheck) return false;          // clock moved backward
  if (lic.lastCheck && now - lic.lastCheck > LICENSE_GRACE_MS && lic.status !== "active") return false;
  return true;
}

function randHexBg(n) {
  const b = new Uint8Array(n);
  crypto.getRandomValues(b);
  return [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
}
// One stable random id per install; persisted in settings.license.deviceId.
async function getDeviceId() {
  const s = await getRawSettings();
  if (s.license.deviceId) return s.license.deviceId;
  const id = "dev-" + randHexBg(16);
  s.license = Object.assign({}, s.license, { deviceId: id });
  await setLocal({ settings: s });
  return id;
}

// Validate a key against the Worker; persist {status, expiresAt, lastCheck}. Returns {valid,status,expiresAt}.
async function validateLicenseKey(key) {
  const k = (key || "").trim().toUpperCase();
  if (!k) return { valid: false, status: "none" };
  const deviceId = await getDeviceId();
  let resp = null;
  try {
    const r = await fetch(WORKER_URL + "/license/check", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ key: k, device_id: deviceId }),
    });
    resp = await r.json().catch(() => null);
  } catch (e) { resp = null; }

  const s = await getRawSettings();
  if (!resp) {
    // network failure: leave prior expiry/lastCheck untouched (don't extend offline grace)
    return { valid: false, status: "offline", expiresAt: s.license.expiresAt };
  }
  const expiresAt = resp.expires_at ? new Date(resp.expires_at).getTime() : 0;
  s.license = Object.assign({}, s.license, {
    key: k, status: resp.status || "unknown", expiresAt, lastCheck: Date.now(), deviceId,
  });
  await setLocal({ settings: s });
  return { valid: !!resp.valid, status: resp.status, expiresAt };
}

// ---- Telegram (one-way phone alerts; posts directly to the Bot API) ----
// Returns null on success, or an error string (used by the Test button).
async function notifyTelegram(text, sOpt) {
  const t = ((sOpt || (await getSettings())).telegram) || {};
  if (!t.enabled || !t.token || !t.chatId) return "not configured";
  try {
    const res = await fetch("https://api.telegram.org/bot" + t.token + "/sendMessage", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ chat_id: t.chatId, text, parse_mode: "HTML", disable_web_page_preview: true }),
    });
    if (res.ok) return null;
    let why = "http " + res.status;
    try { const j = await res.json(); if (j && j.description) why = j.description; } catch (e) {}
    return why;
  } catch (e) {
    return String(e && e.message ? e.message : e);
  }
}

// ---- offscreen (sound) ----
async function hasOffscreen() {
  if (chrome.runtime.getContexts) {
    const ctx = await chrome.runtime.getContexts({
      contextTypes: ["OFFSCREEN_DOCUMENT"],
    });
    return ctx.length > 0;
  }
  return false;
}
async function ensureOffscreen() {
  if (await hasOffscreen()) return;
  try {
    await chrome.offscreen.createDocument({
      url: "offscreen.html",
      reasons: ["AUDIO_PLAYBACK"],
      justification: "Play a notification sound when a new boost request arrives.",
    });
  } catch (e) {
    // May already exist due to a race; ignore.
  }
}
async function playSound(volume) {
  await ensureOffscreen();
  try {
    chrome.runtime.sendMessage({ target: "offscreen", type: "play", volume });
  } catch (e) {}
}

// ---- authenticated relay (offers/conversations go through the page origin) ----
function delay(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function relayFetch(method, path, body) {
  const tabs = await new Promise((r) =>
    chrome.tabs.query({ url: "https://www.eldorado.gg/*" }, (t) => r(t || []))
  );
  if (!tabs.length) return { ok: false, status: 0, error: "no-tab" };
  const tab = tabs.find((t) => t.status === "complete") || tabs[0];
  return new Promise((resolve) => {
    try {
      chrome.tabs.sendMessage(tab.id, { type: "apiFetch", method, path, body }, (resp) => {
        if (chrome.runtime.lastError) resolve({ ok: false, status: 0, error: chrome.runtime.lastError.message });
        else resolve(resp || { ok: false, status: 0, error: "no-response" });
      });
    } catch (e) {
      resolve({ ok: false, status: 0, error: String(e) });
    }
  });
}

// ---- offers ----
function buildOfferBody(detailsId, priceUSD, deliveryTime) {
  return {
    details: {
      boostingRequestId: detailsId,
      guaranteedDeliveryTime: deliveryTime || "Day28",
      pricing: {
        quantity: 1,
        minQuantity: 1,
        volumeDiscounts: null,
        pricePerUnit: { amount: priceUSD, currency: "USD" },
      },
    },
  };
}

async function getOffered() {
  const { offered } = await getLocal("offered");
  return offered && typeof offered === "object" ? offered : {};
}

// Send a single offer. opts.price overrides the computed price (manual button).
async function makeOffer(req, opts) {
  opts = opts || {};
  if (!req || !req.detailsId) return { ok: false, error: "no-detailsId" };
  const s = await getSettings();
  if (!s.master) return { ok: false, error: "master-off" };
  if (!OFFERABLE_CATEGORIES.includes(String(req.categoryId)))
    return { ok: false, error: "category-not-supported" };

  const detailsId = req.detailsId;
  const offered = await getOffered();
  if (offered[detailsId] && offered[detailsId].offerId)
    return { ok: false, error: "already-offered" };

  let price = opts.price;
  if (price == null) {
    const pr = EldoradoPricing.compute(req, gameSettings(s, req.game).pricing || {});
    if (!pr || pr.final == null) return { ok: false, error: "no-price" };
    price = pr.final;
  }

  const resp = await relayFetch(
    "POST",
    "/api/boostingOffers",
    buildOfferBody(detailsId, price, s.offer.deliveryTime)
  );

  if (resp.ok && resp.json) {
    offered[detailsId] = {
      offerId: resp.json.id || null,
      price,
      ts: Date.now(),
      state: resp.json.offerState || "Active",
    };
    await setLocal({ offered });
    if (gameSettings(s, req.game).notifyDesktop) {
      chrome.notifications.create("offer:" + detailsId, {
        type: "basic",
        iconUrl: "assets/icon128.png",
        title: "Offer sent — $" + price,
        message: [req.buyer, req.region].filter(Boolean).join(" · "),
        priority: 1,
      });
    }
    if (s.message.autoStartConversation) startConversation(detailsId, req).catch(() => {});
    return { ok: true, offerId: offered[detailsId].offerId, price };
  }

  offered[detailsId] = Object.assign({}, offered[detailsId], {
    error: resp.error || "http " + resp.status,
    ts: Date.now(),
  });
  await setLocal({ offered });
  return { ok: false, error: resp.error || "http-" + resp.status, status: resp.status };
}

async function startConversation(detailsId, req) {
  const resp = await relayFetch(
    "POST",
    "/api/boostingOffers/boostingRequests/" + detailsId + "/createConversationForSeller",
    {}
  );
  if (resp.ok && resp.json) {
    const offered = await getOffered();
    offered[detailsId] = Object.assign({}, offered[detailsId] || {}, {
      conversationId: resp.json.talkJsConversationId || null,
    });
    await setLocal({ offered });
    // NOTE: opener text sending is not wired yet — needs the outbound TalkJS
    // frame. When available: sendOpener(detailsId, renderTemplate(req)) here.
  }
  return resp;
}

// ---- auto-offer engine ----
async function autoOfferFresh(fresh, s) {
  if (!s.master || !s.offer.autoEnabled) return;
  const cap = Number(s.offer.priceCap) || 0;

  const eligible = [];
  for (const req of fresh) {
    if (!req.detailsId) continue;
    if (!OFFERABLE_CATEGORIES.includes(String(req.categoryId))) continue;
    if (s.offer.categories.length && !s.offer.categories.includes(String(req.categoryId))) continue;
    if (s.offer.regions.length) {
      const rc = EldoradoPricing.regionCode(req.region);
      const rl = (req.region || "").toLowerCase();
      const ok = s.offer.regions.some((x) => rc === x || rl.includes(String(x).toLowerCase()));
      if (!ok) continue;
    }
    const gp = gameSettings(s, req.game).pricing || {};
    const floor = gp.minOffer != null ? gp.minOffer : EldoradoPricing.DEFAULTS.minOffer;
    const pr = EldoradoPricing.compute(req, gp);
    if (!pr || pr.final == null) continue;
    if (pr.final < floor) continue;
    if (cap > 0 && pr.final > cap) continue; // too big for auto → leave for manual
    eligible.push({ req, price: pr.final });
  }
  if (!eligible.length) return;

  let { offerTimes } = await getLocal("offerTimes");
  offerTimes = (Array.isArray(offerTimes) ? offerTimes : []).filter((t) => Date.now() - t < 3600000);

  for (const { req, price } of eligible) {
    if (offerTimes.length >= (s.offer.maxPerHour || 0)) {
      console.log("[BoostWatcher] hourly offer cap reached; skipping rest");
      break;
    }
    const r = await makeOffer(req, { price });
    if (r.ok) {
      offerTimes.push(Date.now());
      await setLocal({ offerTimes });
      await delay(Math.max(0, (s.offer.minGapSec || 0) * 1000));
    } else if (r.error === "master-off") {
      break;
    }
  }
}

// ---- per-game alert filter (gs = that game's settings block) ----
function passesFilter(req, gs) {
  if (gs.filterTypes && gs.filterTypes.length && !gs.filterTypes.includes(req.type)) return false;
  if (gs.filterRegions && gs.filterRegions.length) {
    const region = (req.region || "").toLowerCase();
    if (!gs.filterRegions.some((r) => region.includes(String(r).toLowerCase()))) return false;
  }
  if (gs.minPrice > 0) {
    try {
      const pr = EldoradoPricing.compute(req, gs.pricing || {});
      if (pr && pr.final != null && pr.final < gs.minPrice) return false; // unpriceable (null) still passes
    } catch (e) {}
  }
  return true;
}

// ---- notification icon (rank emblem) + content ----
function emblemFor(req) {
  try {
    const g = EldoradoPricing.GAMES[req.game] || EldoradoPricing.GAMES.lol;
    const i = EldoradoPricing.parseRank(req.targetRank || req.currentRank, g);
    if (i >= 0) {
      const tier = String(g.ladder[i].tier).toLowerCase();
      return (req.game === "valorant" ? "assets/ranks/valorant/" : "assets/ranks/") + tier + ".png";
    }
  } catch (e) {}
  return "assets/icon128.png";
}

function notifContent(req, s) {
  const flow = (req.currentRank && req.targetRank)
    ? `${req.currentRank} → ${req.targetRank}`
    : (req.currentRank || req.extra || req.category || "Boost");
  let price = "";
  try {
    const pr = EldoradoPricing.compute(req, (s && s.pricing) || {});
    if (pr && pr.final != null) price = `$${pr.final}${pr.perWin ? "/win" : ""}`;
  } catch (e) {}
  const gameTag = req.game === "valorant" ? "Valorant" : req.game === "lol" ? "League of Legends" : (req.gameName || "");
  return {
    title: (price ? price + "  ·  " : "") + flow,
    message: [req.category, req.region, req.buyer].filter(Boolean).join("  ·  "),
    contextMessage: gameTag,
  };
}

// Emblem icon, price-first title, Aç/Yoksay buttons (auto-dismisses on its own).
function showBoostNotification(req, s) {
  const { title, message, contextMessage } = notifContent(req, s);
  chrome.notifications.create(NOTIF_PREFIX + req.id, {
    type: "basic",
    iconUrl: emblemFor(req),
    title, message, contextMessage,
    priority: 2,
    buttons: [{ title: "Aç" }, { title: "Yoksay" }],
  }, () => {
    if (chrome.runtime.lastError) console.warn("[BoostWatcher] notification failed:", chrome.runtime.lastError.message);
  });
}

// ---- badge ---- (unread already respects each game's notifyBadge preference)
async function refreshBadge(requests) {
  const unread = requests.filter((r) => r.unread).length;
  if (unread === 0) {
    chrome.action.setBadgeText({ text: "" });
    return;
  }
  chrome.action.setBadgeBackgroundColor({ color: "#e23b3b" });
  chrome.action.setBadgeText({ text: unread > 99 ? "99+" : String(unread) });
}

// ---- main handler: incoming requests ----
let processing = Promise.resolve();
function handleBoostRequests(items, ts) {
  // Process sequentially (avoid storage races)
  processing = processing.then(() => doHandle(items, ts)).catch((e) =>
    console.error("[BoostWatcher] error:", e)
  );
  return processing;
}

async function doHandle(items, ts) {
  const s = await getSettings();
  const { requests, knownIds } = await getLocal(["requests", "knownIds"]);
  const list = Array.isArray(requests) ? requests : [];
  // Dedup memory persists across "Clear list" so cleared/old notifications don't
  // reappear when the API keeps returning them on the next poll.
  const known = new Set(Array.isArray(knownIds) ? knownIds : []);
  list.forEach((r) => known.add(r.id)); // migrate ids from an older version

  const fresh = [];
  for (const it of items) {
    if (!it || !it.id || known.has(it.id)) continue;
    known.add(it.id);
    const gs = gameSettings(s, it.game);
    const alertable = passesFilter(it, gs);
    fresh.push(Object.assign({}, it, {
      firstSeenTs: ts || Date.now(),
      unread: alertable && gs.notifyBadge, // badge only if this game's badge is on
      alertable,
      status: "active", // re-checked later; flips to taken/cancelled/expired when gone
    }));
  }

  // Always update status (poll is alive)
  await setLocal({ lastPoll: ts || Date.now(), status: "ok" });

  if (fresh.length === 0) {
    await refreshBadge(list);
    return;
  }

  // Prepend new ones, cap the list. Keep knownIds capped to recent ones.
  const merged = fresh.concat(list).slice(0, MAX_STORED);
  await setLocal({ requests: merged, knownIds: Array.from(known).slice(-1000) });

  const alertables = fresh.filter((r) => r.alertable);

  // Desktop notifications: only for games whose desktop notification is on
  const desktopItems = alertables.filter((r) => gameSettings(s, r.game).notifyDesktop);
  if (desktopItems.length) {
    const MAX_INDIVIDUAL = 3;
    desktopItems.slice(0, MAX_INDIVIDUAL).forEach((req) => showBoostNotification(req, gameSettings(s, req.game)));
    if (desktopItems.length > MAX_INDIVIDUAL) {
      chrome.notifications.create(NOTIF_PREFIX + "more:" + (ts || Date.now()), {
        type: "basic",
        iconUrl: "assets/icon128.png",
        title: "New boost requests",
        message: `+${desktopItems.length - MAX_INDIVIDUAL} more requests`,
        priority: 1,
        buttons: [{ title: "Aç" }],
      });
    }
  }

  // Sound: once, if any alertable item's game has sound on (use that game's volume)
  const soundItem = alertables.find((r) => gameSettings(s, r.game).notifySound);
  if (soundItem) playSound(gameSettings(s, soundItem.game).soundVolume);

  await refreshBadge(merged);

  // Auto-offer (gated by master + offer.autoEnabled). Awaited so the worker
  // stays alive through the rate-limited offer sequence.
  try {
    await autoOfferFresh(fresh, s);
  } catch (e) {
    console.error("[BoostWatcher] auto-offer:", e);
  }
}

// ---- message routing ----
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (!msg || msg.target === "offscreen") return; // offscreen messages aren't for us
  switch (msg.type) {
    case "boostRequests":
      handleBoostRequests(msg.items || [], msg.ts).then(() => sendResponse({ ok: true }));
      return true; // async
    case "status":
      setLocal({ status: msg.status, lastPoll: msg.ts || Date.now() }).then(() =>
        sendResponse({ ok: true })
      );
      return true;
    case "markAllRead":
      markAllRead().then(() => sendResponse({ ok: true }));
      return true;
    case "clear":
      clearAll().then(() => sendResponse({ ok: true }));
      return true;
    case "statusUpdates":
      applyStatusUpdates(msg.updates || []).then(() => sendResponse({ ok: true }));
      return true;
    case "dismissOne":
      dismissOne(msg.id).then(() => sendResponse({ ok: true }));
      return true;
    case "testNotif":
      testNotification().then((err) => sendResponse({ ok: !err, error: err }));
      return true;
    case "testTelegram":
      notifyTelegram(
        "✅ <b>Boost Watcher</b> — Telegram bağlantısı çalışıyor. Buyer dönünce buraya haber gelecek.",
        { telegram: { enabled: true, token: msg.token, chatId: msg.chatId } }
      ).then((err) => sendResponse({ ok: !err, error: err }));
      return true;
    case "makeOffer":
      makeOffer(msg.req, { price: msg.price }).then((r) => sendResponse(r));
      return true;
    case "startConversation":
      startConversation(msg.detailsId, msg.req)
        .then((r) => sendResponse({ ok: r.ok, status: r.status, json: r.json }))
        .catch((e) => sendResponse({ ok: false, error: String(e) }));
      return true;
    case "testSound":
      getSettings().then((s) => playSound(gameSettings(s, "lol").soundVolume));
      sendResponse({ ok: true });
      return false;
    case "activateLicense":
      validateLicenseKey(msg.key).then((r) => sendResponse(r));
      return true; // async
    case "licenseStatus":
      getRawSettings().then((s) => sendResponse({ license: s.license, allowed: licenseAllowsMaster(s.license) }));
      return true;
  }
});

async function markAllRead() {
  const { requests } = await getLocal("requests");
  const list = (requests || []).map((r) => Object.assign({}, r, { unread: false }));
  await setLocal({ requests: list });
  chrome.action.setBadgeText({ text: "" });
}

async function clearAll() {
  // Remember currently-shown ids before clearing so they can't reappear on the next poll.
  const { requests, knownIds } = await getLocal(["requests", "knownIds"]);
  const known = new Set(Array.isArray(knownIds) ? knownIds : []);
  (requests || []).forEach((r) => known.add(r.id));
  await setLocal({ requests: [], knownIds: Array.from(known).slice(-1000) });
  chrome.action.setBadgeText({ text: "" });
}

// A request that's no longer Active gets flagged red in the list and loses its badge.
async function applyStatusUpdates(updates) {
  if (!updates.length) return;
  const byId = new Map(updates.map((u) => [u.id, u]));
  const s = await getSettings();
  const { requests } = await getLocal("requests");
  const list = Array.isArray(requests) ? requests : [];
  let changed = false;
  const next = list.map((r) => {
    const u = byId.get(r.id);
    if (!u || u.status === (r.status || "active")) return r;
    changed = true;
    return Object.assign({}, r, { status: u.status, state: u.state || r.state || null, expireDate: u.expireDate || r.expireDate || null, unread: false });
  });
  if (!changed) return;
  await setLocal({ requests: next });
  await refreshBadge(next);
}

// Dismiss a single request (the "Yoksay" card button) and remember its id.
async function dismissOne(id) {
  if (!id) return;
  const s = await getSettings();
  const { requests, knownIds } = await getLocal(["requests", "knownIds"]);
  const known = new Set(Array.isArray(knownIds) ? knownIds : []);
  known.add(id);
  const next = (requests || []).filter((r) => r.id !== id);
  await setLocal({ requests: next, knownIds: Array.from(known).slice(-1000) });
  await refreshBadge(next);
}

async function markOneRead(id) {
  const s = await getSettings();
  const { requests } = await getLocal("requests");
  let changed = false;
  const list = (requests || []).map((r) => {
    if (r.id === id && r.unread) { changed = true; return Object.assign({}, r, { unread: false }); }
    return r;
  });
  if (changed) { await setLocal({ requests: list }); await refreshBadge(list); }
}

async function testNotification() {
  const sample = { game: "lol", type: "rank", category: "Rank Boost", currentRank: "Diamond IV", targetRank: "Diamond I", region: "Europe West", buyer: "TestBuyer" };
  try {
    const s = await getSettings();
    const { title, message, contextMessage } = notifContent(sample, gameSettings(s, "lol"));
    return await new Promise((resolve) => {
      chrome.notifications.create(NOTIF_PREFIX + "test:" + Date.now(), {
        type: "basic", iconUrl: emblemFor(sample), title, message, contextMessage,
        priority: 2, buttons: [{ title: "Aç" }, { title: "Yoksay" }],
      }, () => resolve(chrome.runtime.lastError ? chrome.runtime.lastError.message : null));
    });
  } catch (e) { return String(e && e.message ? e.message : e); }
}

// ---- notification interaction ----
function notifReqId(notifId) {
  const id = notifId.startsWith(NOTIF_PREFIX) ? notifId.slice(NOTIF_PREFIX.length) : "";
  return id && !id.startsWith("more:") && !id.startsWith("test:") ? id : "";
}
async function openFromNotif(notifId) {
  let url = "https://www.eldorado.gg/dashboard/notifications";
  const id = notifReqId(notifId);
  if (id) {
    const { requests } = await getLocal("requests");
    const req = (requests || []).find((r) => r.id === id);
    if (req && req.detailsId) url = `https://www.eldorado.gg/boosting-request/${req.detailsId}`;
  }
  await chrome.tabs.create({ url });
}
chrome.notifications.onClicked.addListener(async (notifId) => {
  await openFromNotif(notifId);
  chrome.notifications.clear(notifId);
});
// "Aç" (0) opens the request; "Yoksay" (1) dismisses it (marks read).
chrome.notifications.onButtonClicked.addListener(async (notifId, idx) => {
  if (idx === 0) await openFromNotif(notifId);
  else { const id = notifReqId(notifId); if (id) await markOneRead(id); }
  chrome.notifications.clear(notifId);
});

// ---- first install: default settings ----
chrome.runtime.onInstalled.addListener(async () => {
  const { settings } = await getLocal("settings");
  if (!settings) await setLocal({ settings: normalizeSettings(null) });
  chrome.action.setBadgeBackgroundColor({ color: "#e23b3b" });
  chrome.alarms.create("licenseCheck", { periodInMinutes: 60 * 12 }); // twice daily
  recheckLicense();
});
chrome.runtime.onStartup.addListener(() => {
  chrome.alarms.create("licenseCheck", { periodInMinutes: 60 * 12 });
  recheckLicense();
});
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === "licenseCheck") recheckLicense();
});
async function recheckLicense() {
  const s = await getRawSettings();
  if (s.license && s.license.key) await validateLicenseKey(s.license.key);
}
