// Adstop — фоновый service worker.
// Отвечает за: триал и лицензию, включение/выключение блокировки,
// белый список сайтов (динамические правила DNR), счётчик на бейдже.

const API_BASE = "https://bez-reklamy.ru";
const TRIAL_DAYS = 7;
const LICENSE_CHECK_MINUTES = 360;
const LICENSE_GRACE_MS = 3 * 24 * 60 * 60 * 1000; // 3 дня офлайна не отключают подписку
const STATIC_RULESET_IDS = ["ads-global", "ads-ru"];
const COSMETIC_SCRIPT_ID = "adstop-generic-css";
const ALLOWLIST_RULE_ID_BASE = 1_000_000;

// Автообновление фильтров с сервера: расширение раз в сутки проверяет версию и
// тянет свежий пакет. Применённые правила живут как ДИНАМИЧЕСКИЕ в диапазоне
// [FILTERS_RULE_ID_BASE, ALLOWLIST_RULE_ID_BASE); когда они активны, встроенные
// статические рулсеты отключаются (иначе дубль и двойной расход лимита).
const FILTERS_CHECK_MINUTES = 720; // 2 раза в сутки
const FILTERS_RULE_ID_BASE = 1; // пакет приходит с id 1..~19332
const FILTERS_RULE_ID_MAX = ALLOWLIST_RULE_ID_BASE - 1;

const DAY_MS = 24 * 60 * 60 * 1000;

async function getLocal(keys) {
  return chrome.storage.local.get(keys);
}

async function setLocal(values) {
  return chrome.storage.local.set(values);
}

function trialEndsAt(installedAt) {
  return installedAt + TRIAL_DAYS * DAY_MS;
}

function isTrialActive(installedAt, now = Date.now()) {
  return Boolean(installedAt) && now < trialEndsAt(installedAt);
}

function isLicenseActive(license, now = Date.now()) {
  if (!license) return false;
  if (license.active && license.expiresAt && now < Date.parse(license.expiresAt)) {
    return true;
  }
  // льготный период: сервер был недоступен, но последняя проверка видела активную подписку
  if (license.active && license.checkedAt && now - license.checkedAt < LICENSE_GRACE_MS) {
    return true;
  }
  return false;
}

async function computeState() {
  const data = await getLocal([
    "installedAt", "enabled", "license", "licenseKey", "allowlist",
    "filtersVersion", "filtersUpdatedAt",
  ]);
  const now = Date.now();
  const trialActive = isTrialActive(data.installedAt, now);
  const licenseActive = isLicenseActive(data.license, now);
  const enabled = data.enabled !== false;
  return {
    enabled,
    trialActive,
    trialEndsAt: data.installedAt ? trialEndsAt(data.installedAt) : null,
    licenseActive,
    license: data.license ?? null,
    licenseKey: data.licenseKey ?? null,
    allowlist: data.allowlist ?? [],
    filtersVersion: data.filtersVersion ?? null,
    filtersUpdatedAt: data.filtersUpdatedAt ?? null,
    blockingActive: enabled && (trialActive || licenseActive),
  };
}

async function applyState() {
  const state = await computeState();
  const enable = state.blockingActive;
  const { filtersVersion } = await getLocal(["filtersVersion"]);
  const usingUpdatedFilters = Boolean(filtersVersion);
  // Когда применён свежий пакет с сервера, встроенные статические рулсеты
  // выключаем — их заменяют динамические правила пакета. Иначе работают
  // встроенные (первый запуск / сервер ни разу не отвечал).
  await chrome.declarativeNetRequest.updateEnabledRulesets(
    enable && !usingUpdatedFilters
      ? { enableRulesetIds: STATIC_RULESET_IDS }
      : { disableRulesetIds: STATIC_RULESET_IDS }
  );
  await applyNetworkFilterRules(enable);
  await registerGenericCosmetics(enable, state.allowlist);
  // флаг для content script (косметическая фильтрация)
  await setLocal({ blockingActive: enable });
  return state;
}

// Применяет/снимает сетевые правила из обновлённого пакета (динамические,
// диапазон id [FILTERS_RULE_ID_BASE..FILTERS_RULE_ID_MAX]).
async function applyNetworkFilterRules(enable) {
  const { filterNetworkRules } = await getLocal(["filterNetworkRules"]);
  const existing = await chrome.declarativeNetRequest.getDynamicRules();
  const removeRuleIds = existing
    .map((rule) => rule.id)
    .filter((ruleId) => ruleId >= FILTERS_RULE_ID_BASE && ruleId <= FILTERS_RULE_ID_MAX);
  const addRules = enable && Array.isArray(filterNetworkRules) ? filterNetworkRules : [];
  if (!removeRuleIds.length && !addRules.length) return;
  await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds, addRules });
}

// Generic-косметика (extension/content/generic.css из EasyList) подключается
// как регистрируемый CSS: так её можно выключать целиком и исключать сайты
// из белого списка через excludeMatches.
async function registerGenericCosmetics(enable, allowlist = []) {
  try {
    await chrome.scripting.unregisterContentScripts({ ids: [COSMETIC_SCRIPT_ID] });
  } catch {
    // ещё не был зарегистрирован
  }
  // Если применён пакет, generic-селекторы из него отдаёт content script вместе
  // с доменными (см. cosmeticsForHost) — файловый generic.css не подключаем,
  // иначе он перетрёт обновления устаревшими правилами.
  const { filterGeneric } = await getLocal(["filterGeneric"]);
  if (!enable || Array.isArray(filterGeneric)) return;
  const excludeMatches = allowlist.flatMap((host) => [
    `*://${host}/*`,
    `*://*.${host}/*`,
  ]);
  await chrome.scripting.registerContentScripts([
    {
      id: COSMETIC_SCRIPT_ID,
      css: ["content/generic.css"],
      matches: ["http://*/*", "https://*/*"],
      excludeMatches: excludeMatches.length ? excludeMatches : undefined,
      runAt: "document_start",
      persistAcrossSessions: true,
    },
  ]);
}

// Доменная косметика: из пакета (storage) если обновлялись, иначе из встроенного
// filters/specific.json. Кэшируем в памяти воркера; сбрасывается после обновления.
let specificCosmeticsPromise = null;

function loadSpecificCosmetics() {
  if (!specificCosmeticsPromise) {
    specificCosmeticsPromise = getLocal(["filterSpecific"]).then((data) => {
      if (data.filterSpecific && typeof data.filterSpecific === "object") {
        return data.filterSpecific;
      }
      return fetch(chrome.runtime.getURL("filters/specific.json"))
        .then((response) => response.json())
        .catch(() => ({}));
    });
  }
  return specificCosmeticsPromise;
}

async function cosmeticsForHost(host) {
  const map = await loadSpecificCosmetics();
  const selectors = [];
  // example.com и все родительские домены: a.b.example.com → b.example.com → example.com
  const parts = host.split(".");
  for (let i = 0; i < parts.length - 1; i++) {
    const candidate = parts.slice(i).join(".");
    if (map[candidate]) selectors.push(...map[candidate]);
  }
  // Если фильтры обновлялись, generic больше не идёт файлом — добавляем его сюда.
  const { filterGeneric } = await getLocal(["filterGeneric"]);
  if (Array.isArray(filterGeneric)) selectors.push(...filterGeneric);
  return [...new Set(selectors)];
}

async function rebuildAllowlistRules() {
  const { allowlist = [] } = await getLocal(["allowlist"]);
  const existing = await chrome.declarativeNetRequest.getDynamicRules();
  const removeRuleIds = existing
    .map((rule) => rule.id)
    .filter((ruleId) => ruleId >= ALLOWLIST_RULE_ID_BASE);
  const addRules = allowlist.map((host, index) => ({
    id: ALLOWLIST_RULE_ID_BASE + index,
    priority: 1000,
    action: { type: "allowAllRequests" },
    condition: {
      requestDomains: [host],
      resourceTypes: ["main_frame"],
    },
  }));
  await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds, addRules });
}

// === Автообновление фильтров ===

// Проверяет версию на сервере и качает пакет, только если она новее локальной.
// force=true качает всегда (кнопка «Обновить сейчас»). Возвращает сводку.
async function updateFilters({ force = false } = {}) {
  const { filtersVersion } = await getLocal(["filtersVersion"]);
  try {
    if (!force) {
      const versionResponse = await fetch(`${API_BASE}/api/filters/version`, { cache: "no-store" });
      if (!versionResponse.ok) throw new Error(`version http ${versionResponse.status}`);
      const { version } = await versionResponse.json();
      if (version && version === filtersVersion) {
        await setLocal({ filtersCheckedAt: Date.now() });
        return { updated: false, version, reason: "up-to-date" };
      }
    }
    const bundleResponse = await fetch(`${API_BASE}/api/filters/bundle`, { cache: "no-store" });
    if (!bundleResponse.ok) throw new Error(`bundle http ${bundleResponse.status}`);
    const bundle = await bundleResponse.json();
    if (!bundle?.version || !Array.isArray(bundle.network)) {
      throw new Error("пакет повреждён");
    }
    await setLocal({
      filterNetworkRules: bundle.network,
      filterGeneric: Array.isArray(bundle.generic) ? bundle.generic : [],
      filterSpecific: bundle.specific && typeof bundle.specific === "object" ? bundle.specific : {},
      filtersVersion: bundle.version,
      filtersCheckedAt: Date.now(),
      filtersUpdatedAt: Date.now(),
    });
    specificCosmeticsPromise = null; // сбросить кэш косметики
    await applyState();
    return {
      updated: true,
      version: bundle.version,
      networkRules: bundle.network.length,
    };
  } catch (error) {
    await setLocal({ filtersCheckedAt: Date.now() });
    return { updated: false, error: error.message };
  }
}

// Проверка лицензии по ключу: GET /api/license?key=ADS-...
// Возвращает объект статуса или null (ключа нет / сеть недоступна — тогда
// остаётся кэш, см. isLicenseActive + льготный период).
async function checkLicense() {
  const { licenseKey, license } = await getLocal(["licenseKey", "license"]);
  if (!licenseKey) return null;
  try {
    const response = await fetch(`${API_BASE}/api/license?key=${encodeURIComponent(licenseKey)}`);
    if (!response.ok) throw new Error(`license http ${response.status}`);
    const payload = await response.json();
    const updated = {
      active: Boolean(payload.active),
      plan: payload.plan ?? null,
      expiresAt: payload.expiresAt ?? null,
      checkedAt: Date.now(),
    };
    await setLocal({ license: updated });
    return updated;
  } catch {
    // сеть недоступна — оставляем кэшированный статус
    return license ?? null;
  }
}

// Активация ключа из попапа: проверяем на сервере и сохраняем, если оплачен.
async function activateKey(rawKey) {
  const key = String(rawKey ?? "").trim().toUpperCase().replace(/\s+/g, "");
  if (!/^ADS(-[A-Z0-9]{4}){3}$/.test(key)) {
    throw new Error("Неверный формат ключа. Пример: ADS-XXXX-XXXX-XXXX");
  }
  let payload;
  try {
    const response = await fetch(`${API_BASE}/api/license?key=${encodeURIComponent(key)}`);
    payload = await response.json().catch(() => ({}));
  } catch {
    throw new Error("Не удалось связаться с сервером. Проверьте интернет.");
  }
  if (!payload?.active) {
    throw new Error("Ключ не найден или подписка по нему не активна.");
  }
  await setLocal({
    licenseKey: key,
    license: {
      active: true,
      plan: payload.plan ?? null,
      expiresAt: payload.expiresAt ?? null,
      checkedAt: Date.now(),
    },
  });
  return applyState();
}

async function removeKey() {
  await chrome.storage.local.remove(["licenseKey", "license"]);
  return applyState();
}

async function toggleAllowlist(host) {
  const { allowlist = [] } = await getLocal(["allowlist"]);
  const next = allowlist.includes(host)
    ? allowlist.filter((item) => item !== host)
    : [...allowlist, host];
  await setLocal({ allowlist: next });
  await rebuildAllowlistRules();
  return computeState();
}

async function initialize(reason) {
  const data = await getLocal(["installedAt"]);
  if (!data.installedAt) {
    await setLocal({ installedAt: Date.now(), enabled: true, allowlist: [] });
  }
  chrome.declarativeNetRequest.setExtensionActionOptions({
    displayActionCountAsBadgeText: true,
  });
  chrome.alarms.create("license-check", { periodInMinutes: LICENSE_CHECK_MINUTES });
  chrome.alarms.create("filters-check", { periodInMinutes: FILTERS_CHECK_MINUTES });
  if (reason === "install") {
    chrome.tabs.create({ url: `${API_BASE}/?welcome=extension` });
  }
  await checkLicense();
  await rebuildAllowlistRules();
  await applyState();
  // проверяем обновления фильтров в фоне, не блокируя инициализацию
  updateFilters().catch(() => {});
}

chrome.runtime.onInstalled.addListener((details) => {
  initialize(details.reason);
});

chrome.runtime.onStartup.addListener(() => {
  initialize("startup");
});

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === "license-check") {
    await checkLicense();
    await applyState();
  }
  if (alarm.name === "filters-check") {
    await updateFilters();
  }
});

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  (async () => {
    switch (message?.type) {
      case "getState":
        sendResponse(await computeState());
        break;
      case "setEnabled":
        await setLocal({ enabled: Boolean(message.enabled) });
        sendResponse(await applyState());
        break;
      case "toggleAllowlist":
        await toggleAllowlist(message.host);
        sendResponse(await applyState());
        break;
      case "activateKey":
        try {
          sendResponse(await activateKey(message.key));
        } catch (error) {
          sendResponse({ error: error.message });
        }
        break;
      case "removeKey":
        sendResponse(await removeKey());
        break;
      case "refreshLicense":
        await checkLicense();
        sendResponse(await applyState());
        break;
      case "updateFilters":
        sendResponse(await updateFilters({ force: Boolean(message.force) }));
        break;
      case "getCosmetic": {
        const state = await computeState();
        const host = String(message.host ?? "").replace(/^www\./, "");
        if (!state.blockingActive || !host || state.allowlist.includes(host)) {
          sendResponse({ active: false, selectors: [] });
          break;
        }
        sendResponse({ active: true, selectors: await cosmeticsForHost(host) });
        break;
      }
      default:
        sendResponse({ error: "unknown message" });
    }
  })();
  return true; // асинхронный sendResponse
});
