/**
 * Grax — Manifest V3 service worker
 *
 * Dynamic-only compressed DNR architecture:
 * - No static `rule_resources` entry is used, eliminating Chrome manifest
 *   ruleset parser and static-quota load failures.
 * - 49,182 logical filter rules plus a dedicated navigation-defense layer are installed as compact native dynamic rules
 *   by grouping equivalent domain conditions without losing domain coverage.
 * - EasyList, EasyPrivacy, Grax security, pop-under navigation defense,
 *   trusted-site policies, and custom blocks remain independently controllable.
 * - Runtime validation and resilient installation isolate unsupported rules
 *   instead of allowing one rule to disable the complete engine.
 */
'use strict';

const APP = Object.freeze({
  name: 'Grax',
  schemaVersion: 12,
  alarm: 'grax-list-update-check',
  alarmPeriodMinutes: 1440,
  maxAllowlistedDomains: 1000,
  maxCustomBlockedDomains: 2000,
  badgeColor: '#00E5FF',
  metadataPath: 'rules/metadata.json',
  dynamicSafetyHeadroom: 1000,
  installBatchSize: 1000
});

const RUNTIME_SCRIPT_IDS = Object.freeze({
  compatibilityShim: 'grax-main-compatibility-shim',
  pageGuard: 'grax-main-page-guard',
  contentEngine: 'grax-isolated-content-engine'
});

const TRUSTED_NATIVE_APPLICATION_MATCHES = Object.freeze([
  '*://claude.ai/*',
  '*://*.claude.ai/*',
  '*://deepseek.com/*',
  '*://*.deepseek.com/*'
]);

const SECURITY_VERIFICATION_FRAME_MATCHES = Object.freeze([
  '*://challenges.cloudflare.com/*',
  '*://*.hcaptcha.com/*',
  '*://*.recaptcha.net/*',
  '*://*.arkoselabs.com/*',
  '*://*.funcaptcha.com/*',
  '*://*.captcha-delivery.com/*'
]);

const COMPATIBILITY_SHIM_EXCLUDE_MATCHES = Object.freeze([
  ...TRUSTED_NATIVE_APPLICATION_MATCHES,
  ...SECURITY_VERIFICATION_FRAME_MATCHES
]);

const RUNTIME_CONTENT_SCRIPTS = Object.freeze([
  Object.freeze({
    id: RUNTIME_SCRIPT_IDS.compatibilityShim,
    matches: ['http://*/*', 'https://*/*'],
    js: ['compat-shim.js'],
    excludeMatches: [...COMPATIBILITY_SHIM_EXCLUDE_MATCHES],
    runAt: 'document_start',
    allFrames: true,
    matchOriginAsFallback: true,
    persistAcrossSessions: true,
    world: 'MAIN'
  }),
  Object.freeze({
    id: RUNTIME_SCRIPT_IDS.pageGuard,
    matches: ['http://*/*', 'https://*/*'],
    js: ['page-guard.js'],
    excludeMatches: [...TRUSTED_NATIVE_APPLICATION_MATCHES],
    runAt: 'document_start',
    allFrames: true,
    matchOriginAsFallback: true,
    persistAcrossSessions: true,
    world: 'MAIN'
  }),
  Object.freeze({
    id: RUNTIME_SCRIPT_IDS.contentEngine,
    matches: ['http://*/*', 'https://*/*'],
    js: ['content.js'],
    excludeMatches: [...TRUSTED_NATIVE_APPLICATION_MATCHES],
    runAt: 'document_start',
    allFrames: true,
    matchOriginAsFallback: true,
    persistAcrossSessions: true,
    world: 'ISOLATED'
  })
]);

const DYNAMIC_SUPPLEMENTS = Object.freeze([
  Object.freeze({
    name: 'grax',
    path: 'rules/runtime_grax.json',
    minId: 4_000_000,
    maxId: 4_999_999,
    enabled: settings => settings.enabled
  }),
  Object.freeze({
    name: 'popunder',
    path: 'rules/runtime_popunder.json',
    minId: 7_000_000,
    maxId: 7_999_999,
    enabled: settings => settings.enabled
  }),
  Object.freeze({
    name: 'easyprivacy',
    path: 'rules/runtime_easyprivacy.json',
    minId: 6_000_000,
    maxId: 6_999_999,
    enabled: settings => settings.enabled && settings.lists.easyprivacy
  }),
  Object.freeze({
    name: 'easylist',
    path: 'rules/runtime_easylist.json',
    minId: 5_000_000,
    maxId: 5_999_999,
    enabled: settings => settings.enabled && settings.lists.easylist
  })
]);

const STORAGE_KEYS = Object.freeze({
  schema: 'schemaVersion',
  settings: 'settings',
  allowlist: 'allowlist',
  customBlocklist: 'customBlocklist',
  diagnostics: 'diagnostics',
  upstreamMetadata: 'upstreamMetadata'
});

const SESSION_KEYS = Object.freeze({
  tabBypasses: 'graxTabBypassesV1'
});

const DEFAULT_SETTINGS = Object.freeze({
  enabled: true,
  cosmeticFiltering: true,
  showBadge: true,
  automaticUpdateChecks: true,
  lists: Object.freeze({
    easylist: true,
    easyprivacy: true
  })
});

const DEFAULT_DIAGNOSTICS = Object.freeze({
  lastReconciledAt: null,
  lastReconcileReason: null,
  lastUpdateCheckAt: null,
  lastUpdateCheckError: null,
  updateAvailable: false,
  staticCoverage: null,
  dynamicCoverage: null,
  rejectedRuleIds: []
});

const UPSTREAM_SOURCES = Object.freeze({
  easylist: 'https://easylist.to/easylist/easylist.txt',
  easyprivacy: 'https://easylist.to/easylist/easyprivacy.txt'
});

const RULE_IDS = Object.freeze({
  policyMin: 1_000_000,
  policyMax: 2_999_999,
  allowAllStart: 1_000_000,
  allowRequestStart: 1_100_000,
  customBlockStart: 2_000_000,
  supplementMin: 4_000_000,
  supplementMax: 7_999_999,
  compatibilityMin: 8_000_000,
  compatibilityMax: 8_999_999,
  sessionBypassMin: 9_000_000,
  sessionBypassMax: 9_999_999
});

const RESOURCE_TYPES = Object.freeze([
  'main_frame',
  'sub_frame',
  'stylesheet',
  'script',
  'image',
  'font',
  'object',
  'xmlhttprequest',
  'ping',
  'csp_report',
  'media',
  'websocket',
  'webtransport',
  'webbundle',
  'other'
]);

const SAFE_DYNAMIC_ACTIONS = new Set([
  'block',
  'allow',
  'allowAllRequests',
  'upgradeScheme',
  'redirect'
]);

const PURE_TELEMETRY_DOMAINS = Object.freeze([
  'sentry.io', 'sentry-cdn.com',
  'mixpanel.com', 'mxpnl.com',
  'google-analytics.com', 'analytics.google.com', 'googletagmanager.com', 'doubleclick.net',
  'datadoghq.com', 'datadoghq-browser-agent.com',
  'segment.com', 'segment.io', 'segmentapis.com',
  'newrelic.com', 'nr-data.net',
  'amplitude.com', 'hotjar.com', 'hotjar.io', 'clarity.ms',
  'fullstory.com', 'logrocket.io', 'bugsnag.com', 'bugsnagjs.com',
  'rollbar.com', 'rollbarcdn.com', 'heap.io', 'heapanalytics.com',
  'rudderstack.com', 'rudderlabs.com', 'posthog.com',
  'mouseflow.com', 'smartlook.com', 'inspectlet.com', 'crazyegg.com'
]);

const FUNCTIONAL_SDK_DOMAINS = Object.freeze([
  'intercom.io', 'intercomcdn.com', 'intercomassets.com',
  'hubspot.com', 'hubspot.net', 'hs-analytics.net', 'hs-scripts.com', 'hscollectedforms.net',
  'appcues.com', 'pendo.io'
]);

const TELEMETRY_COMPATIBILITY_DOMAINS = Object.freeze([
  ...PURE_TELEMETRY_DOMAINS,
  ...FUNCTIONAL_SDK_DOMAINS
]);

const STRUCTURAL_AUTH_DOMAINS = Object.freeze([
  'accounts.google.com', 'oauth2.googleapis.com', 'apis.google.com', 'googleapis.com',
  'gstatic.com', 'recaptcha.net', 'hcaptcha.com', 'newassets.hcaptcha.com',
  'challenges.cloudflare.com', 'cloudflare.com', 'supabase.co', 'supabase.com',
  'auth0.com', 'okta.com', 'onelogin.com', 'clerk.com', 'clerk.accounts.dev',
  'appleid.apple.com', 'login.microsoftonline.com', 'login.live.com',
  'github.com', 'githubusercontent.com', 'facebook.com', 'facebook.net',
  'stripe.com', 'stripe.network', 'paypal.com', 'paypalobjects.com',
  'captcha-delivery.com', 'datadome.co', 'arkoselabs.com', 'funcaptcha.com'
]);

const STRUCTURAL_ASSET_FILTERS = Object.freeze([
  '/_next/static/', '/_next/data/', '/_nuxt/', '/static/chunks/',
  '/assets/vendor/', '/assets/chunks/', '/build/assets/', '/dist/assets/',
  '/webpack/', '/vite/', '/runtime-', '/framework-', '/polyfills-',
  '/main-', '/app-', '/chunk-', '/chunks/', '/code-split/', '/vendor/'
]);

const STRUCTURAL_MODULE_FILTERS = Object.freeze([
  '.mjs', '.module.js', '?module', '&module', '/esm/', '/esmodules/', '/modules/'
]);

const FIRST_PARTY_REALTIME_FILTERS = Object.freeze([
  '/socket.io/', '/realtime/', '/events/stream', '/api/events', '/api/stream',
  '/eventsource', '/sse/', '/subscribe', '/subscriptions'
]);

const FIRST_PARTY_AUTH_PATH_FILTERS = Object.freeze([
  '/oauth/', '/oauth2/', '/auth/', '/session', '/sessions', '/token', '/callback',
  '/captcha/', '/recaptcha/', '/turnstile/', '/cdn-cgi/challenge-platform/'
]);

// The universal first-party safeguard is deliberately excluded on the aggressive
// streaming origin whose existing Grax DOM and navigation defenses are tuned as
// a complete unit. This preserves the Filma24 pipeline byte-for-byte while the
// compatibility allowance protects ordinary application shells elsewhere.
const FIRST_PARTY_COMPATIBILITY_EXCLUSIONS = Object.freeze([
  'filma24.stream'
]);

// Grok is a first-party application spread across the xAI/X delivery family.
// Requests are allowed only when both the initiator and destination remain
// inside this entity, so unrelated sites do not inherit a broad X/Twitter
// network bypass. The rule is intentionally stronger than packaged filters and
// compatibility mocks, while tab/site suspension remains stronger still.
const GROK_TRUSTED_ENTITY_DOMAINS = Object.freeze([
  'grok.com', 'x.com', 'twitter.com', 'twimg.com'
]);

// Claude's application runtime and Anthropic-owned delivery hosts remain fully
// native. Turnstile and other verification providers are separately covered by
// the stronger structural-auth allowance above.
const CLAUDE_TRUSTED_ENTITY_DOMAINS = Object.freeze([
  'claude.ai', 'anthropic.com'
]);

// DeepSeek serves its authenticated application, API, static bundles, and policy
// resources across subdomains of deepseek.com. The entity-scoped allowance is
// active only when the initiator is also part of that same official family.
const DEEPSEEK_TRUSTED_ENTITY_DOMAINS = Object.freeze([
  'deepseek.com', 'chat.deepseek.com', 'api.deepseek.com',
  'static.deepseek.com', 'cdn.deepseek.com'
]);

const FIRST_PARTY_STRUCTURAL_RESOURCE_TYPES = Object.freeze([
  'script', 'stylesheet', 'xmlhttprequest', 'font', 'media', 'webbundle', 'other'
]);

// Only high-confidence collector routes are mocked. Generic paths such as
// /events, /analytics, or /metrics are intentionally absent because dashboards
// frequently use those names for product data rather than telemetry.
const VERIFIED_FIRST_PARTY_TELEMETRY_FILTERS = Object.freeze([
  '/api/telemetry/collect', '/telemetry/collect',
  '/api/analytics/collect', '/analytics/collect',
  '/api/metrics/collect', '/metrics/collect',
  '/api/tracking/collect', '/tracking/collect',
  '/api/beacon/collect', '/beacon/collect',
  '/sentry/envelope', '/monitoring/ingest', '/observability/ingest'
]);

// Explicit first-party ad-loader signatures remain neutralized even though
// normal same-site application chunks are allowed. Redirecting to a valid local
// response avoids hard network failures and preserves page lifecycle contracts.
const VERIFIED_FIRST_PARTY_AD_FILTERS = Object.freeze([
  '/adserver/serve', '/adserver/request',
  '/adservice/serve', '/adservice/request',
  '/ads/serve', '/ads/request',
  '/prebid.js', '/prebid.min.js', '/prebid/auction',
  '/ad-loader.js', '/ads-loader.js', '/ad.bundle.js',
  '/vast/adtag', '/vmap/adtag'
]);

const POPUNDER_EXCLUDED_REQUEST_DOMAINS = Object.freeze([
  'ad-maven.com', 'admaven.com', 'adnium.com', 'adnium.net', 'adsession.com',
  'adspyglass.com', 'adspyglass.net', 'adsterra.com', 'affforce.com',
  'clickadilla.com', 'clickadu.com', 'clickaine.com', 'dogsprotectedfeed.info',
  'eagamerz.com', 'evadav.com', 'exoclick.com', 'exosrv.com', 'hilltopads.com',
  'hilltopads.net', 'juicyads.com', 'onclicka.com', 'onclickads.net',
  'onclickperformance.com', 'plugrush.com', 'popads.net', 'popcash.com',
  'popcash.net', 'popunder.net', 'popunderjs.com', 'propellerads.com',
  'richads.com', 'supertracker200.com', 'trafficjunky.com', 'trafficstars.com',
  'zeroredirect1.com', 'zeroredirect2.com', 'zeroredirect3.com'
]);

const INTERNAL_UI_PATHS = Object.freeze(new Set(['/popup.html', '/options.html']));

let metadataPromise = null;
const supplementPromises = new Map();
let operationQueue = Promise.resolve();
let runtimeEnabledCache = null;

chrome.runtime.onInstalled.addListener(details => {
  void install(details);
});

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

chrome.alarms.onAlarm.addListener(alarm => {
  if (alarm.name === APP.alarm) void handleAlarm(alarm);
});

chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'local') void handleStorageChange(changes);
});

chrome.tabs.onRemoved.addListener(tabId => {
  void clearTabBypass(tabId);
});

chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
  if (typeof changeInfo?.url === 'string') {
    void reconcileTabBypassNavigation(tabId, changeInfo.url);
  }
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  void handleMessage(message, sender)
    .then(result => sendResponse({ ok: true, result }))
    .catch(error => {
      sendResponse({ ok: false, error: publicError(error) });
    });
  return true;
});

async function install(details) {
  await initializeStorage();
  await pruneTabBypasses();
  await synchronizeRuntimeLifecycle(`installed:${details.reason}`, {
    injectExisting: true,
    forceBroadcast: true
  });
  if (details.reason === 'install') {
    await chrome.runtime.openOptionsPage().catch(() => undefined);
  }
}

async function startup() {
  await initializeStorage();
  await pruneTabBypasses();
  await synchronizeRuntimeLifecycle('startup', {
    injectExisting: true,
    forceBroadcast: true
  });
}

async function initializeStorage() {
  if (typeof chrome.storage.local.setAccessLevel === 'function') {
    await chrome.storage.local.setAccessLevel({
      accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS'
    });
  }

  const old = await chrome.storage.local.get(Object.values(STORAGE_KEYS));
  await chrome.storage.local.set({
    [STORAGE_KEYS.schema]: APP.schemaVersion,
    [STORAGE_KEYS.settings]: sanitizeSettings(old[STORAGE_KEYS.settings]),
    [STORAGE_KEYS.allowlist]: sanitizeDomains(
      old[STORAGE_KEYS.allowlist],
      APP.maxAllowlistedDomains
    ),
    [STORAGE_KEYS.customBlocklist]: sanitizeDomains(
      old[STORAGE_KEYS.customBlocklist],
      APP.maxCustomBlockedDomains
    ),
    [STORAGE_KEYS.diagnostics]: {
      ...DEFAULT_DIAGNOSTICS,
      ...(isObject(old[STORAGE_KEYS.diagnostics])
        ? old[STORAGE_KEYS.diagnostics]
        : {})
    },
    [STORAGE_KEYS.upstreamMetadata]: isObject(old[STORAGE_KEYS.upstreamMetadata])
      ? old[STORAGE_KEYS.upstreamMetadata]
      : {}
  });
}

async function ensureAlarm(settings = DEFAULT_SETTINGS) {
  if (!settings.enabled || !settings.automaticUpdateChecks) {
    await clearAlarm();
    return;
  }
  if (!(await chrome.alarms.get(APP.alarm))) {
    await chrome.alarms.create(APP.alarm, {
      delayInMinutes: 15,
      periodInMinutes: APP.alarmPeriodMinutes
    });
  }
}

async function clearAlarm() {
  await chrome.alarms.clear(APP.alarm).catch(() => false);
}

async function handleAlarm(alarm) {
  if (alarm?.name !== APP.alarm) return;
  const enabled = await readMasterEnabled();
  if (!enabled) return;
  await scheduledUpdateCheck();
}

async function handleStorageChange(changes) {
  const settingsChanged = Object.hasOwn(changes, STORAGE_KEYS.settings);
  const policyChanged = [STORAGE_KEYS.allowlist, STORAGE_KEYS.customBlocklist]
    .some(key => Object.hasOwn(changes, key));

  if (settingsChanged) {
    const nextEnabled = sanitizeSettings(changes[STORAGE_KEYS.settings]?.newValue).enabled;
    await synchronizeRuntimeLifecycle('storage-change', {
      injectExisting: nextEnabled,
      forceBroadcast: false
    });
    return;
  }

  if (policyChanged && await readMasterEnabled()) {
    await reconcileAll('storage-change');
  }
}

async function readMasterEnabled() {
  if (typeof runtimeEnabledCache === 'boolean') return runtimeEnabledCache;
  const data = await chrome.storage.local.get(STORAGE_KEYS.settings);
  return sanitizeSettings(data[STORAGE_KEYS.settings]).enabled;
}

async function synchronizeRuntimeLifecycle(reason, {
  injectExisting = false,
  forceBroadcast = false
} = {}) {
  return enqueue(async () => {
    const state = await readState();
    const enabled = Boolean(state.settings.enabled);
    const changed = runtimeEnabledCache !== enabled;

    if (!enabled) {
      if (changed || forceBroadcast) {
        await broadcast({ type: 'GRAX_PROTECTION_STATE_CHANGED', enabled: false });
      }
      await unregisterRuntimeContentScripts();
      await clearAlarm();
      const result = await reconcileAllNow(reason, state);
      runtimeEnabledCache = false;
      return result;
    }

    await registerRuntimeContentScripts();
    await ensureAlarm(state.settings);
    const result = await reconcileAllNow(reason, state);
    runtimeEnabledCache = true;

    if (injectExisting || changed) await injectRuntimeIntoOpenTabs();
    if (changed || forceBroadcast) {
      await broadcast({ type: 'GRAX_PROTECTION_STATE_CHANGED', enabled: true });
    }
    return result;
  });
}

async function registerRuntimeContentScripts() {
  if (!chrome.scripting?.registerContentScripts) return;
  const ids = Object.values(RUNTIME_SCRIPT_IDS);
  let registered = [];
  try {
    registered = await chrome.scripting.getRegisteredContentScripts({ ids });
  } catch {}
  const present = new Set(registered.map(item => item.id));
  const missing = RUNTIME_CONTENT_SCRIPTS.filter(item => !present.has(item.id));
  if (missing.length) await chrome.scripting.registerContentScripts(missing);
}

async function unregisterRuntimeContentScripts() {
  if (!chrome.scripting?.unregisterContentScripts) return;
  const ids = Object.values(RUNTIME_SCRIPT_IDS);
  let registered = [];
  try {
    registered = await chrome.scripting.getRegisteredContentScripts({ ids });
  } catch {}
  const present = registered.map(item => item.id);
  if (present.length) await chrome.scripting.unregisterContentScripts({ ids: present });
}

async function injectRuntimeIntoOpenTabs() {
  if (!chrome.scripting?.executeScript) return;
  let tabs = [];
  try {
    tabs = await chrome.tabs.query({ url: ['http://*/*', 'https://*/*'] });
  } catch {
    return;
  }

  for (const tab of tabs) {
    if (!Number.isInteger(tab.id)) continue;
    if (isTrustedNativeApplicationUrl(tab.url)) continue;
    const target = { tabId: tab.id, allFrames: true };
    try {
      await chrome.scripting.executeScript({
        target,
        files: ['compat-shim.js'],
        world: 'MAIN',
        injectImmediately: true
      });
      await chrome.scripting.executeScript({
        target,
        files: ['page-guard.js'],
        world: 'MAIN',
        injectImmediately: true
      });
      await chrome.scripting.executeScript({
        target,
        files: ['content.js'],
        world: 'ISOLATED',
        injectImmediately: true
      });
    } catch {
      // Restricted browser pages and already-destroyed frames are intentionally skipped.
    }
  }
}

function isTrustedNativeApplicationUrl(value) {
  try {
    const hostname = new URL(String(value || '')).hostname.toLowerCase();
    return hostname === 'claude.ai' || hostname.endsWith('.claude.ai') ||
      hostname === 'deepseek.com' || hostname.endsWith('.deepseek.com');
  } catch {
    return false;
  }
}

async function loadMetadata() {
  metadataPromise ??= fetch(chrome.runtime.getURL(APP.metadataPath), {
    cache: 'no-store',
    credentials: 'omit'
  }).then(async response => {
    if (!response.ok) {
      throw new GraxError(
        'METADATA_LOAD_FAILED',
        'Packaged filter metadata could not be loaded.'
      );
    }

    const value = await response.json();
    if (
      !isObject(value) ||
      !Array.isArray(value.dynamicResources)
    ) {
      throw new GraxError(
        'INVALID_METADATA',
        'Packaged filter metadata is invalid.'
      );
    }
    return value;
  });
  return metadataPromise;
}

async function loadSupplement(definition) {
  if (supplementPromises.has(definition.name)) {
    return supplementPromises.get(definition.name);
  }

  const promise = fetch(chrome.runtime.getURL(definition.path), {
    cache: 'no-store',
    credentials: 'omit'
  }).then(async response => {
    if (!response.ok) {
      throw new GraxError(
        'SUPPLEMENT_LOAD_FAILED',
        `Packaged ${definition.name} supplement could not be loaded.`
      );
    }

    const rules = await response.json();
    if (!Array.isArray(rules)) {
      throw new GraxError(
        'INVALID_SUPPLEMENT',
        `Packaged ${definition.name} supplement is not a rule array.`
      );
    }

    const seenIds = new Set();
    for (const rule of rules) {
      validateDynamicSupplementRule(rule, definition, seenIds);
    }
    return rules;
  });

  supplementPromises.set(definition.name, promise);
  return promise;
}

function validateDynamicSupplementRule(rule, definition, seenIds) {
  if (!isObject(rule)) {
    throw new GraxError('INVALID_SUPPLEMENT', 'A packaged dynamic rule is malformed.');
  }
  if (
    !Number.isInteger(rule.id) ||
    rule.id < definition.minId ||
    rule.id > definition.maxId ||
    seenIds.has(rule.id)
  ) {
    throw new GraxError(
      'INVALID_SUPPLEMENT_ID',
      `Packaged ${definition.name} supplement contains an invalid rule ID.`
    );
  }
  seenIds.add(rule.id);
  if (!Number.isInteger(rule.priority) || rule.priority < 1) {
    throw new GraxError('INVALID_SUPPLEMENT_PRIORITY', `Packaged ${definition.name} supplement contains an invalid priority.`);
  }
  if (!isObject(rule.action) || !SAFE_DYNAMIC_ACTIONS.has(rule.action.type)) {
    throw new GraxError('UNSAFE_SUPPLEMENT_ACTION', `Packaged ${definition.name} supplement contains a non-safe action.`);
  }
  if (!isObject(rule.condition) || Object.keys(rule.condition).length === 0) {
    throw new GraxError('INVALID_SUPPLEMENT_CONDITION', `Packaged ${definition.name} supplement contains an invalid condition.`);
  }
  const condition = rule.condition;
  for (const key of ['requestDomains', 'excludedRequestDomains', 'initiatorDomains', 'excludedInitiatorDomains']) {
    if (key in condition && (!Array.isArray(condition[key]) || condition[key].length === 0 || condition[key].some(value => typeof value !== 'string' || !value))) {
      throw new GraxError('INVALID_SUPPLEMENT_DOMAIN_CONDITION', `Packaged ${definition.name} supplement contains an invalid ${key} condition.`);
    }
  }
  for (const key of ['resourceTypes', 'excludedResourceTypes']) {
    if (key in condition && (!Array.isArray(condition[key]) || condition[key].length === 0 || condition[key].some(value => !RESOURCE_TYPES.includes(value)))) {
      throw new GraxError('INVALID_SUPPLEMENT_RESOURCE_TYPE', `Packaged ${definition.name} supplement contains an invalid resource type.`);
    }
  }
  if ('urlFilter' in condition && typeof condition.urlFilter !== 'string') {
    throw new GraxError('INVALID_URL_FILTER', 'A packaged URL filter is malformed.');
  }
  if ('regexFilter' in condition && typeof condition.regexFilter !== 'string') {
    throw new GraxError('INVALID_REGEX_FILTER', 'A packaged regex filter is malformed.');
  }
}

async function readState() {
  const data = await chrome.storage.local.get([
    STORAGE_KEYS.settings,
    STORAGE_KEYS.allowlist,
    STORAGE_KEYS.customBlocklist,
    STORAGE_KEYS.diagnostics,
    STORAGE_KEYS.upstreamMetadata
  ]);

  return {
    settings: sanitizeSettings(data[STORAGE_KEYS.settings]),
    allowlist: sanitizeDomains(
      data[STORAGE_KEYS.allowlist],
      APP.maxAllowlistedDomains
    ),
    customBlocklist: sanitizeDomains(
      data[STORAGE_KEYS.customBlocklist],
      APP.maxCustomBlockedDomains
    ),
    diagnostics: {
      ...DEFAULT_DIAGNOSTICS,
      ...(isObject(data[STORAGE_KEYS.diagnostics])
        ? data[STORAGE_KEYS.diagnostics]
        : {})
    },
    upstreamMetadata: isObject(data[STORAGE_KEYS.upstreamMetadata])
      ? data[STORAGE_KEYS.upstreamMetadata]
      : {}
  };
}

function enqueue(task) {
  const run = operationQueue.then(task, task);
  operationQueue = run.catch(() => undefined);
  return run;
}

async function reconcileAll(reason = 'manual') {
  return enqueue(() => reconcileAllNow(reason));
}

async function reconcileAllNow(reason = 'manual', existingState = null) {
  const state = existingState || await readState();
  const dynamicCoverage = await reconcileDynamicRules(state);
  const staticCoverage = {
    architecture: 'compressed-dynamic-only',
    enabledRulesets: [],
    coreRulesets: [],
    extendedRulesets: [],
    enabledRuleCount: state.settings.enabled ? dynamicCoverage.filterRuleCount : 0,
    disabledOptionalRuleCount: 0,
    availableStaticRuleCount: 0,
    tier: state.settings.enabled ? 'compressed-native-core' : 'paused'
  };
  await configureBadge(state.settings);
  await patchDiagnostics({
    lastReconciledAt: new Date().toISOString(),
    lastReconcileReason: reason,
    staticCoverage,
    dynamicCoverage,
    rejectedRuleIds: dynamicCoverage.rejectedRuleIds
  });
  return publicState(await readState());
}

async function reconcileDynamicRules(state) {
  const allCurrent = await chrome.declarativeNetRequest.getDynamicRules();
  const managedCurrent = allCurrent.filter(rule => isManagedDynamicId(rule.id));
  const unmanagedCount = allCurrent.length - managedCurrent.length;
  const policyRules = buildPolicyRules(state);
  const compatibilityRules = buildCompatibilityRules(state);
  const dynamicLimit = getSafeDynamicRuleLimit();
  const supplementBudget = Math.max(0, dynamicLimit - unmanagedCount - policyRules.length - compatibilityRules.length - APP.dynamicSafetyHeadroom);
  const supplementSelection = await selectSupplementRules(state.settings, supplementBudget);
  const desired = [...policyRules, ...compatibilityRules, ...supplementSelection.rules];
  let rejectedRuleIds = [];
  if (!sameRules(managedCurrent, desired)) {
    rejectedRuleIds = await replaceManagedDynamicRules(managedCurrent.map(rule => rule.id), desired);
  }
  const installedRuleCount = desired.length - rejectedRuleIds.length;
  return {
    architecture: 'compressed-dynamic-only',
    limit: dynamicLimit,
    unmanagedRuleCount: unmanagedCount,
    policyRuleCount: policyRules.length,
    compatibilityRuleCount: compatibilityRules.length,
    filterRuleCount: Math.max(0, installedRuleCount - policyRules.length - compatibilityRules.length),
    totalManagedRuleCount: installedRuleCount,
    supplementRuleCount: supplementSelection.rules.length - rejectedRuleIds.length,
    selectedSupplements: supplementSelection.selected,
    truncated: supplementSelection.truncated,
    rejectedRuleIds,
    fallback: rejectedRuleIds.length ? 'resilient-install' : null
  };
}

async function replaceManagedDynamicRules(removeRuleIds, desiredRules) {
  try {
    await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds, addRules: desiredRules });
    return [];
  } catch {
    if (removeRuleIds.length) await chrome.declarativeNetRequest.updateDynamicRules({ removeRuleIds });
    const rejected = [];
    for (let offset = 0; offset < desiredRules.length; offset += APP.installBatchSize) {
      await addRuleBatchResiliently(desiredRules.slice(offset, offset + APP.installBatchSize), rejected);
    }
    return rejected;
  }
}

async function addRuleBatchResiliently(rules, rejected) {
  if (!rules.length) return;
  try {
    await chrome.declarativeNetRequest.updateDynamicRules({ addRules: rules });
    return;
  } catch {
    if (rules.length === 1) {
      rejected.push(rules[0].id);
      return;
    }
  }
  const middle = Math.ceil(rules.length / 2);
  await addRuleBatchResiliently(rules.slice(0, middle), rejected);
  await addRuleBatchResiliently(rules.slice(middle), rejected);
}

async function selectSupplementRules(settings, budget) {
  const selected = [];
  const rules = [];
  let truncated = false;
  for (const definition of DYNAMIC_SUPPLEMENTS) {
    if (!definition.enabled(settings)) continue;
    let packagedRules;
    try {
      packagedRules = await loadSupplement(definition);
    } catch (error) {
      continue;
    }
    const remaining = Math.max(0, budget - rules.length);
    if (remaining === 0) { truncated = true; break; }
    const accepted = packagedRules.slice(0, remaining);
    rules.push(...accepted);
    selected.push({ name: definition.name, loaded: accepted.length, packaged: packagedRules.length });
    if (accepted.length < packagedRules.length) { truncated = true; break; }
  }
  return { rules, selected, truncated };
}

function getSafeDynamicRuleLimit() {
  const direct = chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_RULES;
  if (Number.isInteger(direct) && direct > 0) return direct;
  const combined = chrome.declarativeNetRequest.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES;
  if (Number.isInteger(combined) && combined > 0) return combined;
  return 30_000;
}

function isManagedDynamicId(id) {
  return (
    (id >= RULE_IDS.policyMin && id <= RULE_IDS.policyMax) ||
    (id >= RULE_IDS.supplementMin && id <= RULE_IDS.supplementMax) ||
    (id >= RULE_IDS.compatibilityMin && id <= RULE_IDS.compatibilityMax)
  );
}

function buildPolicyRules(state) {
  if (!state.settings.enabled) return [];
  const rules = [];

  state.allowlist.forEach((domain, index) => {
    rules.push({
      id: RULE_IDS.allowAllStart + index,
      priority: 1_000_000,
      action: { type: 'allowAllRequests' },
      condition: {
        urlFilter: `||${domain}^`,
        resourceTypes: ['main_frame', 'sub_frame']
      }
    });
    rules.push({
      id: RULE_IDS.allowRequestStart + index,
      priority: 1_000_000,
      action: { type: 'allow' },
      condition: {
        initiatorDomains: [domain],
        resourceTypes: RESOURCE_TYPES
      }
    });
  });

  if (state.settings.enabled) {
    state.customBlocklist.forEach((domain, index) => {
      rules.push({
        id: RULE_IDS.customBlockStart + index,
        priority: 900_000,
        action: { type: 'block' },
        condition: {
          urlFilter: `||${domain}^`,
          resourceTypes: RESOURCE_TYPES
        }
      });
    });
  }

  return rules;
}

function buildCompatibilityRules(state) {
  if (!state.settings.enabled) return [];

  const rules = [];
  let id = RULE_IDS.compatibilityMin;
  const nextId = () => {
    if (id > RULE_IDS.compatibilityMax) {
      throw new GraxError('COMPATIBILITY_RULE_LIMIT', 'Compatibility rule range exhausted.');
    }
    return id++;
  };

  // Vendor-owned dashboards and first-party SDK consoles stay native. This
  // priority remains below explicit user custom blocks but above packaged lists.
  rules.push({
    id: nextId(),
    priority: 850_000,
    action: { type: 'allow' },
    condition: {
      requestDomains: [...TELEMETRY_COMPATIBILITY_DOMAINS],
      domainType: 'firstParty',
      resourceTypes: RESOURCE_TYPES
    }
  });

  // Customer-support and product-onboarding SDKs contain functional UI and
  // cross-frame handshakes. Let those shells load, while pure analytics vendors
  // continue through neutral 200-response mocks.
  rules.push({
    id: nextId(),
    priority: 820_000,
    action: { type: 'allow' },
    condition: {
      requestDomains: [...FUNCTIONAL_SDK_DOMAINS],
      domainType: 'thirdParty',
      resourceTypes: RESOURCE_TYPES
    }
  });

  const redirected = [
    { types: ['script'], path: '/mocks/telemetry.js' },
    { types: ['stylesheet'], path: '/mocks/empty.css' },
    { types: ['image'], path: '/mocks/pixel.gif' },
    { types: ['xmlhttprequest', 'ping', 'other'], path: '/mocks/success.json' }
  ];

  for (const entry of redirected) {
    rules.push({
      id: nextId(),
      priority: 700_000,
      action: { type: 'redirect', redirect: { extensionPath: entry.path } },
      condition: {
        requestDomains: [...PURE_TELEMETRY_DOMAINS],
        domainType: 'thirdParty',
        resourceTypes: entry.types
      }
    });
  }

  // Frame, media, font, and streaming transport bodies cannot be represented by
  // a universal synthetic payload without risking lifecycle or decoder errors.
  rules.push({
    id: nextId(),
    priority: 700_000,
    action: { type: 'allow' },
    condition: {
      requestDomains: [...PURE_TELEMETRY_DOMAINS],
      domainType: 'thirdParty',
      resourceTypes: [
        'main_frame', 'sub_frame', 'font', 'object', 'media', 'csp_report',
        'websocket', 'webtransport', 'webbundle'
      ]
    }
  });

  // WebSockets and WebTransport are application infrastructure. Explicit user
  // custom blocks retain the higher priority and therefore still win.
  rules.push({
    id: nextId(),
    priority: 850_000,
    action: { type: 'allow' },
    condition: { resourceTypes: ['websocket', 'webtransport'] }
  });

  // Authentication, payment, WAF, and CAPTCHA infrastructure is never mocked.
  rules.push({
    id: nextId(),
    priority: 980_000,
    action: { type: 'allow' },
    condition: {
      requestDomains: [...STRUCTURAL_AUTH_DOMAINS],
      resourceTypes: RESOURCE_TYPES
    }
  });

  // Grok's application shell, API streams, workers, code chunks, media, and
  // sockets span grok.com plus the X/Twitter/Twimg domain family. Treat those
  // hosts as one first-party entity only when the request is initiated by that
  // same entity. This outranks all packaged lists and neutral telemetry mocks,
  // but does not relax Filma24 or grant unrelated websites access to the family.
  rules.push({
    id: nextId(),
    priority: 975_000,
    action: { type: 'allow' },
    condition: {
      initiatorDomains: [...GROK_TRUSTED_ENTITY_DOMAINS],
      requestDomains: [...GROK_TRUSTED_ENTITY_DOMAINS],
      resourceTypes: RESOURCE_TYPES
    }
  });

  // Claude and its Anthropic-owned application infrastructure are kept native.
  // This prevents challenge loops and framework boot failures without granting
  // unrelated initiators a broad bypass.
  rules.push({
    id: nextId(),
    priority: 976_000,
    action: { type: 'allow' },
    condition: {
      initiatorDomains: [...CLAUDE_TRUSTED_ENTITY_DOMAINS],
      requestDomains: [...CLAUDE_TRUSTED_ENTITY_DOMAINS],
      resourceTypes: RESOURCE_TYPES
    }
  });

  // DeepSeek's authenticated application shell, static chunks, APIs, workers,
  // media, and streaming transports are one trusted first-party entity.
  rules.push({
    id: nextId(),
    priority: 977_000,
    action: { type: 'allow' },
    condition: {
      initiatorDomains: [...DEEPSEEK_TRUSTED_ENTITY_DOMAINS],
      requestDomains: [...DEEPSEEK_TRUSTED_ENTITY_DOMAINS],
      resourceTypes: RESOURCE_TYPES
    }
  });

  const pushFirstPartyMockRules = (filters, priority) => {
    const payloads = [
      { types: ['script'], path: '/mocks/telemetry.js' },
      { types: ['stylesheet'], path: '/mocks/empty.css' },
      { types: ['image'], path: '/mocks/pixel.gif' },
      { types: ['xmlhttprequest', 'ping', 'other'], path: '/mocks/success.json' },
      { types: ['sub_frame'], path: '/mocks/blank.html' }
    ];

    for (const urlFilter of filters) {
      for (const payload of payloads) {
        rules.push({
          id: nextId(),
          priority,
          action: { type: 'redirect', redirect: { extensionPath: payload.path } },
          condition: {
            urlFilter,
            domainType: 'firstParty',
            excludedInitiatorDomains: [...FIRST_PARTY_COMPATIBILITY_EXCLUSIONS],
            excludedRequestDomains: [...POPUNDER_EXCLUDED_REQUEST_DOMAINS],
            resourceTypes: payload.types
          }
        });
      }
    }
  };

  // Verified same-site telemetry and ad-loader endpoints receive protocol-valid
  // neutral responses. These rules outrank the broad first-party allowance but
  // remain below explicit user custom blocks and trusted auth/WAF infrastructure.
  pushFirstPartyMockRules(VERIFIED_FIRST_PARTY_TELEMETRY_FILTERS, 880_000);
  pushFirstPartyMockRules(VERIFIED_FIRST_PARTY_AD_FILTERS, 890_000);

  // Same-eTLD+1 application code and data are structural by default. This rule
  // prevents broad third-party filter syntax from hard-dropping React/Next/Vue
  // chunks or authenticated JSON while retaining higher-priority explicit user
  // blocks, verified ad mocks, auth rules, and the untouched Filma24 profile.
  rules.push({
    id: nextId(),
    priority: 840_000,
    action: { type: 'allow' },
    condition: {
      domainType: 'firstParty',
      excludedInitiatorDomains: [...FIRST_PARTY_COMPATIBILITY_EXCLUSIONS],
      excludedRequestDomains: [...POPUNDER_EXCLUDED_REQUEST_DOMAINS],
      resourceTypes: [...FIRST_PARTY_STRUCTURAL_RESOURCE_TYPES]
    }
  });

  // Preserve framework code-splitting assets globally without opening a bypass
  // for known pop-under delivery matrices. User custom blocks remain stronger.
  for (const urlFilter of STRUCTURAL_ASSET_FILTERS) {
    rules.push({
      id: nextId(),
      priority: 860_000,
      action: { type: 'allow' },
      condition: {
        urlFilter,
        excludedRequestDomains: [...POPUNDER_EXCLUDED_REQUEST_DOMAINS],
        resourceTypes: ['script', 'stylesheet']
      }
    });
  }

  // ESM bundles cannot be safely replaced by a generic script because callers
  // may require named exports. Pass module-shaped assets through natively.
  for (const urlFilter of STRUCTURAL_MODULE_FILTERS) {
    rules.push({
      id: nextId(),
      priority: 860_000,
      action: { type: 'allow' },
      condition: {
        urlFilter,
        excludedRequestDomains: [...POPUNDER_EXCLUDED_REQUEST_DOMAINS],
        resourceTypes: ['script']
      }
    });
  }

  // First-party EventSource/SSE and application subscription endpoints are
  // structural state channels, not third-party advertising beacons.
  for (const urlFilter of FIRST_PARTY_REALTIME_FILTERS) {
    rules.push({
      id: nextId(),
      priority: 860_000,
      action: { type: 'allow' },
      condition: {
        urlFilter,
        domainType: 'firstParty',
        excludedRequestDomains: [...POPUNDER_EXCLUDED_REQUEST_DOMAINS],
        resourceTypes: ['xmlhttprequest', 'other']
      }
    });
  }

  // Generic first-party auth/session routes are allowed only when they remain
  // first-party, so matching path names on ad matrices do not gain a bypass.
  for (const urlFilter of FIRST_PARTY_AUTH_PATH_FILTERS) {
    rules.push({
      id: nextId(),
      priority: 870_000,
      action: { type: 'allow' },
      condition: {
        urlFilter,
        domainType: 'firstParty',
        excludedRequestDomains: [...POPUNDER_EXCLUDED_REQUEST_DOMAINS],
        resourceTypes: ['main_frame', 'sub_frame', 'script', 'xmlhttprequest', 'other']
      }
    });
  }

  return rules;
}

function sameRules(left, right) {
  if (left.length !== right.length) return false;
  const normalize = rules => [...rules]
    .sort((a, b) => a.id - b.id)
    .map(rule => JSON.stringify(rule));
  const a = normalize(left);
  const b = normalize(right);
  return a.every((value, index) => value === b[index]);
}

async function configureBadge(settings) {
  await chrome.action.setBadgeBackgroundColor({ color: APP.badgeColor });
  await chrome.action.setBadgeTextColor?.({ color: '#031014' });
  await chrome.declarativeNetRequest.setExtensionActionOptions({
    displayActionCountAsBadgeText: Boolean(settings.enabled && settings.showBadge)
  });
  if (!settings.enabled || !settings.showBadge) {
    await chrome.action.setBadgeText({ text: '' });
  }
  await chrome.action.setTitle({
    title: settings.enabled
      ? `${APP.name} — Protection active`
      : `${APP.name} — Protection paused`
  });
}

async function handleMessage(message, sender) {
  if (!isObject(message) || typeof message.type !== 'string') {
    throw new GraxError('INVALID_MESSAGE', 'Malformed extension message.');
  }
  if (message.type !== 'GRAX_GET_STATE') assertTrustedUi(sender);

  switch (message.type) {
    case 'GRAX_GET_STATE':
      return getState(sender, message.hostname);
    case 'GRAX_SET_ENABLED':
      return setEnabled(message.enabled);
    case 'GRAX_SET_SITE_ENABLED':
      return setSiteEnabled(message.hostname, message.enabled, message.tabId);
    case 'GRAX_UPDATE_SETTINGS':
      return updateSettings(message.patch);
    case 'GRAX_ADD_CUSTOM_BLOCK_DOMAIN':
      return addCustomBlock(message.hostname);
    case 'GRAX_REMOVE_CUSTOM_BLOCK_DOMAIN':
      return removeCustomBlock(message.hostname);
    case 'GRAX_FORCE_RECONCILE':
      return reconcileAll('manual-message');
    case 'GRAX_CHECK_LIST_UPDATES':
      return checkForUpdates(true);
    default:
      throw new GraxError(
        'UNKNOWN_MESSAGE',
        `Unsupported message type: ${message.type}`
      );
  }
}

function assertTrustedUi(sender) {
  const extensionRoot = chrome.runtime.getURL('');
  const senderUrl = [sender?.url, sender?.tab?.url]
    .find(value => typeof value === 'string' && value.startsWith(extensionRoot));

  if (sender?.id !== chrome.runtime.id || !senderUrl) {
    throw new GraxError('FORBIDDEN', 'This action is restricted to Grax pages.');
  }

  let path;
  try {
    path = new URL(senderUrl).pathname;
  } catch {
    throw new GraxError('FORBIDDEN', 'This action is restricted to Grax pages.');
  }
  if (!INTERNAL_UI_PATHS.has(path)) {
    throw new GraxError('FORBIDDEN', 'This action is restricted to Grax pages.');
  }
}

async function getState(sender, explicitHostname) {
  const state = await readState();
  const metadata = await loadMetadata();
  const hostname = normalizeHostname(
    explicitHostname ?? safeHostname(sender?.url) ?? safeHostname(sender?.tab?.url)
  );
  const enabledRulesets = [];
  const dynamicRules = await chrome.declarativeNetRequest.getDynamicRules();

  return {
    ...publicState(state),
    site: hostname
      ? {
          hostname,
          enabled: !state.allowlist.includes(hostname),
          allowlisted: state.allowlist.includes(hostname)
        }
      : null,
    filterMetadata: metadata,
    runtime: {
      enabledRulesets,
      dynamicRuleCount: dynamicRules.length,
      staticCoverage: state.diagnostics.staticCoverage,
      dynamicCoverage: state.diagnostics.dynamicCoverage
    }
  };
}

async function setEnabled(enabled) {
  if (typeof enabled !== 'boolean') {
    throw new GraxError('INVALID_VALUE', 'enabled must be boolean.');
  }
  const state = await readState();
  await chrome.storage.local.set({
    [STORAGE_KEYS.settings]: sanitizeSettings({ ...state.settings, enabled })
  });
  await synchronizeRuntimeLifecycle('global-toggle', {
    injectExisting: enabled,
    forceBroadcast: true
  });
  return { enabled };
}

async function setSiteEnabled(input, enabled, tabId = null) {
  if (typeof enabled !== 'boolean') {
    throw new GraxError('INVALID_VALUE', 'enabled must be boolean.');
  }
  const hostname = requireHostname(input);
  const normalizedTabId = Number.isInteger(tabId) && tabId >= 0 ? tabId : null;
  const state = await readState();
  const allowlist = new Set(state.allowlist);

  if (enabled) {
    allowlist.delete(hostname);
  } else {
    if (allowlist.size >= APP.maxAllowlistedDomains) {
      throw new GraxError('ALLOWLIST_LIMIT', 'Trusted-site limit reached.');
    }
    allowlist.add(hostname);
  }

  await chrome.storage.local.set({
    [STORAGE_KEYS.allowlist]: [...allowlist].sort()
  });

  if (!enabled && normalizedTabId !== null) {
    await setTabBypass(normalizedTabId, hostname);
    await sendTabState(normalizedTabId, hostname, false);
  }

  await reconcileAll('site-toggle');

  if (enabled) {
    await clearTabBypassesForHostname(hostname);
  }

  if (normalizedTabId !== null) {
    await sendTabState(normalizedTabId, hostname, enabled);
  }
  await broadcast({ type: 'GRAX_SITE_STATE_CHANGED', hostname, enabled });
  return { hostname, enabled, allowlisted: !enabled, tabId: normalizedTabId };
}

async function readTabBypasses() {
  if (!chrome.storage?.session) return {};
  try {
    const data = await chrome.storage.session.get(SESSION_KEYS.tabBypasses);
    return isObject(data?.[SESSION_KEYS.tabBypasses])
      ? data[SESSION_KEYS.tabBypasses]
      : {};
  } catch {
    return {};
  }
}

async function writeTabBypasses(value) {
  if (!chrome.storage?.session) return;
  try {
    await chrome.storage.session.set({ [SESSION_KEYS.tabBypasses]: value });
  } catch {}
}

async function allocateTabBypassRuleIds(currentMap) {
  const used = new Set();
  for (const value of Object.values(currentMap)) {
    if (!isObject(value) || !Array.isArray(value.ruleIds)) continue;
    for (const id of value.ruleIds) if (Number.isInteger(id)) used.add(id);
  }
  try {
    const currentRules = await chrome.declarativeNetRequest.getSessionRules();
    for (const rule of currentRules) if (Number.isInteger(rule.id)) used.add(rule.id);
  } catch {}

  for (let base = RULE_IDS.sessionBypassMin; base + 1 <= RULE_IDS.sessionBypassMax; base += 2) {
    if (!used.has(base) && !used.has(base + 1)) return [base, base + 1];
  }
  throw new GraxError('SESSION_RULE_LIMIT', 'No free tab-bypass rule slots are available.');
}

async function setTabBypass(tabId, hostname) {
  if (!Number.isInteger(tabId) || tabId < 0 || !hostname) return;
  const map = await readTabBypasses();
  const key = String(tabId);
  const previous = isObject(map[key]) ? map[key] : null;
  const ruleIds = previous?.ruleIds?.length === 2
    ? previous.ruleIds
    : await allocateTabBypassRuleIds(map);

  const removeRuleIds = Array.isArray(previous?.ruleIds) ? previous.ruleIds : [];
  const addRules = [
    {
      id: ruleIds[0],
      priority: 1_100_000,
      action: { type: 'allowAllRequests' },
      condition: { tabIds: [tabId], resourceTypes: ['main_frame', 'sub_frame'] }
    },
    {
      id: ruleIds[1],
      priority: 1_100_000,
      action: { type: 'allow' },
      condition: { tabIds: [tabId], resourceTypes: RESOURCE_TYPES }
    }
  ];

  await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds, addRules });
  map[key] = { hostname, ruleIds, updatedAt: Date.now() };
  await writeTabBypasses(map);
}

async function clearTabBypass(tabId) {
  if (!Number.isInteger(tabId) || tabId < 0) return;
  const map = await readTabBypasses();
  const key = String(tabId);
  const entry = isObject(map[key]) ? map[key] : null;
  if (!entry) return;
  const removeRuleIds = Array.isArray(entry.ruleIds) ? entry.ruleIds.filter(Number.isInteger) : [];
  if (removeRuleIds.length) {
    await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds }).catch(() => undefined);
  }
  delete map[key];
  await writeTabBypasses(map);
}

async function clearTabBypassesForHostname(hostname) {
  const map = await readTabBypasses();
  const removeRuleIds = [];
  let changed = false;
  for (const [key, entry] of Object.entries(map)) {
    if (!isObject(entry) || entry.hostname !== hostname) continue;
    if (Array.isArray(entry.ruleIds)) removeRuleIds.push(...entry.ruleIds.filter(Number.isInteger));
    delete map[key];
    changed = true;
  }
  if (removeRuleIds.length) {
    await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds }).catch(() => undefined);
  }
  if (changed) await writeTabBypasses(map);
}

async function reconcileTabBypassNavigation(tabId, nextUrl) {
  const nextHostname = safeHostname(nextUrl);
  if (!nextHostname) return clearTabBypass(tabId);
  const map = await readTabBypasses();
  const entry = map[String(tabId)];
  if (!isObject(entry) || typeof entry.hostname !== 'string') return;
  if (nextHostname === entry.hostname || nextHostname.endsWith(`.${entry.hostname}`)) return;
  await clearTabBypass(tabId);
}

async function pruneTabBypasses() {
  const map = await readTabBypasses();
  const keys = Object.keys(map);
  if (!keys.length) return;
  let tabs = [];
  try { tabs = await chrome.tabs.query({}); } catch {}
  const live = new Set(tabs.filter(tab => Number.isInteger(tab.id)).map(tab => String(tab.id)));
  const removeRuleIds = [];
  let changed = false;
  for (const key of keys) {
    if (live.has(key)) continue;
    const entry = map[key];
    if (Array.isArray(entry?.ruleIds)) removeRuleIds.push(...entry.ruleIds.filter(Number.isInteger));
    delete map[key];
    changed = true;
  }
  if (removeRuleIds.length) {
    await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds }).catch(() => undefined);
  }
  if (changed) await writeTabBypasses(map);
}

async function sendTabState(tabId, hostname, enabled) {
  try {
    await chrome.tabs.sendMessage(tabId, {
      type: 'GRAX_SITE_STATE_CHANGED',
      hostname,
      enabled
    });
  } catch {}
}

async function updateSettings(patch) {
  if (!isObject(patch)) {
    throw new GraxError('INVALID_SETTINGS', 'Settings patch must be an object.');
  }

  const state = await readState();
  const settings = sanitizeSettings({
    ...state.settings,
    ...(typeof patch.enabled === 'boolean' ? { enabled: patch.enabled } : {}),
    ...(typeof patch.cosmeticFiltering === 'boolean'
      ? { cosmeticFiltering: patch.cosmeticFiltering }
      : {}),
    ...(typeof patch.showBadge === 'boolean'
      ? { showBadge: patch.showBadge }
      : {}),
    ...(typeof patch.automaticUpdateChecks === 'boolean'
      ? { automaticUpdateChecks: patch.automaticUpdateChecks }
      : {}),
    lists: {
      ...state.settings.lists,
      ...(isObject(patch.lists) && typeof patch.lists.easylist === 'boolean'
        ? { easylist: patch.lists.easylist }
        : {}),
      ...(isObject(patch.lists) && typeof patch.lists.easyprivacy === 'boolean'
        ? { easyprivacy: patch.lists.easyprivacy }
        : {})
    }
  });

  await chrome.storage.local.set({ [STORAGE_KEYS.settings]: settings });
  await reconcileAll('settings-update');
  await broadcast({
    type: 'GRAX_SETTINGS_CHANGED',
    settings: publicSettings(settings)
  });
  return publicSettings(settings);
}

async function addCustomBlock(input) {
  const hostname = requireHostname(input);
  const state = await readState();
  const blocklist = new Set(state.customBlocklist);
  if (blocklist.size >= APP.maxCustomBlockedDomains) {
    throw new GraxError('BLOCKLIST_LIMIT', 'Custom-block limit reached.');
  }
  blocklist.add(hostname);
  await chrome.storage.local.set({
    [STORAGE_KEYS.customBlocklist]: [...blocklist].sort()
  });
  await reconcileAll('custom-add');
  return { hostname, blocked: true };
}

async function removeCustomBlock(input) {
  const hostname = requireHostname(input);
  const state = await readState();
  const blocklist = new Set(state.customBlocklist);
  blocklist.delete(hostname);
  await chrome.storage.local.set({
    [STORAGE_KEYS.customBlocklist]: [...blocklist].sort()
  });
  await reconcileAll('custom-remove');
  return { hostname, blocked: false };
}

async function scheduledUpdateCheck() {
  const state = await readState();
  if (!state.settings.enabled || !state.settings.automaticUpdateChecks) return;
  await checkForUpdates(false).catch(error =>
    patchDiagnostics({ lastUpdateCheckError: publicError(error) })
  );
}

async function checkForUpdates(userInitiated) {
  const state = await readState();
  const checkedAt = new Date().toISOString();
  const next = {};
  let changed = false;

  for (const [id, url] of Object.entries(UPSTREAM_SOURCES)) {
    const previous = isObject(state.upstreamMetadata[id])
      ? state.upstreamMetadata[id]
      : {};
    const headers = {};
    if (previous.etag) headers['If-None-Match'] = previous.etag;
    if (previous.lastModified) {
      headers['If-Modified-Since'] = previous.lastModified;
    }

    const response = await timeoutFetch(url, {
      method: 'HEAD',
      cache: 'no-store',
      credentials: 'omit',
      headers
    });

    if (!response.ok && response.status !== 304) {
      throw new GraxError(
        'UPDATE_CHECK_FAILED',
        `${id} returned HTTP ${response.status}.`
      );
    }

    const etag = response.headers.get('etag') ?? previous.etag ?? null;
    const lastModified =
      response.headers.get('last-modified') ?? previous.lastModified ?? null;
    const itemChanged =
      response.status !== 304 &&
      Boolean(previous.checkedAt) &&
      ((etag && previous.etag && etag !== previous.etag) ||
        (lastModified &&
          previous.lastModified &&
          lastModified !== previous.lastModified));

    changed ||= itemChanged;
    next[id] = {
      url,
      etag,
      lastModified,
      changed: itemChanged,
      checkedAt
    };
  }

  await chrome.storage.local.set({ [STORAGE_KEYS.upstreamMetadata]: next });
  await patchDiagnostics({
    lastUpdateCheckAt: checkedAt,
    lastUpdateCheckError: null,
    updateAvailable: changed
  });

  if (
    (changed || userInitiated) &&
    typeof chrome.runtime.requestUpdateCheck === 'function'
  ) {
    await chrome.runtime.requestUpdateCheck().catch(() => undefined);
  }

  return { checkedAt, updateAvailable: changed, lists: next };
}

async function timeoutFetch(url, init, timeoutMs = 15_000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    return await fetch(url, { ...init, signal: controller.signal });
  } catch (error) {
    if (error?.name === 'AbortError') {
      throw new GraxError('NETWORK_TIMEOUT', 'The list check timed out.');
    }
    throw error;
  } finally {
    clearTimeout(timer);
  }
}

async function broadcast(message) {
  let tabs;
  try {
    tabs = await chrome.tabs.query({});
  } catch {
    return;
  }
  await Promise.allSettled(
    tabs
      .filter(tab => Number.isInteger(tab.id))
      .map(tab => chrome.tabs.sendMessage(tab.id, message))
  );
}

function sanitizeSettings(value) {
  const input = isObject(value) ? value : {};
  const lists = isObject(input.lists) ? input.lists : {};
  return {
    enabled:
      typeof input.enabled === 'boolean'
        ? input.enabled
        : DEFAULT_SETTINGS.enabled,
    cosmeticFiltering:
      typeof input.cosmeticFiltering === 'boolean'
        ? input.cosmeticFiltering
        : DEFAULT_SETTINGS.cosmeticFiltering,
    showBadge:
      typeof input.showBadge === 'boolean'
        ? input.showBadge
        : DEFAULT_SETTINGS.showBadge,
    automaticUpdateChecks:
      typeof input.automaticUpdateChecks === 'boolean'
        ? input.automaticUpdateChecks
        : DEFAULT_SETTINGS.automaticUpdateChecks,
    lists: {
      easylist: typeof lists.easylist === 'boolean' ? lists.easylist : true,
      easyprivacy:
        typeof lists.easyprivacy === 'boolean' ? lists.easyprivacy : true
    }
  };
}

function sanitizeDomains(value, limit) {
  if (!Array.isArray(value)) return [];
  const output = new Set();
  for (const item of value) {
    const hostname = normalizeHostname(item);
    if (hostname) output.add(hostname);
    if (output.size >= limit) break;
  }
  return [...output].sort();
}

function normalizeHostname(input) {
  if (typeof input !== 'string' || !input.trim()) return null;
  let hostname;
  try {
    hostname = new URL(
      input.includes('://') ? input : `https://${input}`
    ).hostname.toLowerCase();
  } catch {
    return null;
  }

  hostname = hostname.replace(/^\.+|\.+$/g, '');
  if (
    !hostname ||
    hostname.length > 253 ||
    hostname === 'localhost' ||
    hostname.endsWith('.localhost') ||
    hostname.includes('..')
  ) {
    return null;
  }
  if (
    hostname.split('.').some(label =>
      !label ||
      label.length > 63 ||
      label.startsWith('-') ||
      label.endsWith('-') ||
      !/^[a-z0-9-]+$/.test(label)
    )
  ) {
    return null;
  }
  return hostname;
}

function requireHostname(value) {
  const hostname = normalizeHostname(value);
  if (!hostname) {
    throw new GraxError('INVALID_HOSTNAME', 'A valid hostname is required.');
  }
  return hostname;
}

function safeHostname(value) {
  if (typeof value !== 'string') return null;
  try {
    const url = new URL(value);
    return ['http:', 'https:'].includes(url.protocol) ? url.hostname : null;
  } catch {
    return null;
  }
}

function publicSettings(settings) {
  return { ...settings, lists: { ...settings.lists } };
}

function publicState(state) {
  return {
    settings: publicSettings(state.settings),
    allowlist: [...state.allowlist],
    customBlocklist: [...state.customBlocklist],
    diagnostics: { ...state.diagnostics },
    upstreamMetadata: { ...state.upstreamMetadata }
  };
}

async function patchDiagnostics(patch) {
  const data = await chrome.storage.local.get(STORAGE_KEYS.diagnostics);
  await chrome.storage.local.set({
    [STORAGE_KEYS.diagnostics]: {
      ...DEFAULT_DIAGNOSTICS,
      ...(isObject(data[STORAGE_KEYS.diagnostics])
        ? data[STORAGE_KEYS.diagnostics]
        : {}),
      ...patch
    }
  });
}

function isObject(value) {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}

function publicError(error) {
  return error instanceof GraxError
    ? { code: error.code, message: error.message }
    : { code: 'INTERNAL_ERROR', message: 'Grax encountered an unexpected error.' };
}

class GraxError extends Error {
  constructor(code, message) {
    super(message);
    this.name = 'GraxError';
    this.code = code;
  }
}

void initializeStorage()
  .then(pruneTabBypasses)
  .then(() => synchronizeRuntimeLifecycle('worker-bootstrap', { injectExisting: false, forceBroadcast: false }))
  .catch(async error => {
    await patchDiagnostics({
      lastReconcileReason: 'worker-bootstrap-failed',
      lastUpdateCheckError: publicError(error)
    }).catch(() => undefined);
  });
