function getPageKey(url) {
  try { const u = new URL(url); return u.origin + u.pathname; }
  catch (e) { return url; }
}

function getFolderPatterns(folder) {
  if (!folder) return [];
  if (Array.isArray(folder.urlPatterns)) {
    return folder.urlPatterns.map(p => (p || '').trim()).filter(Boolean);
  }
  const legacy = (folder.urlPattern || '').trim();
  return legacy ? [legacy] : [];
}

function folderMatchesUrl(folder, url) {
  const patterns = getFolderPatterns(folder);
  if (patterns.length === 0) return false;
  return patterns.some(p => urlMatches(p, url));
}

function urlMatches(pattern, url) {
  if (!pattern) return false;
  try {
    const trimmed = pattern.trim();
    if (trimmed.includes('*')) {
      const regex = new RegExp(
        '^' + trimmed.split('*').map(s => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$'
      );
      return regex.test(url);
    }
    return getPageKey(url) === getPageKey(trimmed) || url === trimmed;
  } catch (e) { return false; }
}

function escapeHtml(s) {
  const div = document.createElement('div');
  div.textContent = s || '';
  return div.innerHTML;
}

async function getCurrentTab() {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  return tab;
}

// Discover bars relevant for given URL (matching folder/group/rule scope + having actions).
// Lustrzane do discoverRelevantBars() w content.js. Od v2.13.24 zwraca też bary z enabled=false
// (żeby user mógł je włączyć z popupa) i wystawia pole `enabled` (persistent state).
function discoverRelevantBarsForPopup(url, folders, groups, rules, actionBars, floatingHidden) {
  const matching = folders.filter(f => folderMatchesUrl(f, url));
  // Od v2.13.52: pokazujemy bary też dla hidden/disabled folderów (patrz content.js).
  const hidden = floatingHidden || {};
  const eligibleFolderIds = new Set(
    matching.filter(f => f.active !== false).map(f => f.id)
  );

  const activeByFolder = new Map();
  const activeByGroup  = new Map();
  const activeByRuleId = new Map();
  (rules || []).filter(r => r.active !== false).forEach(r => {
    activeByRuleId.set(r.id, r);
    const g = (groups || []).find(gx => gx.id === r.groupId);
    if (!g) return;
    if (!activeByGroup.has(g.id)) activeByGroup.set(g.id, []);
    activeByGroup.get(g.id).push(r);
    if (!activeByFolder.has(g.folderId)) activeByFolder.set(g.folderId, []);
    activeByFolder.get(g.folderId).push(r);
  });

  const scopedRulesHaveAction = (scopedRules, predicate) =>
    scopedRules.some(r => Array.isArray(r.actions) && r.actions.some(a => a && predicate(a)));

  const out = [];

  (actionBars || []).forEach(bar => {
    let scopedRules = [];
    let cascadeEnabled = false;
    if (bar.parentType === 'folder') {
      if (!eligibleFolderIds.has(bar.parentId)) return;
      scopedRules = activeByFolder.get(bar.parentId) || [];
      const f = (folders || []).find(x => x.id === bar.parentId);
      cascadeEnabled = !!f && !hidden[f.id] && f.active !== false && f.enabled;
    } else if (bar.parentType === 'group') {
      const g = (groups || []).find(gx => gx.id === bar.parentId);
      if (!g || !eligibleFolderIds.has(g.folderId)) return;
      scopedRules = activeByGroup.get(bar.parentId) || [];
      const f = (folders || []).find(x => x.id === g.folderId);
      cascadeEnabled = !!f && !hidden[f.id] && f.active !== false && f.enabled
                       && g.active !== false && g.enabled !== false;
    } else if (bar.parentType === 'rule') {
      const r = activeByRuleId.get(bar.parentId);
      if (!r) return;
      const g = (groups || []).find(gx => gx.id === r.groupId);
      if (!g || !eligibleFolderIds.has(g.folderId)) return;
      scopedRules = [r];
      const f = (folders || []).find(x => x.id === g.folderId);
      cascadeEnabled = !!f && !hidden[f.id] && f.active !== false && f.enabled
                       && g.active !== false && g.enabled !== false
                       && r.active !== false && r.enabled === true;
    }
    if (scopedRules.length === 0) return;
    if (!scopedRulesHaveAction(scopedRules, a => a.barId === bar.id)) return;
    out.push({
      barId: bar.id,
      name: bar.name || '',
      enabled: bar.enabled !== false,
      cascadeEnabled, // od v2.13.50, floatingHidden od v2.13.52
      isLegacy: false,
    });
  });

  matching.forEach(folder => {
    if (!eligibleFolderIds.has(folder.id)) return;
    const hasOwnBar = (actionBars || []).some(b => b.parentType === 'folder' && b.parentId === folder.id);
    if (hasOwnBar) return;
    const folderRules = activeByFolder.get(folder.id) || [];
    if (!scopedRulesHaveAction(folderRules, a => !a.barId)) return;
    out.push({
      barId: '__legacy_' + folder.id,
      name: folder.name || '',
      enabled: folder.showActionsPanel === true,
      cascadeEnabled: !hidden[folder.id] && folder.active !== false && folder.enabled,
      isLegacy: true,
    });
  });

  return out;
}

// Discover CSS panes relevant dla URL — analog do discoverRelevantBarsForPopup ale
// dla pól cssPanes. Zwraca panes ze wszystkimi (włącznie z enabled=false), żeby user
// mógł je włączyć z popup'u. Tak samo lustrzane do discoverRelevantCssPanes w content.js.
// Lustrzane do paneCascadeEnabled w content.js — czy łańcuch rodziców enabled + folder
// nie hidden via floatingHidden (od v2.13.52).
function paneCascadeEnabledForPopup(pane, folders, groups, rules, floatingHidden) {
  const hidden = floatingHidden || {};
  if (pane.parentType === 'folder') {
    const f = (folders || []).find(x => x.id === pane.parentId);
    if (!f || hidden[f.id]) return false;
    return f.active !== false && f.enabled;
  }
  if (pane.parentType === 'group') {
    const g = (groups || []).find(x => x.id === pane.parentId);
    if (!g || g.active === false || g.enabled === false) return false;
    const f = (folders || []).find(x => x.id === g.folderId);
    if (!f || hidden[f.id]) return false;
    return f.active !== false && f.enabled;
  }
  if (pane.parentType === 'rule') {
    const r = (rules || []).find(x => x.id === pane.parentId);
    if (!r || r.active === false || r.enabled !== true) return false;
    const g = (groups || []).find(x => x.id === r.groupId);
    if (!g || g.active === false || g.enabled === false) return false;
    const f = (folders || []).find(x => x.id === g.folderId);
    if (!f || hidden[f.id]) return false;
    return f.active !== false && f.enabled;
  }
  return false;
}

// Discovery okien dla popupa — CSS/JS pane oraz JS/Output Console mają IDENTYCZNĄ
// logikę dopasowania URL (folder→grupa→zestaw, kaskada wzorców); różni je tylko
// kolekcja źródłowa. Jedna funkcja + 4 cienkie aliasy poniżej. Lustrzane do
// discoverRelevant* w content.js. Parametr floatingHidden zachowany w aliasach
// dla zgodności sygnatury (ignored — popup pokazuje też panes hidden folderów).
function discoverRelevantPanesForPopup(url, folders, groups, rules, panes) {
  const matching = folders.filter(f => folderMatchesUrl(f, url));
  const matchingFolderIds = new Set(matching.map(f => f.id));
  const out = [];
  (panes || []).forEach(pane => {
    if (!pane || !pane.id || !pane.parentType || !pane.parentId) return;
    if (pane.parentType === 'folder') {
      if (!matchingFolderIds.has(pane.parentId)) return;
    } else if (pane.parentType === 'group') {
      const g = (groups || []).find(gx => gx.id === pane.parentId);
      if (!g || !matchingFolderIds.has(g.folderId)) return;
      if (Array.isArray(g.urlPatterns) && g.urlPatterns.length > 0) {
        const ok = g.urlPatterns.some(p => urlPatternMatchesUrl(p, url));
        if (!ok) return;
      }
    } else if (pane.parentType === 'rule') {
      const r = (rules || []).find(rx => rx.id === pane.parentId);
      if (!r) return;
      const g = (groups || []).find(gx => gx.id === r.groupId);
      if (!g || !matchingFolderIds.has(g.folderId)) return;
      if (Array.isArray(g.urlPatterns) && g.urlPatterns.length > 0) {
        const ok = g.urlPatterns.some(p => urlPatternMatchesUrl(p, url));
        if (!ok) return;
      }
      if (Array.isArray(r.urlPatterns) && r.urlPatterns.length > 0) {
        const ok = r.urlPatterns.some(p => urlPatternMatchesUrl(p, url));
        if (!ok) return;
      }
    } else {
      return;
    }
    out.push(pane);
  });
  return out;
}
function discoverRelevantCssPanesForPopup(url, folders, groups, rules, cssPanes, floatingHidden) {
  return discoverRelevantPanesForPopup(url, folders, groups, rules, cssPanes);
}

function discoverRelevantJsPanesForPopup(url, folders, groups, rules, jsPanes, floatingHidden) {
  return discoverRelevantPanesForPopup(url, folders, groups, rules, jsPanes);
}

function discoverRelevantOutputConsolesForPopup(url, folders, groups, rules, outputConsoles, floatingHidden) {
  return discoverRelevantPanesForPopup(url, folders, groups, rules, outputConsoles);
}

function discoverRelevantJsConsolesForPopup(url, folders, groups, rules, jsConsoles, floatingHidden) {
  return discoverRelevantPanesForPopup(url, folders, groups, rules, jsConsoles);
}

// Lokalny URL pattern matcher (popup nie ładuje content.js). Spójny semantycznie.
function urlPatternMatchesUrl(pattern, url) {
  if (!pattern) return false;
  const trimmed = String(pattern).trim();
  if (!trimmed) return false;
  if (trimmed.includes('*')) {
    try {
      const regex = new RegExp(
        '^' + trimmed.split('*').map(s => s.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$'
      );
      return regex.test(url);
    } catch (_) { return false; }
  }
  try {
    const a = new URL(trimmed); const b = new URL(url);
    return a.origin === b.origin && a.pathname === b.pathname;
  } catch (_) {
    return url === trimmed;
  }
}

async function load() {
  const tab = await getCurrentTab();
  // Brak aktywnej karty (np. fokus na devtools/oknie bez karty) — getCurrentTab
  // może zwrócić undefined. Bez guardu tab.url rzuca TypeError i cały popup gaśnie.
  if (!tab || !tab.url) {
    document.getElementById('url').textContent = '—';
    return;
  }
  document.getElementById('url').textContent = getPageKey(tab.url);

  const data = await chrome.storage.local.get([
    'folders', 'groups', 'rules', 'actionBars', 'cssPanes', 'jsPanes', 'jsConsoles', 'outputConsoles', 'floatingHidden', 'globalEnabled'
  ]);
  const folders = data.folders || [];
  const groups = data.groups || [];
  const rules = data.rules || [];
  const actionBars = data.actionBars || [];
  const cssPanes = data.cssPanes || [];
  const jsPanes = data.jsPanes || [];
  const jsConsoles = data.jsConsoles || [];
  const outputConsoles = data.outputConsoles || [];
  const hidden = data.floatingHidden || {};
  const globalEnabled = data.globalEnabled !== false;

  // Globalny toggle
  const gtEl = document.getElementById('globalToggle');
  const gtLabel = document.getElementById('gtLabel');
  const gtStatus = document.getElementById('gtStatus');
  // Ikony z JZ_ICONS (od v2.13.16): stateOn = solid circle r=10 / stateGlobalOff = circle z || (pauza).
  // ICON_ON BEZ klasy color — `fill="currentColor"` dziedziczy `color: white` z `.global-toggle` (białe tło
  //   działa tylko gdy tło jest zielone — Inject Green #2D9D53; z color-green ikona zlewałaby się z tłem).
  // ICON_PAUSE z color-red — czerwony pause widoczny na szarym tle off-state.
  const ICON_ON = jzIcon('stateOn');
  const ICON_PAUSE = jzIcon('stateGlobalOff', 'color-red');
  if (globalEnabled) {
    gtEl.classList.remove('off');
    gtLabel.innerHTML = ICON_ON + ' ' + escapeHtml(JZ_I18N.t('popup.toggle.on.label'));
    gtStatus.textContent = JZ_I18N.t('popup.toggle.on.status');
  } else {
    gtEl.classList.add('off');
    gtLabel.innerHTML = ICON_PAUSE + ' ' + escapeHtml(JZ_I18N.t('popup.toggle.off.label'));
    gtStatus.textContent = JZ_I18N.t('popup.toggle.off.status');
  }

  const matching = folders.filter(f => folderMatchesUrl(f, tab.url));

  const list = document.getElementById('list');
  list.innerHTML = '';

  if (matching.length === 0) {
    list.innerHTML = `<div class="empty">${escapeHtml(JZ_I18N.t('popup.empty.title'))}<br>${escapeHtml(JZ_I18N.t('popup.empty.hint'))}</div>`;
  } else {
    matching.forEach(folder => {
      // Hierarchia: folder → grupy → zestawy. Pokazujemy tylko zestawy aktywne w katalogu (active=true).
      const folderGroups = groups.filter(g => g.folderId === folder.id);
      const DEFAULT_GROUP_NAMES = new Set(['Domyślna', 'Default']);
      const onlyDefaultGroup = folderGroups.length === 1
        && (DEFAULT_GROUP_NAMES.has(folderGroups[0].name) || !folderGroups[0].name);

      // Od v2.13.52: hidden folder → checkbox folder/group/rule pokazują się jako disabled
      // + unchecked. Wizualne potwierdzenie że folder jest hidden (bary/panes też pokażą
      // disabled state via cascadeEnabled).
      const isHidden = !!hidden[folder.id];
      const dis = isHidden ? 'disabled' : '';

      let groupsHtml = '';
      let totalRulesShown = 0;
      folderGroups.forEach(group => {
        const groupRules = rules.filter(r => r.groupId === group.id && r.active === true);
        if (groupRules.length === 0) return;
        totalRulesShown += groupRules.length;
        const groupChecked = !isHidden && group.enabled !== false;
        groupsHtml += `
          <div class="group-block">
            ${onlyDefaultGroup ? '' : `
              <label class="group-row${isHidden ? ' row-cascade-off' : ''}">
                <input type="checkbox" data-group-id="${group.id}" ${groupChecked ? 'checked' : ''} ${dis}>
                <span class="group-name">${escapeHtml(group.name || JZ_I18N.t('popup.fallback.groupName'))}</span>
              </label>
            `}
            ${groupRules.map(r => {
              const ruleChecked = !isHidden && r.enabled;
              return `
              <label class="rule-row${onlyDefaultGroup ? '' : ' rule-row-grouped'}${isHidden ? ' row-cascade-off' : ''}">
                <input type="checkbox" data-rule-id="${r.id}" ${ruleChecked ? 'checked' : ''} ${dis}>
                <span class="rule-name">${escapeHtml(r.name || JZ_I18N.t('popup.fallback.unnamed'))}</span>
              </label>
            `;}).join('')}
          </div>
        `;
      });

      // Toggle button "ukryj/przywróć przycisk dla tej strony" (od v2.13.49 toggle behavior —
      // jeden klik hide, drugi klik restore. Wcześniej był osobny restore link niżej w popup,
      // user'om się to mieszało). Ikona zmienia się EyeOff (visible) ↔ Eye (hidden).
      const HIDE_ICON = '<svg class="jz-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
      const SHOW_ICON = '<svg class="jz-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
      const toggleIcon = isHidden ? SHOW_ICON : HIDE_ICON;
      const toggleTitle = isHidden ? JZ_I18N.t('popup.showForPage') : JZ_I18N.t('popup.hideForPage');
      const div = document.createElement('div');
      div.className = 'folder-block' + (isHidden ? ' folder-block-hidden' : '');
      const folderChecked = !isHidden && folder.enabled;
      div.innerHTML = `
        <label class="folder-row${isHidden ? ' row-cascade-off' : ''}">
          <input type="checkbox" data-folder-id="${folder.id}" ${folderChecked ? 'checked' : ''} ${dis}>
          <span class="folder-name">${escapeHtml(folder.name || JZ_I18N.t('popup.fallback.unnamed'))}</span>
          <button class="folder-hide-btn" data-hide-folder-id="${folder.id}" data-currently-hidden="${isHidden ? '1' : '0'}" title="${escapeHtml(toggleTitle)}">${toggleIcon}</button>
        </label>
        <div class="rules-list">
          ${totalRulesShown > 0 ? groupsHtml : `<div class="empty">${escapeHtml(JZ_I18N.t('popup.empty.folderRules'))}</div>`}
        </div>
      `;
      list.appendChild(div);
    });
  }

  // Sekcja "Paski akcji" (od v2.13.22) — checkbox toggle hide per bar dla bieżącego okna.
  const barsArea = document.getElementById('bars-area');
  barsArea.innerHTML = '';
  const relevantBars = discoverRelevantBarsForPopup(tab.url, folders, groups, rules, actionBars, hidden);
  if (relevantBars.length > 0) {
    const section = document.createElement('div');
    section.className = 'bars-section';
    let barsHtml = `<div class="bars-section-title">${escapeHtml(JZ_I18N.t('popup.actionBarsTitle'))}</div>`;
    relevantBars.forEach(b => {
      const displayName = b.name
        || (b.isLegacy ? (folders.find(f => f.id === b.barId.slice('__legacy_'.length))?.name || '') : '')
        || JZ_I18N.t('popup.fallback.barName');
      // Od v2.13.50: cascade-aware checkbox — pokazuje effective state.
      const effective = b.enabled && b.cascadeEnabled;
      const disabled = !b.cascadeEnabled;
      barsHtml += `
        <label class="bar-row${disabled ? ' bar-row-disabled' : ''}">
          <input type="checkbox" data-bar-id="${escapeHtml(b.barId)}" ${effective ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
          <span class="bar-name">${escapeHtml(displayName)}</span>
        </label>
      `;
    });
    section.innerHTML = barsHtml;
    barsArea.appendChild(section);
    section.querySelectorAll('input[data-bar-id]').forEach(cb => {
      cb.addEventListener('change', async (e) => {
        const barId = e.target.dataset.barId;
        const visible = e.target.checked;
        // Od v2.13.24: deleguje do content scripta — setBarVisibility() pisze persistent
        // state (actionBar.enabled lub folder.showActionsPanel) ORAZ czyści per-window
        // barsHidden. Refresh popup'u robi storage.onChanged listener (od v2.13.48) —
        // wcześniejsze synchroniczne load() po sendMessage czytało stale storage (content.js
        // jeszcze nie zdążył przetworzyć msg) → "co drugie kliknięcie" bug.
        chrome.tabs.sendMessage(tab.id, {
          type: 'set-bar-visibility',
          barId,
          visible,
        }).catch(() => {});
      });
    });
  }

  // Sekcja "Okna CSS" (od v2.13.41) — toggle enabled per pane relevant dla bieżącej karty.
  // Discovery analogicznie do bars: matching folders → panes z parentType/parentId w scope.
  const relevantCssPanes = discoverRelevantCssPanesForPopup(tab.url, folders, groups, rules, cssPanes, hidden);
  if (relevantCssPanes.length > 0) {
    const section = document.createElement('div');
    section.className = 'bars-section';
    let panesHtml = `<div class="bars-section-title">${escapeHtml(JZ_I18N.t('cssPanes.btn.label'))}</div>`;
    relevantCssPanes.forEach(p => {
      const displayName = p.name || JZ_I18N.t('cssPanes.btn.label');
      const color = (p.color || '#2D9D53').replace(/[<"']/g, '');
      // Od v2.13.50: cascade-aware checkbox.
      const cascade = paneCascadeEnabledForPopup(p, folders, groups, rules, hidden);
      const effective = (p.enabled !== false) && cascade;
      const disabled = !cascade;
      panesHtml += `
        <label class="bar-row${disabled ? ' bar-row-disabled' : ''}">
          <input type="checkbox" data-css-pane-id="${escapeHtml(p.id)}" ${effective ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
          <span class="bar-color-dot" style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${color};margin-right:6px;border:1px solid rgba(0,0,0,0.1);"></span>
          <span class="bar-name">${escapeHtml(displayName)}</span>
        </label>
      `;
    });
    section.innerHTML = panesHtml;
    barsArea.appendChild(section);
    section.querySelectorAll('input[data-css-pane-id]').forEach(cb => {
      cb.addEventListener('change', async (e) => {
        const paneId = e.target.dataset.cssPaneId;
        const enabled = e.target.checked;
        // Refresh popup'u robi storage.onChanged listener (od v2.13.48) — patrz komentarz
        // przy bar handler powyżej.
        chrome.tabs.sendMessage(tab.id, {
          type: 'set-css-pane-enabled',
          paneId,
          enabled,
        }).catch(() => {});
      });
    });
  }

  // Sekcja "Okna JS" (od v2.13.56) — analogiczna do CSS panes.
  const relevantJsPanes = discoverRelevantJsPanesForPopup(tab.url, folders, groups, rules, jsPanes, hidden);
  if (relevantJsPanes.length > 0) {
    const section = document.createElement('div');
    section.className = 'bars-section';
    let panesHtml = `<div class="bars-section-title">${escapeHtml(JZ_I18N.t('jsPanes.btn.label'))}</div>`;
    relevantJsPanes.forEach(p => {
      const displayName = p.name || JZ_I18N.t('jsPanes.btn.label');
      const color = (p.color || '#D65D0E').replace(/[<"']/g, '');
      const cascade = paneCascadeEnabledForPopup(p, folders, groups, rules, hidden);
      const effective = (p.enabled !== false) && cascade;
      const disabled = !cascade;
      panesHtml += `
        <label class="bar-row${disabled ? ' bar-row-disabled' : ''}">
          <input type="checkbox" data-js-pane-id="${escapeHtml(p.id)}" ${effective ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
          <span class="bar-color-dot" style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${color};margin-right:6px;border:1px solid rgba(0,0,0,0.1);"></span>
          <span class="bar-name">${escapeHtml(displayName)}</span>
        </label>
      `;
    });
    section.innerHTML = panesHtml;
    barsArea.appendChild(section);
    section.querySelectorAll('input[data-js-pane-id]').forEach(cb => {
      cb.addEventListener('change', async (e) => {
        const paneId = e.target.dataset.jsPaneId;
        const enabled = e.target.checked;
        chrome.tabs.sendMessage(tab.id, {
          type: 'set-js-pane-enabled',
          paneId,
          enabled,
        }).catch(() => {});
      });
    });
  }

  // Sekcja "Konsole JS" (od v2.13.68) — analogiczna do JS panes.
  const relevantJsConsoles = discoverRelevantJsConsolesForPopup(tab.url, folders, groups, rules, jsConsoles, hidden);
  if (relevantJsConsoles.length > 0) {
    const section = document.createElement('div');
    section.className = 'bars-section';
    let consHtml = `<div class="bars-section-title">${escapeHtml(JZ_I18N.t('jsConsoles.btn.label'))}</div>`;
    relevantJsConsoles.forEach(c => {
      const displayName = c.name || JZ_I18N.t('jsConsoles.btn.label');
      const color = (c.color || '#7C3AED').replace(/[<"']/g, '');
      const cascade = paneCascadeEnabledForPopup(c, folders, groups, rules, hidden);
      const effective = (c.enabled !== false) && cascade;
      const disabled = !cascade;
      consHtml += `
        <label class="bar-row${disabled ? ' bar-row-disabled' : ''}">
          <input type="checkbox" data-js-console-id="${escapeHtml(c.id)}" ${effective ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
          <span class="bar-color-dot" style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${color};margin-right:6px;border:1px solid rgba(0,0,0,0.1);"></span>
          <span class="bar-name">${escapeHtml(displayName)}</span>
        </label>
      `;
    });
    section.innerHTML = consHtml;
    barsArea.appendChild(section);
    section.querySelectorAll('input[data-js-console-id]').forEach(cb => {
      cb.addEventListener('change', async (e) => {
        const consoleId = e.target.dataset.jsConsoleId;
        const enabled = e.target.checked;
        chrome.tabs.sendMessage(tab.id, {
          type: 'set-js-console-enabled',
          consoleId,
          enabled,
        }).catch(() => {});
      });
    });
  }

  // Sekcja "Konsole Output" (od v2.13.72) — analogiczna do JS Console.
  const relevantOutputConsoles = discoverRelevantOutputConsolesForPopup(tab.url, folders, groups, rules, outputConsoles, hidden);
  if (relevantOutputConsoles.length > 0) {
    const section = document.createElement('div');
    section.className = 'bars-section';
    let ocHtml = `<div class="bars-section-title">${escapeHtml(JZ_I18N.t('outputConsoles.btn.label'))}</div>`;
    relevantOutputConsoles.forEach(o => {
      const displayName = o.name || JZ_I18N.t('outputConsoles.btn.label');
      const color = (o.color || '#10B981').replace(/[<"']/g, '');
      const cascade = paneCascadeEnabledForPopup(o, folders, groups, rules, hidden);
      const effective = (o.enabled !== false) && cascade;
      const disabled = !cascade;
      ocHtml += `
        <label class="bar-row${disabled ? ' bar-row-disabled' : ''}">
          <input type="checkbox" data-output-console-id="${escapeHtml(o.id)}" ${effective ? 'checked' : ''} ${disabled ? 'disabled' : ''}>
          <span class="bar-color-dot" style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${color};margin-right:6px;border:1px solid rgba(0,0,0,0.1);"></span>
          <span class="bar-name">${escapeHtml(displayName)}</span>
        </label>
      `;
    });
    section.innerHTML = ocHtml;
    barsArea.appendChild(section);
    section.querySelectorAll('input[data-output-console-id]').forEach(cb => {
      cb.addEventListener('change', async (e) => {
        const consoleId = e.target.dataset.outputConsoleId;
        const enabled = e.target.checked;
        chrome.tabs.sendMessage(tab.id, {
          type: 'set-output-console-enabled',
          consoleId,
          enabled,
        }).catch(() => {});
      });
    });
  }

  const hiddenForThisPage = matching.filter(f => hidden[f.id]);
  const restoreArea = document.getElementById('restore-area');
  restoreArea.innerHTML = '';
  if (hiddenForThisPage.length > 0) {
    const link = document.createElement('a');
    link.className = 'restore-link';
    const REDO_ICON = '<svg class="jz-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter"><polyline points="16 5 21 10 16 15"/><path d="M21 10H9C5.69 10 3 12.69 3 16V19"/></svg>';
    link.innerHTML = `${REDO_ICON} ${escapeHtml(JZ_I18N.t('popup.restore', { count: hiddenForThisPage.length }))}`;
    link.addEventListener('click', async () => {
      const data = await chrome.storage.local.get(['floatingHidden']);
      const h = data.floatingHidden || {};
      hiddenForThisPage.forEach(f => { delete h[f.id]; });
      await chrome.storage.local.set({ floatingHidden: h });
      load();
    });
    restoreArea.appendChild(link);
  }

  // Cascade semantyka (od v2.13.53): WŁĄCZANIE propaguje w GÓRĘ (rule → group → folder),
  // WYŁĄCZANIE propaguje w DÓŁ (folder → groups → rules, group → rules). Spójne między
  // widget popup (content.js togglePanel), browser action popup (tu) i panel edycji (options.js).
  list.querySelectorAll('input[data-folder-id]').forEach(cb => {
    cb.addEventListener('change', async (e) => {
      const folderId = e.target.dataset.folderId;
      const enable = e.target.checked;
      const data = await chrome.storage.local.get(['folders', 'groups', 'rules']);
      const folders = data.folders || [];
      const groups = data.groups || [];
      const rules = data.rules || [];
      const folder = folders.find(f => f.id === folderId);
      if (folder) {
        folder.enabled = enable;
        if (!enable) {
          const folderGroups = groups.filter(g => g.folderId === folderId);
          folderGroups.filter(g => g.active !== false).forEach(g => { g.enabled = false; });
          const folderGroupIds = new Set(folderGroups.map(g => g.id));
          rules.filter(r => folderGroupIds.has(r.groupId) && r.active).forEach(r => { r.enabled = false; });
        }
        await chrome.storage.local.set({ folders, groups, rules });
        // load() pomijamy — storage.onChanged odświeży popup (spójnie z bar/pane handlerami).
      }
    });
  });

  list.querySelectorAll('input[data-group-id]').forEach(cb => {
    cb.addEventListener('change', async (e) => {
      const groupId = e.target.dataset.groupId;
      const enable = e.target.checked;
      const data = await chrome.storage.local.get(['folders', 'groups', 'rules']);
      const folders = data.folders || [];
      const groups = data.groups || [];
      const rules = data.rules || [];
      const group = groups.find(g => g.id === groupId);
      if (group) {
        group.enabled = enable;
        if (enable) {
          const parentFolder = folders.find(f => f.id === group.folderId);
          if (parentFolder) parentFolder.enabled = true;
        } else {
          rules.filter(r => r.groupId === groupId && r.active).forEach(r => { r.enabled = false; });
        }
        await chrome.storage.local.set({ folders, groups, rules });
        // load() pomijamy — storage.onChanged odświeży popup (spójnie z bar/pane handlerami).
      }
    });
  });

  list.querySelectorAll('input[data-rule-id]').forEach(cb => {
    cb.addEventListener('change', async (e) => {
      const ruleId = e.target.dataset.ruleId;
      const enable = e.target.checked;
      const data = await chrome.storage.local.get(['folders', 'groups', 'rules']);
      const folders = data.folders || [];
      const groups = data.groups || [];
      const rules = data.rules || [];
      const rule = rules.find(r => r.id === ruleId);
      if (rule) {
        rule.enabled = enable;
        if (enable) {
          const parentGroup = groups.find(g => g.id === rule.groupId);
          if (parentGroup) {
            parentGroup.enabled = true;
            const parentFolder = folders.find(f => f.id === parentGroup.folderId);
            if (parentFolder) parentFolder.enabled = true;
          }
        }
        await chrome.storage.local.set({ folders, groups, rules });
        // load() pomijamy — storage.onChanged odświeży popup (spójnie z bar/pane handlerami).
      }
    });
  });

  // Toggle hide/restore per folder (od v2.13.49 — wcześniej tylko hide z osobnym restore link).
  list.querySelectorAll('button[data-hide-folder-id]').forEach(btn => {
    btn.addEventListener('click', async (e) => {
      e.preventDefault();
      e.stopPropagation();
      const folderId = btn.dataset.hideFolderId;
      const currentlyHidden = btn.dataset.currentlyHidden === '1';
      const d = await chrome.storage.local.get(['floatingHidden']);
      const h = d.floatingHidden || {};
      if (currentlyHidden) {
        delete h[folderId];
      } else {
        h[folderId] = true;
      }
      await chrome.storage.local.set({ floatingHidden: h });
      // load() wywołany też przez storage.onChanged listener — tu tylko żeby było snappy.
      load();
    });
  });
}

document.getElementById('optionsBtn').addEventListener('click', () => {
  chrome.runtime.openOptionsPage();
});

// Globalny przełącznik
// Tryb edycji paska (od v2.13.97) — toggle stanu trybu edycji w content.js aktywnej karty.
// Stan jest per-karta (zmienna w content.js), więc odpytujemy/ustawiamy przez sendMessage.
async function refreshEditModeToggle() {
  const el = document.getElementById('editModeToggle');
  const status = document.getElementById('emStatus');
  if (!el || !status) return;
  try {
    const tab = await getCurrentTab();
    if (!tab) throw new Error('no tab');
    const resp = await chrome.tabs.sendMessage(tab.id, { type: 'get-edit-mode' });
    const on = !!(resp && resp.editMode);
    el.classList.toggle('off', !on);
    status.textContent = JZ_I18N.t(on ? 'popup.editMode.on' : 'popup.editMode.off');
  } catch (_) {
    el.classList.add('off');
    status.textContent = JZ_I18N.t('popup.editMode.unavailable');
  }
}

document.getElementById('editModeToggle').addEventListener('click', async () => {
  const el = document.getElementById('editModeToggle');
  const status = document.getElementById('emStatus');
  try {
    const tab = await getCurrentTab();
    if (!tab) throw new Error('no tab');
    const resp = await chrome.tabs.sendMessage(tab.id, { type: 'toggle-edit-mode' });
    const on = !!(resp && resp.editMode);
    el.classList.toggle('off', !on);
    status.textContent = JZ_I18N.t(on ? 'popup.editMode.on' : 'popup.editMode.off');
  } catch (_) {
    el.classList.add('off');
    status.textContent = JZ_I18N.t('popup.editMode.unavailable');
  }
});

// AI Helper (od v2.13.127) — toggle okna AI Helper w aktywnej karcie. Stan per-karta.
async function refreshAiHelperToggle() {
  const el = document.getElementById('aiHelperToggle');
  const status = document.getElementById('ahStatus');
  if (!el || !status) return;
  try {
    const tab = await getCurrentTab();
    if (!tab) throw new Error('no tab');
    const resp = await chrome.tabs.sendMessage(tab.id, { type: 'get-ai-helper' });
    const open = !!(resp && resp.open);
    el.classList.toggle('off', !open);
    status.textContent = JZ_I18N.t(open ? 'popup.aiHelper.on' : 'popup.aiHelper.off');
  } catch (_) {
    el.classList.add('off');
    status.textContent = JZ_I18N.t('popup.aiHelper.unavailable');
  }
}

document.getElementById('aiHelperToggle').addEventListener('click', async () => {
  const el = document.getElementById('aiHelperToggle');
  const status = document.getElementById('ahStatus');
  try {
    const tab = await getCurrentTab();
    if (!tab) throw new Error('no tab');
    const resp = await chrome.tabs.sendMessage(tab.id, { type: 'toggle-ai-helper' });
    const open = !!(resp && resp.open);
    el.classList.toggle('off', !open);
    status.textContent = JZ_I18N.t(open ? 'popup.aiHelper.on' : 'popup.aiHelper.off');
  } catch (_) {
    el.classList.add('off');
    status.textContent = JZ_I18N.t('popup.aiHelper.unavailable');
  }
});

document.getElementById('globalToggle').addEventListener('click', async () => {
  const data = await chrome.storage.local.get(['globalEnabled']);
  const newState = data.globalEnabled === false; // toggle
  await chrome.storage.local.set({ globalEnabled: newState });
  // Powiadom wszystkie taby
  const tabs = await chrome.tabs.query({});
  tabs.forEach(t => {
    chrome.tabs.sendMessage(t.id, { type: 'apply-rules' }).catch(() => {});
  });
  load();
});

// === Theme (od v3.2.78) — czyta storage['theme'] i aplikuje na body.theme-dark.
// Wartości: 'light'|'dark'|'auto'|undefined. 'auto'/undefined → resolve via
// matchMedia('(prefers-color-scheme: dark)'). Spójne z options.js applyTheme.
function _popupResolveTheme(theme) {
  if (theme === 'dark') return 'dark';
  if (theme === 'light') return 'light';
  if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark';
  return 'light';
}
function _popupApplyTheme(theme) {
  const effective = _popupResolveTheme(theme);
  document.body.classList.toggle('theme-dark', effective === 'dark');
}
async function _popupInitTheme() {
  try {
    const d = await chrome.storage.local.get(['theme']);
    _popupApplyTheme(d.theme || 'auto');
    if (window.matchMedia) {
      const mq = window.matchMedia('(prefers-color-scheme: dark)');
      const onChange = () => {
        chrome.storage.local.get(['theme']).then((d2) => {
          const t = d2.theme;
          if (t === 'auto' || !t) _popupApplyTheme(t);
        });
      };
      try { mq.addEventListener('change', onChange); } catch (_) {}
    }
  } catch (_) {}
}
// Apply ASAP — przed I18N init/load, żeby nie było flash białego tła w dark mode.
_popupInitTheme();

// === Link do strony projektu (od v3.2.81) — równoległy do options.js jzAppendUtm/
// jzProjectSiteUrl, ale z utm_term='popup'. Duplikacja świadoma (niskie ryzyko, brak
// dependencji na options.js). browser z userAgent, version z manifestu.
function _popupBrowserName() {
  const ua = navigator.userAgent || '';
  return ua.includes('Firefox') ? 'firefox'
    : ua.includes('OPR/') ? 'opera'
    : ua.includes('Edg/') ? 'edge'
    : 'chrome';
}
function _popupProjectSiteUrl() {
  const lang = (typeof JZ_I18N !== 'undefined' && JZ_I18N.lang) || 'pl';
  try {
    const u = new URL('https://justzix.com/' + lang + '/');
    u.searchParams.set('utm_source', 'extension');
    u.searchParams.set('utm_medium', 'referral');
    u.searchParams.set('utm_campaign', _popupBrowserName());
    u.searchParams.set('utm_term', 'popup');
    u.searchParams.set('utm_content', chrome.runtime.getManifest().version);
    return u.toString();
  } catch (e) {
    return 'https://justzix.com/' + lang + '/';
  }
}
(function wireProjectSiteLink() {
  const a = document.getElementById('projectSiteLink');
  if (!a) return;
  a.addEventListener('click', (e) => {
    e.preventDefault();
    chrome.tabs.create({ url: _popupProjectSiteUrl() });
    window.close();
  });
})();

async function start() {
  await JZ_I18N.init();
  JZ_I18N.applyToDOM(document);
  await load();
  refreshEditModeToggle();
  refreshAiHelperToggle();
}
start();

// Od v2.13.48: refresh popup'u na storage change. Bug "co drugie kliknięcie" w popup'ie:
// bar/CSS pane checkbox handler wysyłał sendMessage do content.js (async, fire-and-forget)
// i NATYCHMIAST wołał load(). load() czytał storage który content.js jeszcze nie zaktualizował
// (wiadomość nie przetworzona). Popup re-renderowany z STAREGO state'u → checkbox wracał do
// poprzedniego stanu mimo browser flip'a → drugi klik usera dawał e.target.checked sprzed
// pierwszego kliku → write tej samej wartości → no-op.
//
// Fix: nie wołamy load() po sendMessage. Zamiast tego ten listener — gdy content.js (lub inny
// kontekst) zaktualizuje storage, popup się odświeża z faktycznym state'm.
chrome.storage.onChanged.addListener((changes, area) => {
  if (area !== 'local') return;
  // od v3.2.78 — gdy user zmieni theme w options, popup się przekoloruje od razu.
  if (changes.theme) _popupApplyTheme(changes.theme.newValue || 'auto');
  if (changes.actionBars || changes.cssPanes || changes.jsPanes || changes.jsConsoles || changes.outputConsoles || changes.folders || changes.groups
      || changes.rules || changes.floatingHidden || changes.globalEnabled) {
    load();
  }
});
