// background.js — sequential GMB scraper (strict sequential; hide first N rows from UI)
// Tags: [JOB] [STEP] [SCRAPE] [WATCHDOG] [INJECT] [STREAM] [PICK] [OPEN] [RETRY] [RESCUE] [END] [SEQ] [SUPPRESS]

const ALLOWED_HOSTS = [
  "localhost",
  "leadsniper.online",
  "www.leadsniper.online",
  "demo.leadsniper.online",
  "app.leadsniper.online",
  "ahmad.leadsniper.online",
  "www.app.leadsniper.online"
];

const TAG = (t, ...a) => console.log(`[${t}]`, ...a);
const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));
const jobs = new Map();
const portsByTab = new Map();
const KEEP_ALIVE_ALARM = "mapsScraperKeepAlive";
let activeJobCount = 0;
const JOB_SNAPSHOT_KEY = "mapsScraperJobs";
const JOB_SNAPSHOT_TTL_MS = 30 * 60 * 1000;
let jobSnapshots = new Map();
let snapshotSaveTimer = null;
let snapshotsReady = null;
const chromeRuntime = typeof chrome !== "undefined" ? chrome.runtime : null;
const emailSenderKeepAlivePorts = new Set();
const emailSenderContentTabs = new Set();

function safeRuntimeSend(message, tag = 'RUNTIME') {
  if (!chromeRuntime || typeof chromeRuntime.sendMessage !== 'function') return;
  try {
    chromeRuntime.sendMessage(message, () => {
      const err = chrome.runtime.lastError;
      if (err && !/Receiving end does not exist|No tab with id|The message port closed/i.test(err.message || '')) {
        TAG(tag, 'sendMessage failed', err.message || err);
      }
    });
  } catch (error) {
    TAG(tag, 'sendMessage threw', error?.message || error);
  }
}

const yelpContentTabs = new Set();
const YELP_CONTENT_QUERY_PATTERNS = [
  '*://*.leadsniper.online/*',
  'http://localhost/*',
  'http://127.0.0.1/*',
  'https://app.leadsniper.online/*',
  'https://demo.leadsniper.online/*',
  'https://ahmad.leadsniper.online/*',
  'https://www.leadsniper.online/*',
  'https://www.app.leadsniper.online/*'
];

const YELP_STATE_STORAGE_KEY = '__lsCachedYelpState';
const YELLOWPAGES_STATE_STORAGE_KEY = '__lsCachedYellowPagesState';
5
const yelpResultsByKey = new Map();
const yelpState = {
  runId: null,
  status: 'idle',
  items: [],
  total: 0,
  index: 0,
  url: '',
  error: null,
  updatedAt: 0
};

const yellowPagesContentTabs = new Set();
const yellowPagesResultsByKey = new Map();
const yellowPagesState = {
  runId: null,
  status: 'idle',
  items: [],
  total: 0,
  index: 0,
  url: '',
  error: null,
  updatedAt: 0
};

const cleanYelpText = (value) => (typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '');
const cleanYellowPagesText = (value) => (typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '');

const getYelpItemKey = (item = {}) => {
  const canonical = cleanYelpText(item.canonical);
  if (canonical) return canonical;
  const profile = cleanYelpText(item.profileUrl);
  if (profile) return profile;
  const website = cleanYelpText(item.websiteUrl);
  if (website) return website;
  const fallback = [item.name, item.phone, item.address]
    .map((entry) => cleanYelpText(entry))
    .filter(Boolean)
    .join('|');
  return fallback || null;
};

const normalizeYelpItem = (raw = {}) => {
  const name = cleanYelpText(raw.name);
  if (!name) return null;
  const rating = cleanYelpText(raw.rating);
  const reviews = cleanYelpText(raw.reviewsText || raw.reviews);
  const hours = cleanYelpText(raw.hours);
  const phone = cleanYelpText(raw.phone);
  const websiteUrl = cleanYelpText(raw.websiteUrl || raw.website);
  const websiteLabel = cleanYelpText(raw.websiteLabel || raw.websiteText || (websiteUrl ? websiteUrl.replace(/^https?:\/\//i, '') : ''));
  const address = cleanYelpText(raw.address);
  const profileUrl = cleanYelpText(raw.profileUrl || raw.url);
  const canonical = cleanYelpText(raw.canonical || (profileUrl ? profileUrl.replace(/\?.*$/, '') : ''));
  return {
    name,
    rating,
    reviews,
    hours,
    phone,
    websiteUrl,
    websiteLabel,
    address,
    profileUrl,
    canonical
  };
};

const getYellowPagesItemKey = (item = {}) => {
  const canonical = cleanYellowPagesText(item.canonical);
  if (canonical) return canonical;
  const profile = cleanYellowPagesText(item.profileUrl);
  if (profile) return profile;
  const website = cleanYellowPagesText(item.websiteUrl);
  if (website) return website;
  const fallback = [item.name, item.phone, item.address]
    .map((entry) => cleanYellowPagesText(entry))
    .filter(Boolean)
    .join('|');
  return fallback || null;
};

const normalizeYellowPagesItem = (raw = {}) => {
  const name = cleanYellowPagesText(raw.name);
  if (!name) return null;
  const rating = cleanYellowPagesText(raw.rating || raw.ratingText);
  const reviews = cleanYellowPagesText(raw.reviews || raw.reviewsText);
  const hours = cleanYellowPagesText(raw.hours || raw.status || raw.openStatus);
  const phone = cleanYellowPagesText(raw.phone);
  const websiteUrl = cleanYellowPagesText(raw.websiteUrl || raw.website);
  const websiteLabel = cleanYellowPagesText(raw.websiteLabel || raw.websiteText || (websiteUrl ? websiteUrl.replace(/^https?:\/\//i, '') : ''));
  const address = cleanYellowPagesText(raw.address);
  const profileUrl = cleanYellowPagesText(raw.profileUrl || raw.url);
  const canonical = cleanYellowPagesText(raw.canonical || profileUrl || '');
  return {
    name,
    rating,
    reviews,
    hours,
    phone,
    websiteUrl,
    websiteLabel,
    address,
    profileUrl,
    canonical
  };
};

const rebuildYelpItems = () => {
  yelpState.items = Array.from(yelpResultsByKey.values());
};

const rebuildYellowPagesItems = () => {
  yellowPagesState.items = Array.from(yellowPagesResultsByKey.values());
};

const resetYelpState = (runId = null) => {
  yelpResultsByKey.clear();
  yelpState.runId = runId;
  yelpState.status = runId ? 'running' : 'idle';
  yelpState.items = [];
  yelpState.total = 0;
  yelpState.index = 0;
  yelpState.url = '';
  yelpState.error = null;
  yelpState.updatedAt = Date.now();
};

const resetYellowPagesState = (runId = null) => {
  yellowPagesResultsByKey.clear();
  yellowPagesState.runId = runId;
  yellowPagesState.status = runId ? 'running' : 'idle';
  yellowPagesState.items = [];
  yellowPagesState.total = 0;
  yellowPagesState.index = 0;
  yellowPagesState.url = '';
  yellowPagesState.error = null;
  yellowPagesState.updatedAt = Date.now();
};

const setYelpItemsFromArray = (items) => {
  yelpResultsByKey.clear();
  if (Array.isArray(items)) {
    items.forEach((raw) => {
      const normalized = normalizeYelpItem(raw);
      if (!normalized) return;
      const key = getYelpItemKey(normalized);
      if (key) {
        yelpResultsByKey.set(key, normalized);
      }
    });
  }
  rebuildYelpItems();
};

const setYellowPagesItemsFromArray = (items) => {
  yellowPagesResultsByKey.clear();
  if (Array.isArray(items)) {
    items.forEach((raw) => {
      const normalized = normalizeYellowPagesItem(raw);
      if (!normalized) return;
      const key = getYellowPagesItemKey(normalized);
      if (key) {
        yellowPagesResultsByKey.set(key, normalized);
      }
    });
  }
  rebuildYellowPagesItems();
};

const upsertYelpItem = (raw) => {
  const normalized = normalizeYelpItem(raw);
  if (!normalized) return;
  const key = getYelpItemKey(normalized);
  if (!key) return;
  yelpResultsByKey.set(key, normalized);
  rebuildYelpItems();
};

const upsertYellowPagesItem = (raw) => {
  const normalized = normalizeYellowPagesItem(raw);
  if (!normalized) return;
  const key = getYellowPagesItemKey(normalized);
  if (!key) return;
  yellowPagesResultsByKey.set(key, normalized);
  rebuildYellowPagesItems();
};

const snapshotYelpState = () => ({
  runId: yelpState.runId,
  status: yelpState.status,
  items: yelpState.items.map((item) => ({ ...item })),
  total: yelpState.total,
  index: yelpState.index,
  url: yelpState.url,
  error: yelpState.error,
  updatedAt: yelpState.updatedAt
});

const snapshotYellowPagesState = () => ({
  runId: yellowPagesState.runId,
  status: yellowPagesState.status,
  items: yellowPagesState.items.map((item) => ({ ...item })),
  total: yellowPagesState.total,
  index: yellowPagesState.index,
  url: yellowPagesState.url,
  error: yellowPagesState.error,
  updatedAt: yellowPagesState.updatedAt
});

const persistYelpState = () => {
  if (!chrome?.storage?.local) return;
  try {
    chrome.storage.local.set({ [YELP_STATE_STORAGE_KEY]: snapshotYelpState() });
  } catch (error) {
    TAG('YELP', 'state persist failed', error?.message || error);
  }
};

const persistYellowPagesState = () => {
  if (!chrome?.storage?.local) return;
  try {
    chrome.storage.local.set({ [YELLOWPAGES_STATE_STORAGE_KEY]: snapshotYellowPagesState() });
  } catch (error) {
    TAG('YP', 'state persist failed', error?.message || error);
  }
};

const restoreYelpStateFromStorage = () => {
  if (!chrome?.storage?.local) return;
  try {
    chrome.storage.local.get([YELP_STATE_STORAGE_KEY], (result) => {
      const err = chrome.runtime?.lastError;
      if (err) {
        TAG('YELP', 'state restore error', err.message || err);
        return;
      }
      const stored = result ? result[YELP_STATE_STORAGE_KEY] : null;
      if (!stored || typeof stored !== 'object') return;
      try {
        resetYelpState(stored.runId || null);
        if (Array.isArray(stored.items) && stored.items.length) {
          setYelpItemsFromArray(stored.items);
        }
        yelpState.status = stored.status || yelpState.status;
        yelpState.total = typeof stored.total === 'number' ? stored.total : yelpState.total;
        yelpState.index = typeof stored.index === 'number' ? stored.index : yelpState.index;
        yelpState.url = stored.url || yelpState.url;
        yelpState.error = stored.error || null;
        yelpState.updatedAt = stored.updatedAt || Date.now();
        persistYelpState();
      } catch (errorRestore) {
        TAG('YELP', 'state restore parse failed', errorRestore?.message || errorRestore);
      }
    });
  } catch (error) {
    TAG('YELP', 'state restore exception', error?.message || error);
  }
};

restoreYelpStateFromStorage();

const restoreYellowPagesStateFromStorage = () => {
  if (!chrome?.storage?.local) return;
  try {
    chrome.storage.local.get([YELLOWPAGES_STATE_STORAGE_KEY], (result) => {
      const err = chrome.runtime?.lastError;
      if (err) {
        TAG('YP', 'state restore error', err.message || err);
        return;
      }
      const stored = result ? result[YELLOWPAGES_STATE_STORAGE_KEY] : null;
      if (!stored || typeof stored !== 'object') return;
      try {
        resetYellowPagesState(stored.runId || null);
        if (Array.isArray(stored.items) && stored.items.length) {
          setYellowPagesItemsFromArray(stored.items);
        }
        yellowPagesState.status = stored.status || yellowPagesState.status;
        yellowPagesState.total = typeof stored.total === 'number' ? stored.total : yellowPagesState.total;
        yellowPagesState.index = typeof stored.index === 'number' ? stored.index : yellowPagesState.index;
        yellowPagesState.url = stored.url || yellowPagesState.url;
        yellowPagesState.error = stored.error || null;
        yellowPagesState.updatedAt = stored.updatedAt || Date.now();
        persistYellowPagesState();
      } catch (errorRestore) {
        TAG('YP', 'state restore parse failed', errorRestore?.message || errorRestore);
      }
    });
  } catch (error) {
    TAG('YP', 'state restore exception', error?.message || error);
  }
};

restoreYellowPagesStateFromStorage();

const updateYelpState = (message = {}) => {
  if (!message || typeof message !== 'object') return;
  const { type } = message;
  if (!type || typeof type !== 'string') return;

  const runId = message.runId || null;
  const now = Date.now();

  if (type === 'yelp-scrape-start') {
    resetYelpState(runId || `run-${now}`);
    yelpState.status = 'running';
    yelpState.total = Number(message.expected) || 0;
    yelpState.url = message.url || '';
    yelpState.updatedAt = now;
    persistYelpState();
    return;
  }

  if (type === 'yelp-scrape-progress') {
    if (runId && runId !== yelpState.runId) {
      resetYelpState(runId);
    }
    if (!yelpState.runId) yelpState.runId = runId || `run-${now}`;
    yelpState.status = 'running';
    if (Array.isArray(message.items) && message.items.length) {
      setYelpItemsFromArray(message.items);
    } else if (message.item) {
      upsertYelpItem(message.item);
    }
    const total = Number(message.total);
    if (!Number.isNaN(total) && total >= 0) {
      yelpState.total = total;
    } else if (yelpState.items.length > yelpState.total) {
      yelpState.total = yelpState.items.length;
    }
    const index = Number(message.index);
    if (!Number.isNaN(index) && index >= 0) {
      yelpState.index = index;
    } else {
      yelpState.index = yelpState.items.length;
    }
    yelpState.url = message.url || yelpState.url;
    yelpState.updatedAt = now;
    persistYelpState();
    return;
  }

  if (type === 'yelp-scrape-results') {
    if (runId && runId !== yelpState.runId) {
      resetYelpState(runId);
    }
    setYelpItemsFromArray(Array.isArray(message.items) ? message.items : []);
    yelpState.runId = runId || yelpState.runId || `run-${now}`;
    yelpState.status = 'completed';
    yelpState.total = Number(message.count) || yelpState.items.length;
    yelpState.index = yelpState.total;
    yelpState.url = message.url || yelpState.url;
    yelpState.error = null;
    yelpState.updatedAt = now;
    persistYelpState();
    return;
  }

  if (type === 'yelp-scrape-error') {
    if (runId && runId !== yelpState.runId) {
      resetYelpState(runId);
    }
    if (!yelpState.runId) yelpState.runId = runId || `run-${now}`;
    yelpState.status = 'error';
    yelpState.error = message.error || 'Scrape failed.';
    yelpState.updatedAt = now;
    persistYelpState();
    return;
  }

  if (type === 'yelp-verification-required') {
    yelpState.status = 'verification_required';
    yelpState.updatedAt = now;
    persistYelpState();
    return;
  }

  if (type === 'yelp-verification-cleared') {
    if (yelpState.status === 'verification_required') {
      yelpState.status = 'idle';
    }
    yelpState.updatedAt = now;
    persistYelpState();
  }
};

const updateYellowPagesState = (message = {}) => {
  if (!message || typeof message !== 'object') return;
  const { type } = message;
  if (!type || typeof type !== 'string') return;

  const runId = message.runId || null;
  const now = Date.now();

  if (type === 'yellowpages-scrape-start') {
    resetYellowPagesState(runId || `run-${now}`);
    yellowPagesState.status = 'running';
    yellowPagesState.total = Number(message.expected) || 0;
    yellowPagesState.url = message.url || '';
    yellowPagesState.updatedAt = now;
    persistYellowPagesState();
    return;
  }

  if (type === 'yellowpages-scrape-progress') {
    if (runId && runId !== yellowPagesState.runId) {
      resetYellowPagesState(runId);
    }
    if (!yellowPagesState.runId) yellowPagesState.runId = runId || `run-${now}`;
    yellowPagesState.status = 'running';
    if (Array.isArray(message.items) && message.items.length) {
      setYellowPagesItemsFromArray(message.items);
    } else if (message.item) {
      upsertYellowPagesItem(message.item);
    }
    const total = Number(message.total);
    if (!Number.isNaN(total) && total >= 0) {
      yellowPagesState.total = total;
    } else if (yellowPagesState.items.length > yellowPagesState.total) {
      yellowPagesState.total = yellowPagesState.items.length;
    }
    const index = Number(message.index);
    if (!Number.isNaN(index) && index >= 0) {
      yellowPagesState.index = index;
    } else if (yellowPagesState.items.length) {
      yellowPagesState.index = yellowPagesState.items.length;
    }
    yellowPagesState.url = message.url || yellowPagesState.url;
    yellowPagesState.updatedAt = now;
    persistYellowPagesState();
    return;
  }

  if (type === 'yellowpages-scrape-results') {
    if (runId && runId !== yellowPagesState.runId) {
      resetYellowPagesState(runId);
    }
    setYellowPagesItemsFromArray(Array.isArray(message.items) ? message.items : []);
    yellowPagesState.runId = runId || yellowPagesState.runId || `run-${now}`;
    yellowPagesState.status = 'completed';
    yellowPagesState.total = Number(message.count) || yellowPagesState.items.length;
    yellowPagesState.index = yellowPagesState.total;
    yellowPagesState.url = message.url || yellowPagesState.url;
    yellowPagesState.error = null;
    yellowPagesState.updatedAt = now;
    persistYellowPagesState();
    return;
  }

  if (type === 'yellowpages-scrape-error') {
    if (runId && runId !== yellowPagesState.runId) {
      resetYellowPagesState(runId);
    }
    if (!yellowPagesState.runId) yellowPagesState.runId = runId || `run-${now}`;
    yellowPagesState.status = 'error';
    yellowPagesState.error = message.error || 'Scrape failed.';
    yellowPagesState.updatedAt = now;
    persistYellowPagesState();
  }
};

function fanoutToTab(tabId, message) {
  if (typeof tabId !== 'number') return;
  chrome.tabs.sendMessage(tabId, message, () => {
    const err = chrome.runtime.lastError;
    if (err && !/No tab with id|Receiving end does not exist/i.test(err.message || '')) {
      TAG('YELP', 'broadcast tab failed', { tabId, error: err.message || err });
    }
    if (err && /No tab with id|Receiving end does not exist/i.test(err.message || '')) {
      yelpContentTabs.delete(tabId);
    }
  });
}

function discoverYelpContentTabs(message) {
  try {
    chrome.tabs.query({ url: YELP_CONTENT_QUERY_PATTERNS }, (tabs) => {
      const err = chrome.runtime.lastError;
      if (err) {
        TAG('YELP', 'content discovery failed', err.message || err);
        return;
      }
      if (!Array.isArray(tabs) || !tabs.length) return;
      tabs.forEach((tab) => {
        if (!tab || typeof tab.id !== 'number') return;
        if (!yelpContentTabs.has(tab.id)) {
          yelpContentTabs.add(tab.id);
        }
        fanoutToTab(tab.id, message);
      });
    });
  } catch (error) {
    TAG('YELP', 'content discovery exception', error?.message || error);
  }
}

function broadcastYelpMessage(payload) {
  const message = Object.assign({ __yelpFanout: true }, payload || {});
  safeRuntimeSend(message, 'YELP');

  if (!yelpContentTabs.size) {
    discoverYelpContentTabs(message);
    return;
  }

  for (const tabId of Array.from(yelpContentTabs)) {
    fanoutToTab(tabId, message);
  }
}

function fanoutToYellowPagesTab(tabId, message) {
  if (typeof tabId !== 'number') return;
  chrome.tabs.sendMessage(tabId, message, () => {
    const err = chrome.runtime.lastError;
    if (err && !/No tab with id|Receiving end does not exist/i.test(err.message || '')) {
      TAG('YP', 'broadcast tab failed', { tabId, error: err.message || err });
    }
    if (err && /No tab with id|Receiving end does not exist/i.test(err.message || '')) {
      yellowPagesContentTabs.delete(tabId);
    }
  });
}

function discoverYellowPagesContentTabs(message) {
  try {
    chrome.tabs.query({ url: YELP_CONTENT_QUERY_PATTERNS }, (tabs) => {
      const err = chrome.runtime.lastError;
      if (err) {
        TAG('YP', 'content discovery failed', err.message || err);
        return;
      }
      if (!Array.isArray(tabs) || !tabs.length) return;
      tabs.forEach((tab) => {
        if (!tab || typeof tab.id !== 'number') return;
        if (!yellowPagesContentTabs.has(tab.id)) {
          yellowPagesContentTabs.add(tab.id);
        }
        fanoutToYellowPagesTab(tab.id, message);
      });
    });
  } catch (error) {
    TAG('YP', 'content discovery exception', error?.message || error);
  }
}

function broadcastYellowPagesMessage(payload) {
  const message = Object.assign({ __yellowPagesFanout: true }, payload || {});
  safeRuntimeSend(message, 'YP');

  if (!yellowPagesContentTabs.size) {
    discoverYellowPagesContentTabs(message);
    return;
  }

  for (const tabId of Array.from(yellowPagesContentTabs)) {
    fanoutToYellowPagesTab(tabId, message);
  }
}

if (chromeRuntime?.onMessage?.addListener) {
  chromeRuntime.onMessage.addListener((message, sender, sendResponse) => {
    if (!message || typeof message !== 'object' || typeof message.type !== 'string') return;

    const reply = (payload) => {
      if (sendResponse) sendResponse(payload);
    };

    switch (message.type) {
      case 'yelp-register-content': {
        const tabId = sender?.tab?.id;
        if (typeof tabId === 'number') {
          yelpContentTabs.add(tabId);
          TAG('YELP', 'content registered', { tabId, count: yelpContentTabs.size });
        }
        reply({ ok: true });
        return true;
      }
      case 'yelp-unregister-content': {
        const tabId = sender?.tab?.id;
        if (typeof tabId === 'number' && yelpContentTabs.delete(tabId)) {
          TAG('YELP', 'content unregistered', { tabId, count: yelpContentTabs.size });
        }
        reply({ ok: true });
        return true;
      }
      case 'yelp-close-tab': {
        const tabId = sender?.tab?.id;
        if (typeof tabId === 'number') {
          chrome.tabs.remove(tabId, () => {
            const err = chrome.runtime.lastError;
            if (err && !/No tab with id/i.test(err.message || '')) {
              TAG('YELP', 'close tab failed', { tabId, error: err.message || err });
            }
          });
        }
        reply({ ok: true });
        return true;
      }
      case 'yelp-request-state': {
        try {
          reply({ ok: true, state: snapshotYelpState() });
        } catch (error) {
          TAG('YELP', 'state snapshot failed', error?.message || error);
          reply({ ok: false, error: error?.message || String(error) });
        }
        return true;
      }
      case 'yelp-reset-state': {
        const reason = message.reason || null;
        resetYelpState(null);
        persistYelpState();
        broadcastYelpMessage({ type: 'yelp-scrape-reset', reason });
        reply({ ok: true });
        return true;
      }
    }

    if (message.type?.startsWith('yelp-scrape') && !message.__yelpFanout) {
      updateYelpState(message);
      broadcastYelpMessage(message);
      reply({ ok: true });
      return true;
    }

    if (message.type === 'yelp-save-json') {
      const results = Array.isArray(message.results) ? message.results : [];
      const payload = {
        scrapedAt: message.timestamp || Date.now(),
        count: results.length,
        results,
      };
      const json = JSON.stringify(payload, null, 2);
      const safeStamp = new Date(payload.scrapedAt).toISOString().replace(/[:.]/g, '-');
      const filename = `yelp-scrape-${safeStamp}.json`;
      const toBase64 = (str) => btoa(unescape(encodeURIComponent(str)));
      const dataUrl = `data:application/json;base64,${toBase64(json)}`;
      try {
        chrome.downloads.download({ url: dataUrl, filename, saveAs: false }, (downloadId) => {
          const err = chrome.runtime.lastError;
          if (err) {
            reply({ ok: false, error: err.message || String(err) });
          } else {
            reply({ ok: true, downloadId, filename });
          }
        });
        return true;
      } catch (error) {
        reply({ ok: false, error: error?.message || String(error) });
        return true;
      }
    }

    switch (message.type) {
      case 'yellowpages-register-content': {
        const tabId = sender?.tab?.id;
        if (typeof tabId === 'number') {
          yellowPagesContentTabs.add(tabId);
          TAG('YP', 'content registered', { tabId, count: yellowPagesContentTabs.size });
        }
        reply({ ok: true });
        return true;
      }
      case 'yellowpages-unregister-content': {
        const tabId = sender?.tab?.id;
        if (typeof tabId === 'number' && yellowPagesContentTabs.delete(tabId)) {
          TAG('YP', 'content unregistered', { tabId, count: yellowPagesContentTabs.size });
        }
        reply({ ok: true });
        return true;
      }
      case 'yellowpages-request-state': {
        try {
          reply({ ok: true, state: snapshotYellowPagesState() });
        } catch (error) {
          TAG('YP', 'state snapshot failed', error?.message || error);
          reply({ ok: false, error: error?.message || String(error) });
        }
        return true;
      }
      case 'yellowpages-reset-state': {
        const reason = message.reason || null;
        resetYellowPagesState(null);
        persistYellowPagesState();
        broadcastYellowPagesMessage({ type: 'yellowpages-scrape-reset', reason });
        reply({ ok: true });
        return true;
      }
    }

    if (message.type?.startsWith('yellowpages-scrape') && !message.__yellowPagesFanout) {
      updateYellowPagesState(message);
      broadcastYellowPagesMessage(message);
      reply({ ok: true });
      return true;
    }

    if (message.type === 'yellowpages-save-json') {
      const results = Array.isArray(message.results) ? message.results : [];
      const payload = {
        scrapedAt: message.timestamp || Date.now(),
        count: results.length,
        results,
      };
      const json = JSON.stringify(payload, null, 2);
      const safeStamp = new Date(payload.scrapedAt).toISOString().replace(/[:.]/g, '-');
      const filename = `yellowpages-scrape-${safeStamp}.json`;
      const toBase64 = (str) => btoa(unescape(encodeURIComponent(str)));
      const dataUrl = `data:application/json;base64,${toBase64(json)}`;
      try {
        chrome.downloads.download({ url: dataUrl, filename, saveAs: false }, (downloadId) => {
          const err = chrome.runtime.lastError;
          if (err) {
            reply({ ok: false, error: err.message || String(err) });
          } else {
            reply({ ok: true, downloadId, filename });
          }
        });
        return true;
      } catch (error) {
        reply({ ok: false, error: error?.message || String(error) });
        return true;
      }
    }
  });
}
chrome.tabs.onRemoved.addListener((tabId) => {
  if (yelpContentTabs.delete(tabId)) {
    TAG('YELP', 'content tab removed', { tabId, count: yelpContentTabs.size });
  }
  if (yellowPagesContentTabs.delete(tabId)) {
    TAG('YP', 'content tab removed', { tabId, count: yellowPagesContentTabs.size });
  }
  if (emailSenderContentTabs.delete(tabId)) {
    TAG('EMAIL', 'content tab removed', { tabId, count: emailSenderContentTabs.size });
  }
});

/* =============== Email Sender automation =============== */
const EMAIL_SENDER_CONFIG = {
  composeUrl: "https://mail.google.com/mail/u/0/?tab=rm&ogbl#inbox?compose=new",
  delayBetween: 1400,
  focused: false,
  popup: { width: 200, height: 200 }
};

const emailSenderJobs = new Map();
let emailSenderJobSeq = 1;

const COMPOSE_SELECTORS = {
  to: 'input[aria-label="To recipients"], textarea[aria-label="To"], div[aria-label="To"] input',
  subject: 'input[name="subjectbox"], input[aria-label="Subject"]',
  body: 'div[aria-label="Message Body"][contenteditable="true"]',
  send: 'div[role="button"][data-tooltip*="Send"], div[role="button"][aria-label*="Send"]'
};

function nextEmailJobId() {
  return `emailJob-${Date.now()}-${emailSenderJobSeq++}`;
}

function cloneStats(stats) {
  return {
    total: stats.total || 0,
    sent: stats.sent || 0,
    failed: stats.failed || 0
  };
}

function formatEmailError(reason) {
  if (!reason) return "Unknown error";
  if (/gmail_login_required/i.test(reason)) return "Sign in to Gmail in this browser and try again.";
  if (/compose_load_timeout/i.test(reason)) return "Timed out waiting for the Gmail compose window.";
  if (/compose_fields_missing/i.test(reason)) return "Could not locate Gmail compose fields.";
  if (/identity_unavailable|identity_error|identity_exception/i.test(reason)) return "Unable to read Chrome profile email (continuing without it).";
  return reason;
}

function emailSenderBroadcast(type, payload) {
  try {
    const message = { tool: "emailSender", type, ...payload };
    TAG("EMAIL", "broadcast", { type, note: payload?.note, outcome: payload?.outcome, error: payload?.error, stats: payload?.stats });
    safeRuntimeSend(message, 'EMAIL');
    if (emailSenderContentTabs.size && chrome.tabs && typeof chrome.tabs.sendMessage === 'function') {
      for (const tabId of Array.from(emailSenderContentTabs)) {
        chrome.tabs.sendMessage(tabId, message, () => {
          const err = chrome.runtime.lastError;
          if (err && /No tab with id|Receiving end does not exist/i.test(err.message || '')) {
            emailSenderContentTabs.delete(tabId);
          }
        });
      }
    }
  } catch (err) {
    console.warn("[EMAIL] broadcast error", err?.message || err);
  }
}

function getProfileEmail() {
  return new Promise((resolve) => {
    if (!chrome.identity || !chrome.identity.getProfileUserInfo) {
      TAG("EMAIL", "identity unavailable");
      resolve({ email: null, error: "identity_unavailable" });
      return;
    }
    try {
      chrome.identity.getProfileUserInfo({ accountStatus: "ANY" }, (info) => {
        const err = chrome.runtime.lastError;
        if (err) {
          TAG("EMAIL", "identity error", err.message || err);
          resolve({ email: null, error: err.message || "identity_error" });
          return;
        }
        TAG("EMAIL", "identity result", info);
        resolve({ email: info?.email || null, id: info?.id || null });
      });
    } catch (error) {
      TAG("EMAIL", "identity exception", error?.message || error);
      resolve({ email: null, error: error?.message || "identity_exception" });
    }
  });
}

function openComposePopup(url) {
  return new Promise((resolve, reject) => {
    TAG("EMAIL", "opening popup", { url });
    chrome.windows.create({
      url,
      type: "popup",
      focused: EMAIL_SENDER_CONFIG.focused,
      width: EMAIL_SENDER_CONFIG.popup.width,
      height: EMAIL_SENDER_CONFIG.popup.height
    }, (win) => {
      const err = chrome.runtime.lastError;
      if (err || !win) {
        reject(new Error(err?.message || "popup_create_failed"));
        return;
      }

      const finalize = (tab) => {
        if (!tab || typeof tab.id !== "number") {
          reject(new Error("popup_tab_missing"));
          return;
        }
        resolve({ tabId: tab.id, windowId: win.id });
      };

      if (win.tabs && win.tabs[0]) {
        TAG("EMAIL", "popup initial tab", { tabId: win.tabs[0].id, windowId: win.id });
        finalize(win.tabs[0]);
        return;
      }

      chrome.tabs.query({ windowId: win.id }, (tabs) => {
        const queryErr = chrome.runtime.lastError;
        if (queryErr || !tabs || !tabs[0]) {
          reject(new Error(queryErr?.message || "popup_tab_query_failed"));
          return;
        }
        TAG("EMAIL", "popup queried tab", { tabId: tabs[0].id, windowId: win.id });
        finalize(tabs[0]);
      });
    });
  });
}

function waitForTabComplete(tabId, timeoutMs = 30000) {
  return new Promise((resolve, reject) => {
    let finished = false;
    const cleanup = () => {
      if (finished) return;
      finished = true;
      clearTimeout(timer);
      try { chrome.tabs.onUpdated.removeListener(listener); } catch (_) {}
    };

    const timer = setTimeout(() => {
      cleanup();
      reject(new Error("compose_load_timeout"));
    }, timeoutMs);

    const listener = (updatedTabId, changeInfo, tab) => {
      if (updatedTabId !== tabId) return;
      TAG("EMAIL", "tab update", { tabId, status: changeInfo.status, url: changeInfo.url });
      if (changeInfo.url && /accounts\.google\.com/i.test(changeInfo.url)) {
        cleanup();
        reject(new Error("gmail_login_required"));
        return;
      }
      if (changeInfo.status === "complete") {
        cleanup();
        resolve(tab);
      }
    };

    chrome.tabs.onUpdated.addListener(listener);

    chrome.tabs.get(tabId, (tab) => {
      const err = chrome.runtime.lastError;
      if (!err && tab && tab.status === "complete") {
        TAG("EMAIL", "tab already complete", { tabId });
        cleanup();
        resolve(tab);
      }
    });
  });
}

async function waitForComposeElements(tabId, logProgress) {
  let lastDetail = null;
  for (let attempt = 0; attempt < 160; attempt += 1) {
    const result = await execScript(tabId, (selectors) => {
      const locate = (selector) => {
        try {
          return document.querySelector(selector);
        } catch (err) {
          console.warn('compose selector error', selector, err);
          return null;
        }
      };

      const toField = locate(selectors.to);
      const subjectField = locate(selectors.subject);
      const bodyField = locate(selectors.body);
      const sendBtn = locate(selectors.send);

      return {
        ready: Boolean(toField && subjectField && bodyField && sendBtn),
        detail: {
          hasTo: Boolean(toField),
          hasSubject: Boolean(subjectField),
          hasBody: Boolean(bodyField),
          hasSend: Boolean(sendBtn),
          location: window.location.href
        }
      };
    }, [COMPOSE_SELECTORS]);

    const state = Array.isArray(result) ? result[0]?.result : result;
    if (state?.ready) {
      TAG("EMAIL", "compose ready", { tabId, detail: state.detail });
      return state.detail;
    }

    lastDetail = state?.detail || null;
    if (attempt % 20 === 0) {
      TAG("EMAIL", "compose waiting", { tabId, attempt, detail: lastDetail });
      logProgress?.("Waiting for Gmail compose UI...");
    }
    await sleep(250);
  }

  const errorDetail = lastDetail || {};
  TAG("EMAIL", "compose wait timeout", { tabId, detail: errorDetail });
  const error = new Error("compose_fields_missing");
  error.detail = errorDetail;
  throw error;
}

const SCRIPT_FETCH_ERROR_PATTERN = /fetching the script/i;

function execScript(tabId, func, args = [], attempt = 0) {
  return new Promise((resolve, reject) => {
    chrome.scripting.executeScript({ target: { tabId }, func, args }, (results) => {
      const err = chrome.runtime.lastError;
      if (err) {
        const message = err.message || "script_injection_failed";
        const shouldRetry = attempt < 2 && SCRIPT_FETCH_ERROR_PATTERN.test(message);
        if (shouldRetry) {
          setTimeout(() => {
            execScript(tabId, func, args, attempt + 1).then(resolve).catch(reject);
          }, 400);
          return;
        }
        reject(new Error(message));
        return;
      }
      resolve(results);
    });
  });
}

function closePopupQuietly({ tabId, windowId }) {
  if (typeof windowId === "number") {
    chrome.windows.remove(windowId, () => {
      const err = chrome.runtime.lastError;
      if (err && !/No window with id/i.test(err.message || "")) {
        console.warn("[EMAIL] popup remove error", err.message || err);
      }
    });
    TAG("EMAIL", "popup closed", { windowId });
    return;
  }
  if (typeof tabId === "number") {
    chrome.tabs.remove(tabId, () => {
      const err = chrome.runtime.lastError;
      if (err && !/No tab with id/i.test(err.message || "")) {
        console.warn("[EMAIL] tab remove error", err.message || err);
      }
    });
    TAG("EMAIL", "tab closed", { tabId });
  }
}

async function sendViaGmailPopup({ recipient, subject, body, jobId, logProgress }) {
  let popupRef = null;
  let outcome = { ok: false, reason: "unknown_error" };

  try {
    logProgress?.(`Opening Gmail compose popup for ${recipient}...`);
    popupRef = await openComposePopup(EMAIL_SENDER_CONFIG.composeUrl);
    TAG("EMAIL", "popup opened", { jobId, popupRef });
  } catch (err) {
    const reason = err?.message || String(err) || "popup_create_failed";
    TAG("EMAIL", "popup failed", { jobId, reason });
    logProgress?.(`Failed to open Gmail popup: ${reason}`);
    return { ok: false, reason, fatal: true };
  }

  const { tabId, windowId } = popupRef;

  try {
    logProgress?.("Waiting for Gmail compose to finish loading...");
    try {
      await waitForTabComplete(tabId);
      TAG("EMAIL", "compose loaded", { jobId, tabId });
      logProgress?.("Compose UI reported as loaded.");
    } catch (waitErr) {
      const reason = waitErr?.message || String(waitErr) || "compose_load_timeout";
      TAG("EMAIL", "waitForTabComplete warning", { jobId, reason });
      logProgress?.(`Load watcher warning: ${reason}. Continuing to poll for compose UI.`);
    }

    let composeDetail;
    try {
      composeDetail = await waitForComposeElements(tabId, logProgress);
    } catch (composeErr) {
      const reason = composeErr?.message || "compose_fields_missing";
      TAG("EMAIL", "compose readiness failed", { jobId, reason, detail: composeErr?.detail });
      logProgress?.(`Compose UI not ready: ${reason}`);
      throw new Error(reason);
    }

    TAG("EMAIL", "compose detail", { jobId, composeDetail });

    const fillResult = await execScript(tabId, ({ toAddresses, subjectLine, bodyText, selectors }) => {
      const log = (...args) => {
        try {
          console.log('[EMAIL-COMPOSE]', ...args);
        } catch (_) {}
      };
      const locate = (selector) => {
        try {
          return document.querySelector(selector);
        } catch (err) {
          console.warn('[EMAIL-COMPOSE] selector error', selector, err);
          return null;
        }
      };

      const toField = locate(selectors.to);
      const subjectField = locate(selectors.subject);
      const bodyField = locate(selectors.body);
      const sendBtn = locate(selectors.send);

      const detail = {
        hasTo: Boolean(toField),
        hasSubject: Boolean(subjectField),
        hasBody: Boolean(bodyField),
        hasSend: Boolean(sendBtn)
      };

      if (!toField || !subjectField || !bodyField || !sendBtn) {
        log('compose fields missing', detail);
        return { ok: false, reason: 'compose_fields_missing', detail };
      }

      const safeExecCommand = (command, value) => {
        try {
          if (typeof document.execCommand === 'function') {
            return document.execCommand(command, false, value);
          }
        } catch (err) {
          console.warn('[EMAIL-COMPOSE] execCommand error', command, err);
        }
        return false;
      };

      try { toField.focus(); } catch (_) {}
      const selection = window.getSelection();
      if (selection) {
        try { selection.removeAllRanges(); } catch (_) {}
      }
      if (safeExecCommand('selectAll')) {
        safeExecCommand('delete');
      } else {
        toField.value = '';
      }
      toField.dispatchEvent(new Event('input', { bubbles: true }));

      const recipientsValue = (Array.isArray(toAddresses) ? toAddresses : [])
        .filter(Boolean)
        .join(', ');
      if (recipientsValue) {
        const insertedRecipients = safeExecCommand('insertText', recipientsValue);
        if (!insertedRecipients) {
          toField.value = recipientsValue;
          toField.dispatchEvent(new Event('input', { bubbles: true }));
          toField.dispatchEvent(new Event('change', { bubbles: true }));
        }
      }

      try { subjectField.focus(); } catch (_) {}
      subjectField.value = subjectLine;
      subjectField.dispatchEvent(new Event('input', { bubbles: true }));
      subjectField.dispatchEvent(new Event('change', { bubbles: true }));

      try { bodyField.focus(); } catch (_) {}
      if (!safeExecCommand('selectAll')) {
        const selection = window.getSelection();
        if (selection) {
          try {
            const range = document.createRange();
            range.selectNodeContents(bodyField);
            selection.removeAllRanges();
            selection.addRange(range);
          } catch (_) {}
        }
      }
      const insertedBody = safeExecCommand('insertText', bodyText);
      if (!insertedBody) {
        bodyField.innerHTML = '';
        const lines = String(bodyText || '').split(/\r?\n/);
        if (!lines.length) {
          bodyField.innerHTML = '<br>';
        } else {
          lines.forEach((line, index) => {
            if (index > 0) bodyField.appendChild(document.createElement('br'));
            bodyField.appendChild(document.createTextNode(line));
          });
        }
      }
      bodyField.dispatchEvent(new Event('input', { bubbles: true }));
      bodyField.dispatchEvent(new Event('change', { bubbles: true }));

      try { sendBtn.focus(); } catch (_) {}
      sendBtn.click();
      log('compose send triggered');

      return { ok: true, detail };
    }, [{ toAddresses: [recipient], subjectLine: subject, bodyText: body, selectors: COMPOSE_SELECTORS }]);

    const injection = Array.isArray(fillResult) ? fillResult[0]?.result : fillResult;
    TAG("EMAIL", "compose injection result", { jobId, injection });
    if (!injection || !injection.ok) {
      const reason = injection?.reason || "compose_fields_missing";
      TAG("EMAIL", "compose fill failed", { jobId, reason, injection });
      logProgress?.(`Failed to populate compose fields (${reason}).`);
      throw new Error(reason);
    }

    TAG("EMAIL", "compose filled", { jobId, recipient });
    logProgress?.(`Compose fields populated for ${recipient}, clicking send...`);
    await sleep(2000);

    let confirmation = false;
    try {
      const confirmResult = await execScript(tabId, () => {
        const alert = document.querySelector('[role="alert"] span.bAq');
        if (alert && /Message sent/i.test(alert.textContent || '')) return true;
        const snackbar = document.querySelector('div[aria-live="assertive"] span');
        if (snackbar && /Message sent/i.test(snackbar.textContent || '')) return true;
        return false;
      });
      confirmation = Boolean(confirmResult?.[0]?.result);
      TAG("EMAIL", "confirmation result", { jobId, confirmation, confirmResult });
    } catch (confirmErr) {
      TAG("EMAIL", "confirmation check failed", { jobId, error: confirmErr?.message || confirmErr });
      logProgress?.("Unable to confirm Gmail success banner (will assume send went through).");
      confirmation = false;
    }

    outcome = { ok: true, confirmed: confirmation };
    logProgress?.(confirmation ? `Gmail reported "Message sent" for ${recipient}.` : `Sent ${recipient}; Gmail confirmation banner not detected.`);
  } catch (err) {
    const reason = err?.message || "compose_load_failed";
    TAG("EMAIL", "sendViaGmailPopup error", { jobId, reason });
    logProgress?.(`Error while automating Gmail: ${reason}`);
    const fatal = reason === "gmail_login_required";
    outcome = { ok: false, reason, fatal };
  } finally {
    await sleep(800);
    closePopupQuietly(popupRef || {});
    logProgress?.("Closed Gmail popup window.");
  }

  TAG("EMAIL", "sendViaGmailPopup outcome", { jobId, recipient, outcome });
  return outcome;
}

function normalizePersonalizationKey(input) {
  if (input === undefined || input === null) return "";
  let base = String(input);
  if (typeof base.normalize === "function") {
    base = base.normalize("NFKD").replace(/[\u0300-\u036f]/g, "");
  }
  return base
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

function normalizeRecipientPayload(raw) {
  if (typeof raw === "string") {
    const email = raw.trim();
    if (!email) return null;
    return { email, variables: { email } };
  }
  if (!raw || typeof raw !== "object") return null;
  const email = typeof raw.email === "string" ? raw.email.trim() : "";
  if (!email) return null;
  const normalized = {};

  if (raw.variables && typeof raw.variables === "object") {
    Object.entries(raw.variables).forEach(([key, value]) => {
      const normalizedKey = normalizePersonalizationKey(key);
      if (!normalizedKey) return;
      const textValue = typeof value === "string" ? value : value === undefined || value === null ? "" : String(value);
      normalized[normalizedKey] = textValue;
    });
  }

  if (raw.fields && typeof raw.fields === "object") {
    Object.entries(raw.fields).forEach(([key, value]) => {
      const normalizedKey = normalizePersonalizationKey(key);
      if (!normalizedKey || normalized[normalizedKey]) return;
      const textValue = typeof value === "string" ? value : value === undefined || value === null ? "" : String(value);
      normalized[normalizedKey] = textValue;
    });
  }

  if (!normalized.email) normalized.email = email;
  return { email, variables: normalized };
}

function formatRecipientLabel(recipient) {
  if (!recipient || !recipient.email) return "unknown recipient";
  const vars = recipient.variables || {};
  const name = vars["first-name"] || vars.name || vars["full-name"] || vars["company"];
  if (name) return `${name} <${recipient.email}>`;
  return recipient.email;
}

function renderPersonalizedTemplate(template, recipient) {
  if (!template) return "";
  const source = typeof template === "string" ? template : String(template);
  const vars = recipient?.variables || {};
  return source.replace(/\{([^{}]+)\}/g, (match, token) => {
    const normalizedKey = normalizePersonalizationKey(token);
    if (!normalizedKey) return match;
    if (normalizedKey === "email") return recipient?.email || "";
    const value = vars[normalizedKey];
    if (value === undefined || value === null) return "";
    return value;
  });
}

async function runEmailSenderJob(job) {
  const stats = { total: job.recipients.length, sent: 0, failed: 0 };
  TAG("EMAIL", "job start", { jobId: job.jobId, total: stats.total });
  const logProgress = (note) => {
    emailSenderBroadcast("progress", { jobId: job.jobId, stats: cloneStats(stats), note });
  };

  logProgress(`Starting batch of ${stats.total} recipient(s).`);

  let fatalError = null;

  for (const recipient of job.recipients) {
    if (!recipient || !recipient.email) {
      stats.failed += 1;
      logProgress("Skipped an entry that was missing an email address.");
      continue;
    }
    const recipientLabel = formatRecipientLabel(recipient);
    logProgress(`Beginning send workflow for ${recipientLabel}...`);
    const subjectTemplate = job.subjectTemplate || job.subject || "";
    const bodyTemplate = job.bodyTemplate || job.body || "";
    const subjectToSend = subjectTemplate ? renderPersonalizedTemplate(subjectTemplate, recipient) : job.subject;
    const bodyToSend = bodyTemplate ? renderPersonalizedTemplate(bodyTemplate, recipient) : job.body;
    try {
      const result = await sendViaGmailPopup({ recipient: recipient.email, subject: subjectToSend, body: bodyToSend, jobId: job.jobId, logProgress });
      if (result.ok) {
        stats.sent += 1;
        const message = result.confirmed ? `Delivered to ${recipientLabel}.` : `Delivered to ${recipientLabel} (confirmation not available).`;
        logProgress(message);
      } else {
        stats.failed += 1;
        const reason = formatEmailError(result.reason);
        logProgress(`Failed for ${recipientLabel}: ${reason}`);
        if (result.fatal) {
          fatalError = reason;
          break;
        }
      }
    } catch (err) {
      stats.failed += 1;
      const reason = formatEmailError(err?.message || "Unexpected error");
      logProgress(`Error for ${recipientLabel}: ${reason}`);
    }

    emailSenderBroadcast("popupClosed", { jobId: job.jobId, stats: cloneStats(stats) });

    await sleep(EMAIL_SENDER_CONFIG.delayBetween);
  }

  if (fatalError) {
    TAG("EMAIL", "job fatal error", { jobId: job.jobId, fatalError });
    const finalStats = cloneStats(stats);
    emailSenderBroadcast("error", { jobId: job.jobId, stats: finalStats, error: fatalError });
    emailSenderBroadcast("terminated", {
      jobId: job.jobId,
      stats: finalStats,
      note: "Email workflow stopped due to a fatal error."
    });
  } else {
    TAG("EMAIL", "job complete", { jobId: job.jobId, sent: stats.sent, failed: stats.failed });
    const finalStats = cloneStats(stats);
    emailSenderBroadcast("complete", {
      jobId: job.jobId,
      stats: finalStats,
      outcome: `Finished sending. Delivered ${stats.sent} of ${stats.total} email(s); ${stats.failed} failed.`
    });
    emailSenderBroadcast("terminated", {
      jobId: job.jobId,
      stats: finalStats,
      note: "Email workflow finished."
    });
  }

  emailSenderJobs.delete(job.jobId);
}

if (chromeRuntime?.onConnect?.addListener) {
  chromeRuntime.onConnect.addListener((port) => {
    if (!port || typeof port.name !== "string") return;
    if (port.name !== "emailSenderKeepAlive") return;
    TAG("EMAIL", "UI keep-alive connected", { senderUrl: port.sender?.url });
    emailSenderKeepAlivePorts.add(port);
    port.onDisconnect.addListener(() => {
      TAG("EMAIL", "UI keep-alive disconnected", { senderUrl: port.sender?.url });
      emailSenderKeepAlivePorts.delete(port);
    });
  });
}

if (chromeRuntime?.onMessage?.addListener) {
  chromeRuntime.onMessage.addListener((request, sender, sendResponse) => {
    if (!request || request.tool !== "emailSender") return;

    const senderTabId = sender?.tab?.id;
    if (typeof senderTabId === 'number') {
      emailSenderContentTabs.add(senderTabId);
    }

    if (request.action === "whoami") {
      TAG("EMAIL", "whoami request", { from: sender?.url });
      getProfileEmail().then((info) => {
        TAG("EMAIL", "whoami response", info);
        sendResponse?.(info);
      });
      return true;
    }

    if (request.action === "ping") {
      TAG("EMAIL", "ping", { from: sender?.url });
      sendResponse?.({ ok: true, time: Date.now() });
      return false;
    }

    if (request.action === "sendBatch") {
      const payload = request.payload || {};
      const rawRecipients = Array.isArray(payload.recipients) ? payload.recipients : [];
      const normalizedRecipients = rawRecipients
        .map((entry) => normalizeRecipientPayload(entry))
        .filter(Boolean);
      const subject = typeof payload.subject === "string" ? payload.subject : "";
      const bodyTemplate = typeof payload.bodyTemplate === "string"
        ? payload.bodyTemplate
        : (typeof payload.body === "string" ? payload.body : "");

      TAG("EMAIL", "sendBatch request", {
        from: sender?.url,
        recipientCount: normalizedRecipients.length,
        subjectLength: subject.length,
        bodyLength: bodyTemplate.length
      });

      if (!normalizedRecipients.length) {
        TAG("EMAIL", "sendBatch validation failed", { reason: "no_recipients" });
        sendResponse?.({ ok: false, error: "No recipients provided" });
        return;
      }
      if (!subject.trim()) {
        TAG("EMAIL", "sendBatch validation failed", { reason: "missing_subject" });
        sendResponse?.({ ok: false, error: "Missing subject" });
        return;
      }
      if (!bodyTemplate.trim()) {
        TAG("EMAIL", "sendBatch validation failed", { reason: "missing_body" });
        sendResponse?.({ ok: false, error: "Missing body" });
        return;
      }

      getProfileEmail().then((info) => {
        TAG("EMAIL", "profile email", info);
        const jobId = nextEmailJobId();
        const job = {
          jobId,
          recipients: normalizedRecipients,
          subject,
          subjectTemplate: subject,
          body: bodyTemplate,
          bodyTemplate
        };
        emailSenderJobs.set(jobId, job);
        TAG("EMAIL", "job queued", { jobId, count: normalizedRecipients.length });
        runEmailSenderJob(job).catch((err) => {
          TAG("EMAIL", "job execution error", { jobId, error: err?.message || err });
          emailSenderBroadcast("error", {
            jobId,
            stats: { total: normalizedRecipients.length, sent: 0, failed: normalizedRecipients.length },
            error: err?.message || "Unexpected error running job"
          });
          emailSenderJobs.delete(jobId);
        });

        const responseBase = { ok: true, jobId };
        if (info?.email) {
          const response = Object.assign({}, responseBase, { accountEmail: info.email });
          TAG("EMAIL", "sendBatch response", response);
          sendResponse?.(response);
        } else {
          const warning = info?.error ? formatEmailError(info.error) : "Could not determine active Gmail account.";
          const response = Object.assign({}, responseBase, { accountEmail: null, warning });
          TAG("EMAIL", "sendBatch response", response);
          sendResponse?.(response);
        }
      });
      return true;
    }
  });
}
/* =============== Social Link Extractor (popup-based) =============== */
const socialExtractorQueue = [];
let socialExtractorBusy = false;

function normalizeSocialUrl(raw) {
  if (!raw || typeof raw !== "string") return "";
  const trimmed = raw.trim();
  if (!trimmed) return "";
  return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
}

function processSocialExtractorQueue() {
  if (socialExtractorBusy) return;
  const job = socialExtractorQueue.shift();
  if (!job) return;
  socialExtractorBusy = true;
  runSocialExtractorJob(job);
}

function runSocialExtractorJob(job) {
  const targetUrl = normalizeSocialUrl(job.targetUrl);
  const respond = (() => {
    let sent = false;
    return (payload) => {
      if (sent) return;
      sent = true;
      try {
        job.sendResponse(payload);
      } catch (err) {
        console.error("[SOCIAL] sendResponse error", err?.message || err);
      }
    };
  })();

  const finish = (payload) => {
    respond(payload);
    socialExtractorBusy = false;
    processSocialExtractorQueue();
  };

  if (!targetUrl) {
    finish({ error: "invalid_url" });
    return;
  }

  chrome.windows.create({
    url: targetUrl,
    type: "popup",
    width: 200,
    height: 200,
    left: 0,
    top: 0,
    focused: false
  }, (popupWin) => {
    const createErr = chrome.runtime.lastError;
    if (createErr || !popupWin) {
      console.error("[SOCIAL] popup create failed", createErr?.message || createErr || "unknown");
      finish({ error: createErr?.message || "popup_create_failed" });
      return;
    }

    const winId = popupWin.id;

    const closeWindowAndFinish = (payload) => {
      chrome.windows.remove(winId, () => {
        const removeErr = chrome.runtime.lastError;
        if (removeErr && !/No window with id/i.test(removeErr.message || "")) {
          console.warn("[SOCIAL] window remove warning", removeErr.message || removeErr);
        }
        finish(payload);
      });
    };

    chrome.tabs.query({ windowId: winId }, (tabs) => {
      const queryErr = chrome.runtime.lastError;
      if (queryErr) {
        console.error("[SOCIAL] tab query failed", queryErr.message || queryErr);
        closeWindowAndFinish({ error: queryErr.message || "tab_query_failed" });
        return;
      }

      const tab = Array.isArray(tabs) ? tabs[0] : null;
      if (!tab) {
        console.error("[SOCIAL] no tab found in popup window");
        closeWindowAndFinish({ error: "tab_missing" });
        return;
      }

      let extractionInFlight = false;
      let extractionTimeout = null;
      let onUpdated = null;
      let lastScrapedUrl = "";
      let rerunAttempts = 0;
      const maxReruns = 4;

      const hasLinks = (payload) => {
        if (!payload || typeof payload !== "object") return false;
        return Object.values(payload).some(Boolean);
      };

      const scheduleRetry = (delayMs = 1500) => {
        if (rerunAttempts >= maxReruns) return false;
        rerunAttempts += 1;
        if (extractionTimeout) clearTimeout(extractionTimeout);
        extractionTimeout = setTimeout(() => {
          extractionTimeout = null;
          executeExtraction();
        }, delayMs);
        return true;
      };

      const cleanupListeners = () => {
        if (onUpdated) {
          try { chrome.tabs.onUpdated.removeListener(onUpdated); } catch (_) {}
          onUpdated = null;
        }
        if (extractionTimeout) {
          clearTimeout(extractionTimeout);
          extractionTimeout = null;
        }
      };

      const extractLinksFromTab = () => {
        return chrome.scripting.executeScript({
          target: { tabId: tab.id },
          func: () => {
            return new Promise((resolve) => {
              let attempts = 0;
              const maxAttempts = 30;
              const delayMs = 1000;
              const SOC = ["facebook", "instagram", "twitter", "youtube", "linkedin"];
              const found = SOC.reduce((acc, key) => {
                acc[key] = "";
                return acc;
              }, { contact: "" });

              const hasAny = () => Object.values(found).some(Boolean);

              const gatherLinks = (root) => {
                root.querySelectorAll("a[href]").forEach((anchor) => {
                  const href = anchor.href.trim();
                  const lowerHref = href.toLowerCase();
                  SOC.forEach((platform) => {
                    if (!found[platform] && lowerHref.includes(`${platform}.com`)) {
                      found[platform] = href;
                    }
                  });
                  if (!found.contact) {
                    const text = anchor.innerText.trim().toLowerCase();
                    if (lowerHref.includes("/contact") || text.includes("contact")) {
                      found.contact = href;
                    }
                  }
                });
              };

              const parseJsonLd = () => {
                document.querySelectorAll('script[type="application/ld+json"]').forEach((node) => {
                  try {
                    const json = JSON.parse(node.textContent || "{}");
                    const sameAs = json.sameAs || (Array.isArray(json) && json[0]?.sameAs);
                    if (!sameAs) return;
                    const list = Array.isArray(sameAs) ? sameAs : [sameAs];
                    list.forEach((url) => {
                      const lower = String(url || "").toLowerCase();
                      SOC.forEach((platform) => {
                        if (!found[platform] && lower.includes(`${platform}.com`)) {
                          found[platform] = String(url);
                        }
                      });
                    });
                  } catch (_) {
                    /* ignore malformed JSON-LD */
                  }
                });
              };

              const parseRelMe = () => {
                document.querySelectorAll('link[rel~="me"]').forEach((link) => {
                  const href = link.href;
                  const lower = href.toLowerCase();
                  SOC.forEach((platform) => {
                    if (!found[platform] && lower.includes(`${platform}.com`)) {
                      found[platform] = href;
                    }
                  });
                });
              };

              const parseMeta = () => {
                const twitterSite = document.querySelector('meta[name="twitter:site"]');
                if (twitterSite?.content && !found.twitter) {
                  found.twitter = twitterSite.content.trim();
                }
                const seeAlso = document.querySelector('meta[property="og:see_also"]');
                if (seeAlso?.content) {
                  seeAlso.content.split(",").forEach((entry) => {
                    const lower = entry.toLowerCase();
                    SOC.forEach((platform) => {
                      if (!found[platform] && lower.includes(`${platform}.com`)) {
                        found[platform] = entry.trim();
                      }
                    });
                  });
                }
              };

              const regexFallback = () => {
                const html = document.documentElement.innerHTML || "";
                SOC.forEach((platform) => {
                  if (found[platform]) return;
                  const re = new RegExp(`(?:https?:)?\\/\\/[^"'>]*${platform}\\.com[\\w\\./?=&%-]*`, "gi");
                  const match = re.exec(html);
                  if (match) {
                    found[platform] = match[0];
                  }
                });
              };

              const attempt = () => {
                attempts += 1;

                const header = document.querySelector("header");
                if (header) gatherLinks(header);
                const footer = document.querySelector("footer");
                if (footer) gatherLinks(footer);

                parseJsonLd();
                parseRelMe();
                parseMeta();

                if (!hasAny()) gatherLinks(document);
                if (!hasAny()) regexFallback();

                if (hasAny() || attempts >= maxAttempts) {
                  resolve(found);
                } else {
                  setTimeout(attempt, delayMs);
                }
              };

              attempt();
            });
          }
        });
      };

      const executeExtraction = () => {
        if (extractionInFlight) return;
        extractionInFlight = true;
        if (extractionTimeout) {
          clearTimeout(extractionTimeout);
          extractionTimeout = null;
        }

        chrome.tabs.get(tab.id, (info) => {
          const getErr = chrome.runtime.lastError;
          if (getErr) {
            extractionInFlight = false;
            console.error("[SOCIAL] tabs.get error", getErr.message || getErr);
            cleanupListeners();
            closeWindowAndFinish({ error: getErr.message || "tab_access_failed" });
            return;
          }

          const currentHref = info?.url || targetUrl;

          extractLinksFromTab().then((results) => {
            extractionInFlight = false;

            const resultPayload = (Array.isArray(results) ? results[0]?.result : null) || {
              facebook: "",
              instagram: "",
              twitter: "",
              youtube: "",
              linkedin: "",
              contact: ""
            };

            const linksFound = hasLinks(resultPayload);
            lastScrapedUrl = currentHref;

            if (!linksFound) {
              if (scheduleRetry(2000)) {
                return;
              }
            }

            cleanupListeners();
            closeWindowAndFinish(resultPayload);
          }).catch((err) => {
            extractionInFlight = false;
            const message = err?.message || String(err);
            console.error("[SOCIAL] extraction promise rejected", message);

            if (/Frame with ID 0 .*error page/i.test(message)) {
              chrome.tabs.update(tab.id, { url: targetUrl }, () => {
                const updateErr = chrome.runtime.lastError;
                if (updateErr && !/No tab with id/i.test(updateErr.message || "")) {
                  console.warn('[SOCIAL] tab reload warning', updateErr.message || updateErr);
                }
              });
              if (scheduleRetry(1500)) {
                return;
              }
            } else if (/Frame with ID 0 was removed|No frame with id 0|Cannot access/i.test(message)) {
              if (scheduleRetry(1500)) {
                return;
              }
            } else if (scheduleRetry(2000)) {
              return;
            }

            cleanupListeners();
            closeWindowAndFinish({ error: message || "extraction_failed" });
          });
        });
      };

      if (tab.status === "complete") {
        executeExtraction();
      }

      onUpdated = (tabId, changeInfo, updatedTab) => {
        if (tabId !== tab.id || changeInfo.status !== "complete") return;
        const updatedUrl = updatedTab?.url || "";
        if (updatedUrl && updatedUrl === lastScrapedUrl) {
          if (!extractionInFlight) executeExtraction();
          return;
        }
        rerunAttempts = 0;
        if (!extractionInFlight) executeExtraction();
      };

      chrome.tabs.onUpdated.addListener(onUpdated);

      extractionTimeout = setTimeout(() => {
        extractionTimeout = null;
        executeExtraction();
      }, 1000);
    });
  });
}

if (chromeRuntime?.onMessage?.addListener) {
  chromeRuntime.onMessage.addListener((request, sender, sendResponse) => {
    if (!request || request.tool !== "socialLinkExtractor") return;
    const fromUrl = sender?.url || "";
    if (fromUrl && !isAllowedUrl(fromUrl)) {
      sendResponse({ error: "unauthorized_host" });
      return false;
    }

    const targetUrl = request.searchUrl || request.url || "";
    socialExtractorQueue.push({ targetUrl, sendResponse });
    processSocialExtractorQueue();
    return true; // keep sendResponse alive for async completion
  });
}

function isAllowedUrl(u){
  try{
    const parsed = new URL(u);
    if (parsed.protocol === "file:") return true;
    const hostname = parsed.hostname;
    return ALLOWED_HOSTS.includes(hostname) || hostname === "localhost" || hostname === "127.0.0.1";
  }catch{ return false; }
}

async function openPopupForKeyword({ keyword, targetUrl }){
  const baseKeyword = (keyword || "").trim();
  const url = targetUrl || (baseKeyword ? `https://www.google.com/maps/search/${encodeURIComponent(baseKeyword)}` : "https://www.google.com/maps");

  const createWindow = () => new Promise((resolve, reject) => {
    chrome.windows.create({
      url,
      type: "popup",
      focused: true,
      width: 300,
      height: 300,
      left: 10,
      top: 10
    }, async (win) => {
      const err = chrome.runtime.lastError;
      if (err || !win) {
        reject(new Error(err?.message || "Failed to open popup window"));
        return;
      }
      resolve(win);
    });
  });

  const queryFirstTab = (windowId) => new Promise((resolve) => {
    chrome.tabs.query({ windowId }, (tabs) => resolve(tabs?.[0] || null));
  });

  const createTab = () => new Promise((resolve, reject) => {
    chrome.tabs.create({ url, active: false }, (tab) => {
      const err = chrome.runtime.lastError;
      if (err || !tab) {
        reject(new Error(err?.message || "Failed to open tab"));
        return;
      }
      resolve(tab);
    });
  });

  try {
    const win = await createWindow();
    const tab = (win.tabs && win.tabs[0]) || await queryFirstTab(win.id);
    if (!tab) {
      try { await chrome.windows.remove(win.id); } catch {}
      throw new Error("No tab in popup window");
    }
    return { windowId: win.id, tabId: tab.id, url, via: "window" };
  } catch (windowErr) {
    TAG("POPUP", "window create failed", windowErr?.message || windowErr);
    try {
      const tab = await createTab();
      return { windowId: null, tabId: tab.id, url, via: "tab" };
    } catch (tabErr) {
      const msg = tabErr?.message || windowErr?.message || "Failed to open Google Maps";
      return { error: msg, url };
    }
  }
}

async function closePopup(winId, tabId){
  let closed = false;

  async function closeWindow(id){
    return new Promise((resolve) => {
      try{
        chrome.windows.remove(id, () => {
          const err = chrome.runtime.lastError;
          if (err && !/No window with id/i.test(err.message || "")) {
            TAG("POPUP","window close failed", err.message || err);
            resolve(false);
            return;
          }
          resolve(true);
        });
      }catch(e){
        TAG("POPUP","window close threw", e?.message || e);
        resolve(false);
      }
    });
  }

  async function closeTab(id){
    return new Promise((resolve) => {
      try{
        chrome.tabs.remove(id, () => {
          const err = chrome.runtime.lastError;
          if (err && !/No tab with id/i.test(err.message || "")) {
            TAG("POPUP","tab close failed", err.message || err);
            resolve(false);
            return;
          }
          resolve(true);
        });
      }catch(e){
        TAG("POPUP","tab close threw", e?.message || e);
        resolve(false);
      }
    });
  }

  if(winId){
    closed = await closeWindow(winId);
  }
  if(!closed && tabId){
    closed = await closeTab(tabId);
  }
  return closed;
}


async function applyStealthMask(tabId){
  if (tabId == null) return false;
  try {
    await chrome.scripting.executeScript({
      target: { tabId },
      func: () => {
        try {
          const patchFocus = () => {
            const forceDefine = (obj, prop, value) => {
              try {
                Object.defineProperty(obj, prop, {
                  configurable: true,
                  get: () => value
                });
              } catch {}
            };

            forceDefine(document, 'hidden', false);
            forceDefine(document, 'visibilityState', 'visible');
            forceDefine(document, 'webkitVisibilityState', 'visible');
            try {
              document.hasFocus = () => true;
            } catch {}

            const suppress = (ev) => {
              try { ev.stopImmediatePropagation(); } catch {}
            };

            try { window.addEventListener('blur', suppress, true); } catch {}
            try { document.addEventListener('visibilitychange', suppress, true); } catch {}
          };

          patchFocus();
          setInterval(patchFocus, 3000);
        } catch {}
      }
    });
    return true;
  } catch (err) {
    TAG("POPUP", "stealth mask failed", err?.message || err);
    return false;
  }
}

function looksLikeMapsUrl(url){
  if (!url) return false;
  try {
    const parsed = new URL(url);
    if (/accounts\.google\.com/i.test(parsed.hostname)) return false;
    if (!/\.google\./i.test(parsed.hostname)) return false;
    return parsed.pathname.startsWith('/maps');
  } catch {
    return false;
  }
}

function looksLikeGoogleAccountUrl(url){
  if (!url) return false;
  try {
    const parsed = new URL(url);
    return /accounts\.google\.com/i.test(parsed.hostname);
  } catch {
    return false;
  }
}

async function ensureMapsContext(tabId, desiredUrl, attempts = 3){
  for (let i = 0; i < attempts; i++){
    const tab = await safeGetTab(tabId);
    const currentUrl = tab?.url || "";
    if (looksLikeMapsUrl(currentUrl)) return true;
    if (!desiredUrl){
      TAG("POPUP", "ensureMapsContext: missing desiredUrl", currentUrl);
      return false;
    }
    TAG("POPUP", "ensureMapsContext: reloading", { attempt: i + 1, currentUrl });
    try {
      await chrome.tabs.update(tabId, { url: desiredUrl });
      await applyStealthMask(tabId);
    } catch (err) {
      TAG("POPUP", "ensureMapsContext update failed", err?.message || err);
      await sleep(600);
    }
    const loaded = await waitTabComplete(tabId, 45000);
    if (!loaded){
      TAG("POPUP", "ensureMapsContext: reload timeout", { attempt: i + 1 });
    }
  }
  const finalTab = await safeGetTab(tabId);
  const finalUrl = finalTab?.url || "";
  return looksLikeMapsUrl(finalUrl);
}

function attachPopupTabWatcher(job, desiredUrl){
  if (!job || !job.popupTabId || job.popupTabWatcher) return;
  if (!desiredUrl) return;
  let guardReloads = 0;
  const MAX_RELOADS = 10;
  const listener = (tabId, changeInfo, tab) => {
    if (tabId !== job.popupTabId) return;
    const url = changeInfo.url || tab?.url || "";
    if (!url) return;
    if (looksLikeMapsUrl(url)) {
      guardReloads = 0;
      return;
    }
    if (!looksLikeGoogleAccountUrl(url)) return;
    if (guardReloads >= MAX_RELOADS) {
      TAG("POPUP", "tab guard exhausted", { url });
      return;
    }
    guardReloads++;
    TAG("POPUP", "tab guard redirect", { attempt: guardReloads, url });
    const revive = async () => {
      try {
        await chrome.tabs.update(tabId, { url: desiredUrl });
        await applyStealthMask(tabId);
      } catch (err) {
        TAG("POPUP", "tab guard revive failed", err?.message || err);
      }
    };
    revive();
  };
  chrome.tabs.onUpdated.addListener(listener);
  job.popupTabWatcher = listener;
}

function getPortForJob(job){
  if(!job) return null;
  if(job.portTabId != null){
    const mapped = portsByTab.get(job.portTabId);
    if (mapped) return mapped;
  }
  return job.portRef || null;
}

function sendToFrontend(job, payload){
  const port = getPortForJob(job);
  if(port){
    try{
      port.postMessage(payload);
      return true;
    }catch(err){
      TAG("PORT","postMessage failed", err?.message || err);
      if (job) job.portRef = null;
    }
  }

  const tabId = job?.portTabId ?? job?.portRef?.sender?.tab?.id ?? null;
  if(tabId != null){
    try{
      chrome.tabs.sendMessage(tabId, payload, () => {
        const err = chrome.runtime.lastError;
        if(err){
          TAG("PORT","sendMessage fallback error", err.message || err);
        }
      });
      return true;
    }catch(err){
      TAG("PORT","sendMessage fallback threw", err?.message || err);
    }
  }

  TAG("PORT","no active route for job", { jobId: job?.id, queue: job?.queueLabel });
  return false;
}

function sendToTabDirect(tabId, payload){
  if (tabId == null) return false;
  try{
    chrome.tabs.sendMessage(tabId, payload, () => {
      const err = chrome.runtime.lastError;
      if (err){
        TAG("PORT","direct send error", err.message || err);
      }
    });
    return true;
  }catch(err){
    TAG("PORT","direct send threw", err?.message || err);
    return false;
  }
}

function notifyJobError(job, message){
  if(!job) return;
  sendToFrontend(job, {
    type: "EXT_SCRAPER_EVENT",
    tool: "mapsScraper",
    queue: job.queueLabel,
    event: {
      type: "SCRAPE_ERROR",
      keyword: job.queueLabel,
      message: message || "error"
    }
  });
}

function updateKeepAliveAlarm(){
  if(activeJobCount > 0){
    chrome.alarms.create(KEEP_ALIVE_ALARM, { periodInMinutes: 0.5 });
  } else {
    chrome.alarms.clear(KEEP_ALIVE_ALARM);
  }
}

async function cleanupJob(jobId, { closePopupWindow = true } = {}){
  const job = jobs.get(jobId);
  if(!job) return;
  if (job.popupTabWatcher){
    try { chrome.tabs.onUpdated.removeListener(job.popupTabWatcher); } catch {}
    job.popupTabWatcher = null;
  }
  if(closePopupWindow && (job.popupWindowId || job.popupTabId)){
    const closed = await closePopup(job.popupWindowId, job.popupTabId);
    if(!closed){
      TAG("POPUP","cleanup close retry failed", { jobId, windowId: job.popupWindowId, tabId: job.popupTabId });
    }
  }
  job.popupWindowId = null;
  job.popupTabId = null;
  job.portRef = null;
  job.portTabId = null;
  jobs.delete(jobId);
  removeJobSnapshot(jobId);
  if(activeJobCount > 0) activeJobCount--;
  updateKeepAliveAlarm();
}

chrome.alarms.onAlarm.addListener((alarm)=>{
  if(alarm.name === KEEP_ALIVE_ALARM){
    TAG("KEEPALIVE","ping", Date.now());
  }
});

function waitTabComplete(tabId, timeout=55000){
  return new Promise((resolve)=>{
    let done=false;
    const timer=setTimeout(()=>{ if(done) return; done=true; TAG("WATCHDOG","tab load timeout"); resolve(false); }, timeout);
    const onUpd=(id,info)=>{
      if(id!==tabId) return;
      if(info.status==="complete"){ if(done) return; done=true; clearTimeout(timer); chrome.tabs.onUpdated.removeListener(onUpd); resolve(true); }
    };
    chrome.tabs.onUpdated.addListener(onUpd);
  });
}
async function safeGetTab(tabId){ try{ return await chrome.tabs.get(tabId); }catch{ return null; } }

async function persistJobSnapshots(){
  try{
    const payload = {};
    for (const [jobId, snap] of jobSnapshots.entries()){
      payload[jobId] = snap;
    }
    await chrome.storage.local.set({ [JOB_SNAPSHOT_KEY]: payload });
  }catch(err){
    TAG("JOBSTORE","persist error", err?.message || err);
  }
}

function scheduleSnapshotPersist(){
  if (snapshotSaveTimer){
    clearTimeout(snapshotSaveTimer);
  }
  snapshotSaveTimer = setTimeout(() => {
    snapshotSaveTimer = null;
    persistJobSnapshots();
  }, 200);
}

function snapshotFromJob(job){
  return {
    id: job.id,
    queueLabel: job.queueLabel || "",
    keyword: job.keyword || "",
    options: job.options || {},
    portTabId: job.portTabId ?? null,
    portSenderUrl: job.portSenderUrl || "",
    popupWindowId: job.popupWindowId ?? null,
    popupTabId: job.popupTabId ?? null,
    targetUrl: job.targetUrl || null,
    savedThisKeyword: job.savedThisKeyword || 0,
    createdAt: job.createdAt || Date.now()
  };
}

function syncJobSnapshot(job){
  if (!job || !job.id) return;
  job.createdAt = job.createdAt || Date.now();
  jobSnapshots.set(job.id, snapshotFromJob(job));
  scheduleSnapshotPersist();
}

function removeJobSnapshot(jobId){
  if (!jobId) return;
  if (jobSnapshots.delete(jobId)){
    scheduleSnapshotPersist();
  }
}

function reviveJobFromSnapshot(snapshot){
  return {
    id: snapshot.id,
    queueLabel: snapshot.queueLabel || "",
    keyword: snapshot.keyword || "",
    options: snapshot.options || {},
    portTabId: snapshot.portTabId ?? null,
    portRef: null,
    portSenderUrl: snapshot.portSenderUrl || "",
    popupWindowId: snapshot.popupWindowId ?? null,
    popupTabId: snapshot.popupTabId ?? null,
    popupTabWatcher: null,
    savedThisKeyword: snapshot.savedThisKeyword || 0,
    targetUrl: snapshot.targetUrl || null,
    createdAt: snapshot.createdAt || Date.now(),
    _revived: true
  };
}

async function restoreJobSnapshots(){
  try{
    const stored = await chrome.storage.local.get(JOB_SNAPSHOT_KEY);
    const raw = stored[JOB_SNAPSHOT_KEY] || {};
    const now = Date.now();
    let changed = false;
    jobSnapshots = new Map();
    for (const [jobId, snapshot] of Object.entries(raw)){
      if (!snapshot || typeof snapshot !== "object"){ changed = true; continue; }
      if (snapshot.createdAt && (now - snapshot.createdAt) > JOB_SNAPSHOT_TTL_MS){ changed = true; continue; }
      if (snapshot.portTabId != null){
        const tab = await safeGetTab(snapshot.portTabId);
        if (!tab){ changed = true; continue; }
      }
      jobSnapshots.set(jobId, snapshot);
      if (!jobs.has(jobId)){
        const revived = reviveJobFromSnapshot(snapshot);
        jobs.set(jobId, revived);
      }
    }
    activeJobCount = jobs.size;
    updateKeepAliveAlarm();
    if (changed){
      await persistJobSnapshots();
    }
  }catch(err){
    TAG("JOBSTORE","restore error", err?.message || err);
  }
}

async function ensureJob(jobId){
  if (!jobId) return null;
  const existing = jobs.get(jobId);
  if (existing) return existing;
  try{ await snapshotsReady; }catch{}
  const stored = jobSnapshots.get(jobId);
  if (!stored) return null;
  const revived = reviveJobFromSnapshot(stored);
  jobs.set(jobId, revived);
  syncJobSnapshot(revived);
  activeJobCount = jobs.size;
  updateKeepAliveAlarm();
  return revived;
}

snapshotsReady = restoreJobSnapshots();

async function injectWithRetry(tabId, func, args, label, max=6){
  for(let i=1;i<=max;i++){
    const tab = await safeGetTab(tabId);
    if(!tab){ TAG("INJECT", `${label}: tab missing`); return { error:"tab_missing" }; }
    try{
      TAG("INJECT", `${label}: attempt ${i}/${max}`);
      const [{ result } = {}] = await chrome.scripting.executeScript({
        target: { tabId }, world: "ISOLATED", func, args
      });
      if(result && !result.error) return result;
      if(result?.error){ TAG("INJECT", `${label}: returned error`, result.error); return result; }
    }catch(e){
      const msg=String(e?.message || e);
      TAG("INJECT", `${label}: error`, msg);
      if(/Frame with ID 0 was removed|No frame with id 0|Cannot access/.test(msg)){ await sleep(800); continue; }
      return { error: msg };
    }
    await sleep(500);
  }
  return { error: `${label}: inject_retries_exhausted` };
}

/* ========================= injected worker ========================= */
function MAPS_SEQUENTIAL_WORKER(options, jobId, queueLabel, searchUrl, portTabId){
  const targetTabId = portTabId ?? options?.targetTabId ?? null;
  const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));
  const FEED_SEL='div[role="feed"]';
  const PLACE_NAME_SEL='h1 span.DUwDvf, div.zvLtDc';
  const DEFAULT_SUPPRESS_FIRST = 7;
  const MAX_SUPPRESS_FIRST = 1000;
  const SUPPRESS_FIRST = (() => {
    if (options && options.skipFirst != null) {
      const raw = options.skipFirst;
      if (typeof raw === 'number' && Number.isFinite(raw)) {
        return Math.max(0, Math.min(MAX_SUPPRESS_FIRST, Math.floor(raw)));
      }
      const str = String(raw).trim();
      if (str) {
        const parsed = Number(str);
        if (Number.isFinite(parsed)) {
          return Math.max(0, Math.min(MAX_SUPPRESS_FIRST, Math.floor(parsed)));
        }
      }
    }
    return DEFAULT_SUPPRESS_FIRST;
  })();
  const keywordLabel = queueLabel || (options && options.keyword) || (options && options.queue) || "";
  const runtimeSend = (message) => {
    try {
      chrome.runtime.sendMessage(message, () => {
        const err = chrome.runtime.lastError;
        if (err && !/Receiving end does not exist|No tab with id|The message port closed/i.test(err.message || '')) {
          try { console.warn('[GMB worker] sendMessage failed', err.message || err); } catch (_) {}
        }
      });
    } catch (err) {
      try { console.warn('[GMB worker] sendMessage threw', err?.message || err); } catch (_) {}
    }
  };

  const postEvent = (type, payload = {}) => {
    runtimeSend({
      __gmb_evt: true,
      jobId,
      targetTabId,
      event: Object.assign({ type, keyword: keywordLabel, queue: queueLabel }, payload || {})
    });
  };

  const dbg=[]; const log=(...a)=>{ try{console.debug("[STEP]",...a);}catch{} dbg.push(a.map(String).join(" ")); };
  console.log("[SEQ] Mode: scrape from 1st → last; hide first", SUPPRESS_FIRST, "rows from UI");

  const SEARCH_URL = searchUrl || (queueLabel ? `https://www.google.com/maps/search/${encodeURIComponent(queueLabel)}` : null);

  const postRow = (row) => runtimeSend({ __gmb_stream: true, jobId, queue: queueLabel, row, targetTabId });
  const postLog = (msg) => runtimeSend({ __gmb_log: true, jobId, msg, targetTabId });

  try {
    window.addEventListener('keydown', (ev) => {
      if (ev.defaultPrevented) return;
      const key = (ev.key || "").toLowerCase();
      if (key === '/' || (key === 'l' && (ev.ctrlKey || ev.metaKey))) {
        ev.preventDefault();
        ev.stopImmediatePropagation();
      }
    }, true);
  } catch {}

  const feed=()=>document.querySelector(FEED_SEL);
  const text=(el)=> (el?.innerText||el?.textContent||"").trim();
  const isCaptcha=()=>{ const t=(document.body?.innerText||"").toLowerCase(); return t.includes("unusual traffic")||t.includes("not a robot")||!!document.querySelector('iframe[src*="recaptcha"]'); };

  function scrollSidePaneToTop(){
    try { window.scrollTo(0,0); } catch {}
    const panes = document.querySelectorAll('div.m6QErb, div.m6QErb.XiKgde');
    panes.forEach(p => { try { p.scrollTo({ top: 0, behavior: 'auto' }); } catch {} });
  }

  async function clickDeep(el){
    if(!el) return false;
    try{
      const r = el.getBoundingClientRect();
      ["pointerdown","mousedown","pointerup","mouseup","click"].forEach(type =>
        el.dispatchEvent(new MouseEvent(type,{bubbles:true,cancelable:true,clientX:r.left+r.width/2,clientY:r.top+r.height/2}))
      );
    }catch{ try{ el.click?.(); }catch{} }
    await sleep(300);
    return true;
  }

  /* ------------------- Back-arrow detection ------------------- */
  function findBackArrowCandidates(){
    const out = [];
    const prefs = [
      'button[aria-label="Back"]',
      'button[aria-label*="Back to results" i]',
      'button[aria-label*="Back" i]',
      'button[jsaction*="pane.back"]',
      'button[jsaction*="pane.results.back"]',
      'button[jsaction*="pane.place.backToList"]',
      'button[jsaction*="pane.place.back"]',
      'button[jsaction*="pane.place.exit"]',
      'button[jsaction*="pane.place.close"]',
      'div[role="button"][aria-label*="Back" i]'
    ];
    prefs.forEach(sel=>{
      const el = document.querySelector(sel);
      if (el && el.offsetParent !== null && !el.disabled) out.push(el);
    });
    // New icon
    const iconNew = [...document.querySelectorAll('span.google-symbols')].find(s => {
      const t = (s.textContent||'').trim();
      return t && t.charCodeAt(0) === 0xE5C4; // arrow_back
    });
    if (iconNew) {
      const btn = iconNew.closest('button,[role="button"]');
      if (btn) out.push(btn);
      out.push(iconNew.parentElement || iconNew);
    }
    // Legacy icon
    const iconLegacy = [...document.querySelectorAll('span.google-symbols.G47vBd')].find(s => (s.textContent||'').includes(''));
    if (iconLegacy){
      const btn = iconLegacy.closest('button,[role="button"]');
      if (btn) out.push(btn);
      if (iconLegacy.parentElement) out.push(iconLegacy.parentElement);
      out.push(iconLegacy);
    }
    document.querySelectorAll('button[aria-label="Close"], button[aria-label*="Close" i], div[role="button"][aria-label*="Close" i]').forEach((el)=>{
      if (el && el.offsetParent !== null && !el.disabled) out.push(el);
    });
    const a = document.querySelector('a[href*="/maps/search/"]');
    if (a) out.push(a);
    return out.filter((el, idx, arr) => el && arr.indexOf(el) === idx);
  }

  /* ---------------- feed-scoped list detection ---------------- */
  function feedCardCountStrict(){
    const f=feed(); if(!f) return 0;
    const strict = f.querySelectorAll(".Nv2PK,[role='article'][data-result-index]").length;
    if (strict) return strict;
    return f.querySelectorAll(".Nv2PK,[role='article']").length;
  }
  function atEndOfList(){
    const f=feed(); if(!f) return false;
    const end=f.querySelector('span.HlvSq'); if(!end) return false;
    const s=text(end).replace(/[’]/g,"'");
    return /you'?ve reached the end of the list\.?/i.test(s);
  }
  function looksLikeResults(){
    const f = feed(); if (!f) return false;
    if (feedCardCountStrict() >= 1) return true;
    if (atEndOfList()) return true;
    return !!f.querySelector('div[role="complementary"], div[aria-label*="Results" i]');
  }
  function isPlacePanel(){
    if (looksLikeResults()) return false;
    const nameEl = document.querySelector(PLACE_NAME_SEL);
    if (!nameEl) return false;
    const tt = text(nameEl);
    if (/^results\b/i.test(tt)) return false;
    return true;
  }

  /* ---------------- keys & helpers ---------------- */
  function keyOfHref(href){
    try{
      const u=new URL(href, location.href);
      const cid=u.searchParams.get("cid"); if(cid) return "cid:"+cid;
      const m=u.pathname.match(/\/maps\/place\/([^/]+)/);
      return m ? "place:"+decodeURIComponent(m[1]) : u.href;
    }catch{ return href||""; }
  }
  function cardKey(card){
    const cid = card.getAttribute("data-cid") || card.getAttribute("data-id") || card.getAttribute("data-result-id");
    if(cid) return "cid:"+cid;
    const idx = card.getAttribute("data-result-index") || "";
    const nm = (text(card.querySelector('.qBF1Pd, .NrDZNb, .fontHeadlineSmall, [role="heading"], .lI9IFe'))||"").toLowerCase().slice(0,80);
    return `card:${idx}:${nm}`;
  }
  function keyForNode(node){
    return node && node.tagName === "A" ? keyOfHref(node.href) : cardKey(node.closest(".Nv2PK,[role='article']")||node);
  }
  function norm(s){ return (s||"").toLowerCase().replace(/\s+/g," ").trim(); }
  function cidFromHref(href){
    try{
      const u = new URL(href, location.href);
      return u.searchParams.get("cid") || u.searchParams.get("ftid") || "";
    }catch{ return ""; }
  }
  function cidFromNode(node){
    if(!node) return "";
    const card = node.closest(".Nv2PK,[role='article']") || node;
    const attrCid = card?.getAttribute("data-cid") || card?.getAttribute("data-id");
    if (attrCid) return String(attrCid).trim();
    const a = node.tagName==="A" ? node :
              card.querySelector('a[href*="/maps/place/"], a[href*="/maps?cid="], a[href*="maps.google"][href*="cid="]');
    return a ? cidFromHref(a.href) : "";
  }
  function approxKeyFromCard(node){
    const card = node.closest(".Nv2PK,[role='article']") || node;
    const T    = el => (el?.innerText || el?.textContent || "").trim();
    const name = T(card.querySelector('.qBF1Pd, .NrDZNb, .fontHeadlineSmall, .lI9IFe, [role="heading"]'));
    const addr = T(card.querySelector('.W4Efsd .Io6YTe, .rllt__details div:nth-child(3), .UY7F9 ~ span'));
    const cat  = T(card.querySelector('.W4Efsd, .rllt__details div:nth-child(2)'));
    const part = addr || cat || "";
    if (!name) return "";
    return norm(name) + "|" + norm(part);
  }

  /* ---------- ORDERED candidates (stable for main picking) ---------- */
  function orderedCandidatesFromFeed(){
    const f = feed(); if(!f) return [];
    const cards = [...f.querySelectorAll(".Nv2PK,[role='article']")]
      .filter(el => !/\bads?\b|sponsored/i.test((el.innerText||"").toLowerCase()))
      .map(card => {
        const a = card.querySelector('a[href*="/maps/place/"], a[href*="/maps?cid="]') ||
                  card.querySelector('a[href*="maps.google"][href*="cid="], a[href^="/maps/place"]');
        const node = a || card;
        const key  = keyForNode(node);
        const cid  = cidFromNode(node);
        const approx = approxKeyFromCard(node);
        const idxAttr = card.getAttribute("data-result-index");
        const idx = (idxAttr && /^\d+$/.test(idxAttr)) ? parseInt(idxAttr,10) : null;
        const top = card.getBoundingClientRect().top;
        return { node, key, cid, approx, idx, top };
      });

    cards.sort((a,b)=>{
      if (a.idx!=null && b.idx!=null) return a.idx - b.idx;
      if (a.idx!=null && b.idx==null) return -1;
      if (a.idx==null && b.idx!=null) return 1;
      return a.top - b.top;
    });
    return cards;
  }

  const PLACE_GUARD_INTERVAL_MS = 1200;
  const PLACE_GUARD_MAX_RECOVERIES = 3;
  let placeGuardTimer = null;
  const placeGuardState = {
    active: false,
    inFlight: false,
    hardFail: false,
    failures: 0,
    intent: null
  };

  function normalizeMapsUrl(href){
    if(!href) return null;
    try { return new URL(href, location.origin).href; } catch { return href; }
  }

  function canonicalPlaceHref(){
    const canonical = document.querySelector('link[rel="canonical"][href*="/maps/"]');
    if (canonical?.href) return canonical.href;
    const meta = document.querySelector('meta[itemprop="url"][content*="/maps/"]');
    if (meta?.content) return meta.content;
    const share = document.querySelector('a[href*="/maps/place/"][data-value], a[href*="/maps?cid="]');
    if (share?.href) return share.href;
    if (/\/maps\//.test(location.pathname)) return location.href;
    return null;
  }

  function appendIntentUrl(list, candidate){
    const normalized = normalizeMapsUrl(candidate);
    if (!normalized) return;
    if (!list.includes(normalized)) list.push(normalized);
  }

  function buildIntentFromPick(pick){
    const intent = {
      key: pick?.key || "",
      approx: pick?.approx || "",
      cid: pick?.cid || "",
      urls: []
    };
    const raw = pick?.node?.href || pick?.node?.getAttribute?.('href');
    appendIntentUrl(intent.urls, raw);
    if (!intent.cid && intent.urls.length){
      intent.cid = cidFromHref(intent.urls[0]) || "";
    }
    if (intent.cid){
      appendIntentUrl(intent.urls, `https://www.google.com/maps?cid=${intent.cid}`);
      appendIntentUrl(intent.urls, `https://maps.google.com/?cid=${intent.cid}`);
    }
    if (!intent.key){
      intent.key = intent.cid || (intent.urls[0] || "");
    }
    return intent;
  }

  function updateIntentFromDocument(intent){
    if (!intent) return;
    const canon = canonicalPlaceHref();
    if (canon) appendIntentUrl(intent.urls, canon);
    if (!intent.cid && canon){
      const cid = cidFromHref(canon);
      if (cid) intent.cid = cid;
    }
  }

  async function recoverToIntent(intent){
    if (!intent || !intent.urls.length) return false;
    const targets = intent.urls.slice();
    if (intent.cid){
      appendIntentUrl(targets, `https://www.google.com/maps?cid=${intent.cid}`);
      appendIntentUrl(targets, `https://maps.google.com/?cid=${intent.cid}`);
    }
    for (const target of targets){
      if (!target) continue;
      if (location.href === target && !looksLikeResults()){
        return true;
      }
      try { history.replaceState?.(null, '', target); } catch {}
      try { location.href = target; }
      catch { try { location.assign(target); } catch {} }
      await sleep(350);
      const ok = await waitPlace(6000);
      if (ok && !looksLikeResults()){
        updateIntentFromDocument(intent);
        return true;
      }
    }
    return false;
  }

  async function guardRecoverIntent(intent, attemptLabel){
    const success = await recoverToIntent(intent);
    if (!success){
      placeGuardState.failures++;
      if (placeGuardState.failures === 1 || placeGuardState.failures >= PLACE_GUARD_MAX_RECOVERIES){
        postLog(`[GUARD] recovery attempt ${placeGuardState.failures} failed (${intent?.key||'unknown'})`);
      }
      if (placeGuardState.failures >= PLACE_GUARD_MAX_RECOVERIES){
        placeGuardState.hardFail = true;
        stopPlaceGuard();
      }
    }else{
      placeGuardState.failures = 0;
    }
    return success;
  }

  async function placeGuardTick(){
    if (!placeGuardState.active || placeGuardState.inFlight) return;
    if (!looksLikeResults()) return;
    placeGuardState.inFlight = true;
    try {
      await guardRecoverIntent(placeGuardState.intent, 'tick');
    } catch (err){
      postLog(`[GUARD] recovery exception: ${err?.message || err}`);
    } finally {
      placeGuardState.inFlight = false;
    }
  }

  async function ensurePlaceGuard(intent){
    stopPlaceGuard();
    placeGuardState.intent = intent || null;
    placeGuardState.failures = 0;
    placeGuardState.hardFail = false;
    if (!intent){
      return { ok: true };
    }
    const ready = !looksLikeResults() ? true : await guardRecoverIntent(intent, 'initial');
    if (!ready){
      placeGuardState.hardFail = true;
      return { ok: false };
    }
    updateIntentFromDocument(intent);
    placeGuardState.active = true;
    placeGuardTimer = setInterval(() => {
      placeGuardTick().catch(err => postLog(`[GUARD] tick error: ${err?.message || err}`));
    }, PLACE_GUARD_INTERVAL_MS);
    return { ok: true };
  }

  function stopPlaceGuard(){
    if (placeGuardTimer){
      clearInterval(placeGuardTimer);
      placeGuardTimer = null;
    }
    placeGuardState.active = false;
    placeGuardState.inFlight = false;
    placeGuardState.intent = null;
  }

  function placeGuardFailed(){
    return !!placeGuardState.hardFail;
  }

  async function waitPlaceDetailsSettled(timeout=2200){
    const t0 = Date.now();
    while(Date.now()-t0 < timeout){
      if (!looksLikeResults() && document.querySelector(PLACE_NAME_SEL)){
        const infoReady = document.querySelector('div[role="main"] .Io6YTe, div[aria-label*="Website" i], div[role="main"] a[href^="tel:"]');
        if (infoReady) return true;
      }
      await sleep(120);
    }
    return false;
  }

  /* ---------------- waiting & navigation helpers ---------------- */
  async function waitResults(timeout=35000){
    const t0=Date.now();
    while(Date.now()-t0<timeout){
      if(looksLikeResults()) return true;
      await sleep(160);
    }
    return false;
  }
  async function waitPlace(timeout=24000){
    const t0=Date.now();
    while(Date.now()-t0<timeout){
      if(isPlacePanel()) return true;
      await sleep(160);
    }
    return false;
  }
  async function ensureBackToList(timeout=14000){
    const t0=Date.now();
    while(Date.now()-t0<timeout){
      if(looksLikeResults()) return true;
      await sleep(200);
    }
    return false;
  }

  async function gentleFeedNudge(){
    const f = feed();
    if (!f) { window.scrollBy(0, Math.max(600, Math.floor(window.innerHeight*0.9))); await sleep(360); return; }
    const before = { top:f.scrollTop, h:f.scrollHeight };
    f.scrollBy(0, Math.max(520, Math.floor(f.clientHeight*0.7)));
    await sleep(520);
    if (Math.abs(f.scrollTop - before.top) < 6 && f.scrollHeight === before.h){
      const lastCard=f.querySelector(".Nv2PK:last-child, [role='article']:last-child");
      lastCard?.scrollIntoView({block:"center"}); await sleep(420);
    }
  }

  async function clickBackToListOnce(){
    for (let guard=0; guard<3; guard++){
      if (document.querySelector('div[role="dialog"] div.W0fu2b') ||
          document.querySelector('div[role="dialog"] [aria-label^="Photo -"]') ||
          document.querySelector('div[role="dialog"] [data-photo-index]') ||
          document.querySelector('div[role="dialog"] button[aria-label*="Close" i]')){
        const closeBtn =
          document.querySelector('div[role="dialog"] button[aria-label="Close"]') ||
          document.querySelector('div[role="dialog"] button[aria-label*="Close" i]') ||
          document.querySelector('div[role="dialog"] button[jsaction*="viewer.close"]');
        if (closeBtn){ try { closeBtn.click(); } catch {} await sleep(350); }
        document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',keyCode:27,which:27,bubbles:true}));
        document.dispatchEvent(new KeyboardEvent('keyup'  ,{key:'Escape',keyCode:27,which:27,bubbles:true}));
        await sleep(350);
        continue;
      }
      if (document.querySelector('a.OKAoZd')){
        const galleryBack =
          document.querySelector('button[aria-label*="Back" i]') ||
          document.querySelector('button[jsaction*="pane.media.back"], button[jsaction*="pane.back"]') ||
          document.querySelector('div[role="button"][aria-label*="Back" i]');
        if (galleryBack){ try { galleryBack.click(); } catch {} await sleep(800); }
        document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',keyCode:27,which:27,bubbles:true}));
        document.dispatchEvent(new KeyboardEvent('keyup'  ,{key:'Escape',keyCode:27,which:27,bubbles:true}));
        await sleep(400);
        if (!isPlacePanel()){
          history.back();
          await sleep(1000);
        }
        continue;
      }
      break;
    }
    if (looksLikeResults()) return true;

    scrollSidePaneToTop(); await sleep(150);

    const cands = findBackArrowCandidates();
    for (const el of cands){
      await clickDeep(el);
      await sleep(750);
      if (await ensureBackToList(3000) || looksLikeResults()) return true;
    }

    document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',keyCode:27,which:27,bubbles:true}));
    document.dispatchEvent(new KeyboardEvent('keyup'  ,{key:'Escape',keyCode:27,which:27,bubbles:true}));
    await sleep(450);
    if (looksLikeResults()) return true;

    history.back(); await sleep(1100);
    if (await ensureBackToList(4000) || looksLikeResults()) return true;

    history.back(); await sleep(1100);
    if (looksLikeResults()) return true;

    window.dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowLeft',code:'ArrowLeft',altKey:true,bubbles:true}));
    window.dispatchEvent(new KeyboardEvent('keyup'  ,{key:'ArrowLeft',code:'ArrowLeft',altKey:true,bubbles:true}));
    await sleep(900);
    if (looksLikeResults()) return true;

    return false;
  }

  async function backToListSmart(){
    for (let i=0; i<3; i++){
      const ok = await clickBackToListOnce();
      if (ok) return true;
      await sleep(600);
    }
    return looksLikeResults();
  }

  async function hardResetToList(){
    if (!SEARCH_URL) return false;
    try {
      console.log("[SEQ] hard reset →", SEARCH_URL);
      location.href = SEARCH_URL;
    } catch (err) {
      console.warn("[SEQ] hard reset navigation failed", err);
      return false;
    }
    const ok = await waitResults(45000);
    if (ok) {
      scrollSidePaneToTop();
      await sleep(400);
    }
    return ok;
  }

  /* ---------------- Prefetch & rescue in FEED ---------------- */
  function countUnprocessed(doneSet){
    const list=orderedCandidatesFromFeed();
    let c=0;
    for(const it of list){ if(!doneSet.has(it.key)) c++; }
    return c;
  }
  async function scrollUntilMoreOrEnd(doneSet, maxSteps=240){
    const f=feed(); if(!f) return "no-feed";
    for(let step=1; step<=maxSteps; step++){
      if(atEndOfList()){ log("end-of-list marker found"); return "end"; }
      const beforeTop=f.scrollTop, beforeH=f.scrollHeight;
      f.scrollBy(0, Math.max(640, Math.floor(f.clientHeight*0.92)));
      await sleep(560);
      if(countUnprocessed(doneSet)>0){ log(`prefetch: got more cards after step ${step}`); return "more"; }
      if(Math.abs(f.scrollTop-beforeTop)<6 && f.scrollHeight===beforeH){
        const lastCard=f.querySelector(".Nv2PK:last-child, [role='article']:last-child");
        lastCard?.scrollIntoView({block:"end"}); await sleep(460);
        if(countUnprocessed(doneSet)>0) return "more";
      }
    }
    log("prefetch: max steps reached without new cards; treating as end");
    return "end";
  }

  async function rescueSweepForUnprocessed(doneSet){
    const f = feed(); if(!f) return false;
    let found = false;
    log("[RESCUE] sweep up start");
    let guard = 0;
    while (f.scrollTop > 0 && guard < 340){
      f.scrollBy(0, -Math.max(620, Math.floor(f.clientHeight*0.92)));
      await sleep(280);
      guard++;
      if (countUnprocessed(doneSet) > 0){ found = true; log(`[RESCUE] found more after ${guard} up-steps`); break; }
    }
    guard = 0;
    while (!atEndOfList() && guard < 340){
      f.scrollBy(0, Math.max(620, Math.floor(f.clientHeight*0.92)));
      await sleep(260);
      guard++;
      if (countUnprocessed(doneSet) > 0){ break; }
    }
    return found;
  }

  function websiteOnly(href){
    if(!href) return ""; try{ const u=new URL(href, location.href); if(u.hostname.includes("google.")||u.pathname.startsWith("/maps")) return ""; return u.href; }catch{ return ""; }
  }

  /* =================== scrapeOne (Hours = from <li.G8aQO>) =================== */
  async function scrapeOne(){
    const q=s=>document.querySelector(s), qa=s=>[...document.querySelectorAll(s)];
    const qx=(arr)=>{ for(const s of arr){ const el=q(s); if(el) return el; } return null; };

    if (looksLikeResults()){
      postLog('[GUARD] detected results view during scrape; skipping current place');
      return null;
    }

    function looksLikeProse(s){
      if(!s) return false;
      const t = s.replace(/\s+/g,' ').trim();
      if(t.length < 40) return false;
      if(/[✓•●■▪\-–]\s/.test(s)) {
        const lines = s.split(/\r?\n/).filter(x=>x.trim());
        if (lines.length >= 2 && lines.every(x => /[✓•●■▪\-–]\s/.test(x))) return false;
      }
      return true;
    }
    function extractDescription(){
      const about = q('section[role="region"][aria-label="About"]') || q('div[aria-label="About"]');
      if (about){
        const head = [...about.querySelectorAll('div[role="heading"], h2, h3')]
          .find(h => /from the business|business description|about/i.test((h.textContent||"").trim()));
        if (head){
          const sib = head.nextElementSibling;
          const block = sib?.querySelector('[lang]') || sib;
          const txt = (block?.innerText || "").trim();
          if (looksLikeProse(txt)) return txt;
        }
        const cands = [...about.querySelectorAll('[lang]')].filter(el=>{
          const t=(el.innerText||"").trim();
          if (t.length<40) return false;
          if (el.closest('[role="listitem"], ul, ol, table')) return false;
          return true;
        }).sort((a,b)=> b.innerText.length - a.innerText.length);
        if (cands[0] && looksLikeProse(cands[0].innerText)) return cands[0].innerText.trim();
      }
      const overCand = q('div[role="main"] section [lang]') || q('div[role="main"] [lang]');
      const overTxt = (overCand?.innerText || "").trim();
      if (looksLikeProse(overTxt)) return overTxt;
      return "";
    }

    function daysFromRelativeTime(s){
      if(!s) return Infinity;
      s = String(s).toLowerCase().trim().replace(/\s+/g,' ');
      if (s.includes('today')) return 0;
      if (s.includes('yesterday')) return 1;
      const m = s.match(/(\d+|a|an)\s*(minute|minutes|hour|hours|day|days|week|weeks|month|months|year|years)\s*ago/);
      if(!m) return Infinity;
      const n = (m[1]==='a' || m[1]==='an') ? 1 : parseInt(m[1],10);
      const unit = m[2];
      switch(unit){
        case 'minute': case 'minutes': return n/1440;
        case 'hour':   case 'hours':   return n/24;
        case 'day':    case 'days':    return n;
        case 'week':   case 'weeks':   return n*7;
        case 'month':  case 'months':  return n*30;
        case 'year':   case 'years':   return n*365;
        default: return Infinity;
      }
    }

    function countOwnerResponses() {
      const exactBoxes = [...document.querySelectorAll('div.CDe7pd')].filter(box => {
        const label = (box.querySelector('span.fontTitleSmall')?.innerText || "")
          .trim()
          .toLowerCase();
        return label === 'response from the owner' || /response from (the )?owner/i.test(label);
      }).length;
      if (exactBoxes > 0) return exactBoxes;

      return [...document.querySelectorAll('span.fontTitleSmall, div, span')]
        .filter(n => /response from (the )?owner/i.test((n.innerText || "").trim()))
        .length;
    }

    let hasPhotosSection = false;
    {
      const ownerBtn =
        document.querySelector('button.K4UgGe[aria-label="By owner"]') ||
        document.querySelector('button.K4UgGe[aria-label*="By owner" i]') ||
        document.querySelector('button.K4UgGe[data-carousel-index="3"]');
      const allBtn =
        document.querySelector('button.K4UgGe[aria-label="All"]') ||
        document.querySelector('button.K4UgGe[data-carousel-index="0"]');
      const heading = [...document.querySelectorAll('div[role="heading"], h2, h3, span')]
        .find(el => /photos?\s*&\s*videos?/i.test((el.textContent || '').trim()));
      const tilesHint = document.querySelector('a.OKAoZd');
      hasPhotosSection = !!(ownerBtn || allBtn || heading || tilesHint);
    }

    const PHOTO_THRESHOLD_LIMIT = 10;
    let galleryWasOpened = false;

    /* ===================== OPEN "BY OWNER" PHOTOS & COUNT BY SCROLLING THE GRID ===================== */
    async function openAllAndCountPhotosRobust(){
      const ENTER_DELAY = 900;
      const SCROLL_DELAY_FAST = 420;   // fast path when tiles keep loading
      const SCROLL_DELAY_SLOW = 820;   // cap the wait if grid is still streaming
      const SETTLE_DELAY_SHORT = 520;  // quick settle for tiny galleries
      const SETTLE_DELAY_LONG = 820;   // longer settle when we are at the bottom
      const MAX_STEPS    = 650;        // safety cap
      const STABLE_LIMIT_BASE = 8;     // fallback upper bound for stability

      // Click the "By owner" card to focus on owner-uploaded photos; fallback to "All"
      const ownerSelectors = [
        '.ofKBgf button.K4UgGe[aria-label="By owner"]',
        '.ofKBgf button.K4UgGe[aria-label*="By owner" i]',
        'button.K4UgGe[aria-label="By owner"]',
        'button.K4UgGe[aria-label*="By owner" i]',
        'button.K4UgGe[data-carousel-index="3"]'
      ];
      let galleryBtn = null;
      for (const selector of ownerSelectors){
        const el = document.querySelector(selector);
        if (el){ galleryBtn = el; break; }
      }

      if (!galleryBtn){
        galleryBtn =
          document.querySelector('.ofKBgf button.K4UgGe[aria-label="All"]') ||
          document.querySelector('button.K4UgGe[aria-label="All"]') ||
          document.querySelector('button.K4UgGe[data-carousel-index="0"]');
      }
      if (!galleryBtn){
        // Fallback: any photo tile link (will also open gallery grid)
        galleryBtn = document.querySelector('a.OKAoZd') || document.querySelector('a[href*="photos"]');
      }
      if (!galleryBtn) return null;

      galleryWasOpened = true;
      await clickDeep(galleryBtn);
      await sleep(ENTER_DELAY);

      // Wait for the gallery grid or viewer to appear
      const t0 = Date.now();
      while (Date.now()-t0 < 25000){
        const grid = document.querySelector('div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde');
        const anyTile = document.querySelector('a.OKAoZd[data-photo-index]');
        const viewer = document.querySelector('div[role="dialog"] img');
        if (grid && anyTile) break;
        if (viewer) break;
        await sleep(220);
      }

      /* ---------- Preferred path: count via grid scrolling ---------- */
      const gridContainer =
        document.querySelector('div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde') ||
        document.querySelector('div[role="dialog"] div.m6QErb.DxyBCb.kA9KIf.dS8AEf.XiKgde');
      if (gridContainer){
        const seenIdx = new Set();
        const seenKeys = new Set();
        let thresholdReached = false;
        let currentScrollDelay = SCROLL_DELAY_FAST;

        const gridScrollable = () => (gridContainer.scrollHeight - gridContainer.clientHeight) > 12;

        function captureVisible(){
          const tiles = [...gridContainer.querySelectorAll('a.OKAoZd[data-photo-index]')];
          let added = 0;
          for (const t of tiles){
            const idx = t.getAttribute('data-photo-index');
            if (idx != null) {
              if (!seenIdx.has(idx)) { seenIdx.add(idx); added++; }
            } else {
              // fallback: build a key from aria-label + inline style bg image
              const aria = (t.getAttribute('aria-label') || '').trim();
              const bg   = (t.querySelector('.U39Pmb')?.getAttribute('style') || '').replace(/\s+/g,' ');
              const key  = aria + '|' + bg;
              if (!seenKeys.has(key)) { seenKeys.add(key); added++; }
            }
          }
          return added;
        }

        // initial capture
        captureVisible();
        if ((seenIdx.size + seenKeys.size) >= PHOTO_THRESHOLD_LIMIT) {
          thresholdReached = true;
        } else if (!gridScrollable()) {
          await sleep(SETTLE_DELAY_SHORT);
        } else {
          await sleep(currentScrollDelay);
        }

        let stableRounds = 0;
        let steps = 0;

        // Slow, careful scroll to the bottom (and beyond, to trigger final lazy loads)
        while (steps < MAX_STEPS){
          steps++;

          const beforeCount = seenIdx.size + seenKeys.size;

          // Scroll by ~90% of the viewport of the grid
          const by = Math.max( Math.floor(gridContainer.clientHeight * 0.9), 350 );
          gridContainer.scrollBy(0, by);
          await sleep(currentScrollDelay);

          // Capture new tiles
          const added = captureVisible();

          // If we're near bottom, try a few extra nudges and a hard jump
          const nearBottom = (gridContainer.scrollTop + gridContainer.clientHeight) >= (gridContainer.scrollHeight - 8);
          if (nearBottom){
            await sleep(SETTLE_DELAY_SHORT);
            const settleAdded = captureVisible();
            if (settleAdded > 0) stableRounds = 0;
            gridContainer.scrollTop = gridContainer.scrollHeight;
            await sleep(SETTLE_DELAY_LONG);
            const hardJumpAdded = captureVisible();
            if (hardJumpAdded > 0) stableRounds = 0;
          }

          const afterCount = seenIdx.size + seenKeys.size;
          if (afterCount > beforeCount){
            stableRounds = 0;
            currentScrollDelay = SCROLL_DELAY_FAST;
          } else {
            stableRounds++;
            if (currentScrollDelay < SCROLL_DELAY_SLOW){
              currentScrollDelay = Math.min(SCROLL_DELAY_SLOW, currentScrollDelay + 120);
            }
          }

          if (afterCount >= PHOTO_THRESHOLD_LIMIT){
            thresholdReached = true;
            break;
          }

          const dynamicStableLimit = (!gridScrollable())
            ? 1
            : afterCount <= 4
              ? 2
              : afterCount <= 8
                ? 3
                : STABLE_LIMIT_BASE;

          // Safety close: if we haven't grown for a while and we're basically at the end, stop
          if (stableRounds >= dynamicStableLimit && (nearBottom || !gridScrollable())) break;
        }

        const total = seenIdx.size + seenKeys.size;
        if (total >= PHOTO_THRESHOLD_LIMIT) thresholdReached = true;
        const reportedCount = thresholdReached ? PHOTO_THRESHOLD_LIMIT : total;
        const label = thresholdReached ? `More than ${PHOTO_THRESHOLD_LIMIT}` : `Less than ${PHOTO_THRESHOLD_LIMIT}`;

        await sleep(SETTLE_DELAY_SHORT);

        // Exit gallery grid (back to place)
        const backBtn =
          document.querySelector('button[aria-label="Back"]') ||
          document.querySelector('button[aria-label*="Back" i]') ||
          document.querySelector('button[jsaction*="pane.media.back"], button[jsaction*="pane.back"]');
        if (backBtn){ try{ backBtn.click(); }catch{} await sleep(700); }
        document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',keyCode:27,which:27,bubbles:true}));
        document.dispatchEvent(new KeyboardEvent('keyup'  ,{key:'Escape',keyCode:27,which:27,bubbles:true}));
        await sleep(400);
        await sleep(SETTLE_DELAY_SHORT);

        return {
          count: reportedCount,
          thresholdHit: thresholdReached,
          label
        };
      }

      /* ---------- Fallback: enter viewer and press "Next" repeatedly ---------- */
      // Open the FIRST tile to enter the photo viewer
      const firstTile =
        document.querySelector('div[role="dialog"] .U39Pmb[role="img"]') ||
        document.querySelector('.U39Pmb[role="img"]');
      if (firstTile){ galleryWasOpened = true; await clickDeep(firstTile); await sleep(650); }

      const getDialog = () => document.querySelector('div[role="dialog"]') || document.body;

      function currentImageKey(){
        const scope = getDialog();
        const imgs = [...scope.querySelectorAll('img')]
          .filter(img => img.offsetParent !== null && img.width > 50 && img.height > 50);
        if (!imgs.length) return "";
        imgs.sort((a,b)=> (b.width*b.height) - (a.width*a.height));
        const src = imgs[0].currentSrc || imgs[0].src || "";
        return src.split('=')[0].split('?')[0];
      }
      function nextButton(){
        const scope = getDialog();
        const sels = [
          'button[aria-label="Next"]',
          'button[aria-label*="Next" i]',
          'div[role="button"][aria-label*="Next" i]',
          'button[jsaction*="next"]',
          'button[aria-label="Next photo"]',
          'button[aria-label*="next photo" i]',
          'button[aria-label*="forward" i]'
        ];
        for (const s of sels){
          const el = scope.querySelector(s);
          if (el && el.offsetParent !== null && !el.disabled) return el;
        }
        return null;
      }

      let key = currentImageKey();
      if (!key && firstTile){ await clickDeep(firstTile); await sleep(650); key = currentImageKey(); }

      const visited = new Set();
      const MAX_VISITS = 5000;
      const MAX_STALLS = 10;
      let thresholdReached = false;

      let visits = 0;
      let stalls = 0;

      while (visits < MAX_VISITS){
        const curKey = currentImageKey();
        if (curKey) visited.add(curKey);

        if (visited.size >= PHOTO_THRESHOLD_LIMIT){
          thresholdReached = true;
          break;
        }

        const nb = nextButton();
        if (nb){
          await clickDeep(nb);
        }else{
          document.dispatchEvent(new KeyboardEvent('keydown',{ key:'ArrowRight', code:'ArrowRight', which:39, keyCode:39, bubbles:true }));
          document.dispatchEvent(new KeyboardEvent('keyup'  ,{ key:'ArrowRight', code:'ArrowRight', which:39, keyCode:39, bubbles:true }));
        }
        await sleep(820);

        const after = currentImageKey();
        visits++;

        if (after === curKey || !after){
          stalls++;
          if (stalls >= MAX_STALLS) break;
        }else{
          stalls = 0;
          if (visited.size > 1 && after === [...visited][0]) break;
        }
      }

      const dialog = getDialog();
      const closeBtn =
        dialog.querySelector('button[aria-label="Close"]') ||
        dialog.querySelector('button[aria-label*="Close" i]') ||
        dialog.querySelector('button[jsaction*="viewer.close"]');
      if (closeBtn){ try{ closeBtn.click(); }catch{} await sleep(520); }
        document.dispatchEvent(new KeyboardEvent('keydown',{key:'Escape',keyCode:27,which:27,bubbles:true}));
        document.dispatchEvent(new KeyboardEvent('keyup'  ,{key:'Escape',keyCode:27,which:27,bubbles:true}));
        await sleep(400);
        await sleep(SETTLE_DELAY_SHORT);

      const total = visited.size;
      if (total >= PHOTO_THRESHOLD_LIMIT) thresholdReached = true;
      const reportedCount = thresholdReached ? PHOTO_THRESHOLD_LIMIT : total;
      const label = thresholdReached ? `More than ${PHOTO_THRESHOLD_LIMIT}` : `Less than ${PHOTO_THRESHOLD_LIMIT}`;

      return {
        count: reportedCount,
        thresholdHit: thresholdReached,
        label
      };
    }

    function estimatePhotosCount(){
      try{
        const labels = [];
        document.querySelectorAll('[aria-label],[role="button"]').forEach(el=>{
          const s=(el.getAttribute('aria-label')||'').trim(); if (s) labels.push(s);
        });
        for (const s of labels){
          const m = s.match(/(\d{1,3}(?:[,.\s]\d{3})*|\d+)\s*photos?/i);
          if (m){
            const p = parseInt((m[1]||'').replace(/[^\d]/g,''),10);
            if (p>0) return p;
          }
        }
      }catch{}
      return null;
    }

    const name = (document.querySelector('h1 span.DUwDvf')?.innerText || document.querySelector('div.zvLtDc')?.innerText || document.querySelector('h1')?.innerText || "").trim();
    if(/^\s*results\s*$/i.test(name||"")) return null;

    const q2 = (sel)=>document.querySelector(sel), qa2=(sel)=>[...document.querySelectorAll(sel)];
    const text2=(el)=> (el?.innerText||el?.textContent||"").trim();

    const primary = (q2('button.DkEaL[jsaction*="category"]')||q2('button[jsaction*="pane.rating.category"]')||q2('div[role="main"] button[aria-label*="Category"]'))?.innerText?.trim() || "";
    let phone="", address=""; const phoneRe=/^[+()0-9\s\-–.]{7,}$/;
    qa("div.rogA2c div.Io6YTe.fontBodyMedium.kR99db.fdkmkc, div.rogA2c .Io6YTe").forEach(el=>{
      const v=text2(el); if(!v) return;
      if(!phone && phoneRe.test(v)) phone=v; else if(!address && !phoneRe.test(v)) address=v;
    });
    let website=""; const siteBox=q2("div.DKPXOb.OyjIsf");
    if(siteBox) website=websiteOnly(siteBox.querySelector("a[href]")?.href);
    if(!website){
      website = websiteOnly(q2('a[data-item-id="authority"]')?.href) || websiteOnly(q2('a[aria-label^="Website"]')?.href);
      if(!website){ for(const a of qa('div[role="main"] a[href^="http"]')){ const z=websiteOnly(a.href); if(z){ website=z; break; } } }
    }

    let totalReviews=""; { const rev=q2('span[aria-label$="reviews"]')||q2('button[aria-label*="reviews"] span'); if(rev){ const s=(rev.getAttribute("aria-label")||rev.textContent||"").replace(/[(),]/g,""); const m=s.match(/\d+/); if(m) totalReviews=m[0]; } }
    let rating=""; {
      const nums=qa('span[aria-hidden="true"]').map(n=>n.innerText?.trim()||"");
      const m=nums.find(t=>/^[0-9]+(\.[0-9])?$/.test(t)); if(m) rating=m;
      if(!rating){ const star=q2('[aria-label*="stars"]'); if(star){ const lbl=star.getAttribute("aria-label")||star.textContent||""; const m2=lbl.match(/([0-9.]+)\s*stars?/i); if(m2) rating=m2[1]; } }
    }

    const chips = qa('button.DkEaL[jsaction*="category"], button[aria-label*="Category"], span[aria-label*="category"]').map(el=>el.innerText.trim()).filter(Boolean);
    const categories = primary ? [primary, ...chips.filter(c=>c!==primary)] : chips;

    const desc = extractDescription();
    const hasDescription = !!desc && desc.trim().length > 0;
    const weakDescription = desc ? (desc.length < 80) : null;

    // ---------------- Hours ----------------
    let openNow = null;
    let hours   = null;

    {
      const normalize = (s)=> String(s || "").replace(/\u202F/g,' ').replace(/\s*–\s*/g,'–').replace(/\s+-\s+/g,'–').trim();

      const readHours = (el)=>{
        if (!el) return null;
        const aria = el.getAttribute?.('aria-label') || '';
        const li = el.matches?.('li.G8aQO') ? el : el.querySelector?.('li.G8aQO');
        const txt = li?.innerText || li?.textContent || el.innerText || el.textContent || '';
        const val = normalize(aria || txt || '');
        return val || null;
      };

      const hoursCell = (
        q('td.mxowUb[role="text"][aria-label]') ||
        q('td.mxowUb[aria-label]') ||
        q('td.mxowUb')
      );

      hours = readHours(hoursCell) || readHours(q('li.G8aQO')) || null;

      const chip = (
        q('span[aria-label*="Open"]') ||
        q('div[aria-label*="Open"]') ||
        q('span[aria-label*="Closed"]') ||
        q('div[aria-label*="Closed"]')
      )?.getAttribute?.("aria-label") || "";
      if (/open/i.test(chip)) openNow = true;
      if (/closed/i.test(chip)) openNow = false;
    }

    // >>> NEW: accurate gallery "visited" count by scrolling the grid; fallback to viewer Next
    let galleryPhotoCount = null;
    let galleryPhotoCountLabel = null;
    let galleryPhotoCountThresholdHit = false;

    let photoResult;
    try {
      photoResult = await openAllAndCountPhotosRobust();
      if (photoResult && typeof photoResult === 'object') {
        if (typeof photoResult.count === 'number') {
          galleryPhotoCountThresholdHit = !!photoResult.thresholdHit;
          galleryPhotoCount = galleryPhotoCountThresholdHit
            ? Math.min(photoResult.count, PHOTO_THRESHOLD_LIMIT)
            : photoResult.count;
        }
        if (typeof photoResult.thresholdHit === 'boolean') {
          galleryPhotoCountThresholdHit = photoResult.thresholdHit;
        }
        if (typeof photoResult.label === 'string' && photoResult.label.trim()) {
          galleryPhotoCountLabel = photoResult.label.trim();
        }
      } else if (typeof photoResult === 'number') {
        galleryPhotoCount = photoResult;
        galleryPhotoCountThresholdHit = photoResult >= PHOTO_THRESHOLD_LIMIT;
      }
    } catch {}

    if (galleryWasOpened) {
      await waitPlaceDetailsSettled(1200);
    }

    if (galleryPhotoCount == null) {
      const estimate = estimatePhotosCount();
      if (typeof estimate === 'number' && !Number.isNaN(estimate)) {
        galleryPhotoCountThresholdHit = estimate >= PHOTO_THRESHOLD_LIMIT;
        galleryPhotoCount = galleryPhotoCountThresholdHit
          ? PHOTO_THRESHOLD_LIMIT
          : estimate;
      }
    }

    if (!galleryPhotoCountLabel && typeof galleryPhotoCount === 'number') {
      galleryPhotoCountLabel = galleryPhotoCountThresholdHit
        ? `More than ${PHOTO_THRESHOLD_LIMIT}`
        : `Less than ${PHOTO_THRESHOLD_LIMIT}`;
    }

    if (galleryPhotoCountLabel) {
      const countLog = typeof galleryPhotoCount === 'number' ? galleryPhotoCount : 'n/a';
      console.log('[PHOTOS] gallery total label:', galleryPhotoCountLabel, `(count: ${countLog})`);
      runtimeSend({
        __gmb_log: true,
        jobId,
        msg: `[PHOTOS] gallery total label: ${galleryPhotoCountLabel} (count: ${countLog})`
      });
    } else if (typeof galleryPhotoCount === 'number') {
      console.log('[PHOTOS] gallery visited total:', galleryPhotoCount);
      runtimeSend({ __gmb_log: true, jobId, msg: `[PHOTOS] gallery visited total: ${galleryPhotoCount}` });
    } else {
      console.log('[PHOTOS] gallery total: unknown');
      runtimeSend({ __gmb_log: true, jobId, msg: '[PHOTOS] gallery total: unknown' });
    }

    let ownerReplyCount = countOwnerResponses();
    let serviceArea=""; { const n=[...document.querySelectorAll('div[role="main"] *')].find(x=>/service area/i.test(x.innerText||"")); if(n){ const nxt=n.parentElement?.nextElementSibling; serviceArea=(nxt?.innerText||n?.innerText||"").trim(); } }

    // Q/A detection
    let qaCount = null, hasQA = null;
    {
      const qaHeaderExact = document.querySelector('div.sErS0c.kjYwid.fontHeadlineSmall.Cpt1Qd');
      const qaHeaderByText =
        qaHeaderExact ||
        [...document.querySelectorAll('div[role="heading"], h2, h3, div')]
          .find(el => /questions?\s*and\s*answers?/i.test((el.textContent || el.innerText || "").trim()));
      const qaHeaderByAria = qaHeaderByText || 
        [...document.querySelectorAll('[aria-label]')]
          .find(el => /questions?\s*and\s*answers?/i.test(el.getAttribute('aria-label') || ""));
      const qaHeader = qaHeaderByAria;

      if (qaHeader) {
        hasQA = true;
        const qaSection =
          qaHeader.closest('section,[role="region"]') ||
          qaHeader.parentElement;
        if (qaSection) {
          const items = qaSection.querySelectorAll(
            'div[role="article"], article, [role="listitem"], div[jscontroller]'
          );
          if (items.length) qaCount = items.length;
        }
      } else {
        const bodyText = (document.body?.innerText || "").toLowerCase();
        hasQA = /\bquestions?\s*and\s*answers?\b/.test(bodyText);
      }
    }

    // Products / SWIS detection
    let productsCount = null, hasProducts = null;
    {
      const productsRegion =
        document.querySelector('section[role="region"][aria-label*="Products" i]') ||
        document.querySelector('section[role="region"][aria-label*="See what\'s in store"]') ||
        ([...document.querySelectorAll('section[role="region"], div[role="region"]')])
          .find(el => /products|see what'?s in store/i.test(el.getAttribute('aria-label') || el.textContent));
      const productCards = productsRegion
        ? [...productsRegion.querySelectorAll('a, [role="listitem"], div, article')]
            .filter(el => /\S/.test(el.textContent || "") && el.querySelector('img'))
        : [];
      if (productsRegion) {
        hasProducts = true;
        productsCount = productCards.length || null;
      } else {
        hasProducts = false;
      }
    }

    // Active Business (last 30d reviews)
    let recent30Count = 0, recentActivityPct = null, activeBusiness = null;
    const reviewAges = [...document.querySelectorAll('span.rsqaWe')];
    const visibleReviews = reviewAges.length;
    for (const el of reviewAges){
      const t = (el.textContent || el.innerText || '').trim();
      const d = daysFromRelativeTime(t);
      if (isFinite(d) && d <= 30) recent30Count++;
    }
    let totalReviewsInt = parseInt((totalReviews||'').replace(/[^\d]/g,''), 10);
    if (isNaN(totalReviewsInt) || totalReviewsInt <= 0) totalReviewsInt = visibleReviews;
    if (totalReviewsInt > 0){
      recentActivityPct = (recent30Count / totalReviewsInt) * 100;
      activeBusiness = recentActivityPct > 30;
    }

    const whatsapp = /wa\.me|api\.whatsapp\.com/i.test(website) || /whatsapp/i.test(phone);

    // Canonical GMB URL
    let gmbUrl = location.href;
    if (!/\/maps\/place\/|[?&]cid=/.test(gmbUrl)) {
      const any = document.querySelector('a[href*="/maps/place/"], a[href*="maps?cid="]');
      gmbUrl = any ? any.href : "";
    }

    await sleep(300);
    return {
      name, phone, address, website, totalReviews, rating, categories,
      description: desc || null, hasDescription, weakDescription,
      openNow, hours,
      ownerReplyCount,
      serviceArea: serviceArea || null, qaCount, hasQA,
      productsCount, hasProducts, whatsapp,
      gmbUrl,
      recent30Count, recentActivityPct, activeBusiness,
      hasPhotosSection,
      galleryPhotoCount, // << NUMBER: bounded by threshold for faster scraping
      galleryPhotoCountLabel
    };
  }

  function scrapeCardQuickFromNode(node){
    const card = node.tagName==="A" ? (node.closest(".Nv2PK,[role='article']")||node) : node;
    if(!card) return null;
    const T = el => (el?.innerText || el?.textContent || "").trim();
    const name = T(card.querySelector('.qBF1Pd, .NrDZNb, .fontHeadlineSmall, .vvjwJb, [role="heading"], .lI9IFe')) || "";
    let rating = "";
    const starEl = card.querySelector('[aria-label*="stars"]');
    if (starEl){
      const lbl = starEl.getAttribute("aria-label") || starEl.textContent || "";
      const m = lbl.match(/([0-9.]+)\s*stars?/i);
      if (m) rating = m[1];
    }
    let totalReviews = "";
    const revEl = card.querySelector('[aria-label$="reviews"], .UY7F9, .HHrUdb');
    if (revEl){
      const s = (revEl.getAttribute("aria-label") || revEl.textContent || "").replace(/[(),]/g,"");
      const m = s.match(/\d+/); if(m) totalReviews = m[0];
    }
    const catText = T(card.querySelector('.W4Efsd, .rllt__details div:nth-child(2), .rllt__wrapped > div:nth-child(2)')) || "";
    const categories = catText ? [catText] : [];
    const a = card.querySelector('a[href*="/maps/place/"], a[href*="/maps?cid="]') || (node.tagName==="A" ? node : null);
    const gmbUrl = a ? a.href : "";
    return { name, rating, totalReviews, categories, gmbUrl };
  }

  /* =============================== main =============================== */
  async function run(){
    if(isCaptcha()){
      postEvent("SCRAPE_ERROR", { message: "captcha" });
      return { captcha:true, debugLines: dbg };
    }
    const ok=await waitResults(35000);
    if(!ok){
      postEvent("SCRAPE_ERROR", { message: "no_results_panel" });
      return { error:"no_results_panel", debugLines: dbg };
    }

    let knownTotal = 0;
    let lastVisitedReport = { visited: -1, total: -1 };

    postEvent("SCRAPE_INIT", { status: "Running" });

    const done=new Set();
    const openFails=new Map();
    const invalidTries=new Map();
    const seenCid        = new Set();
    const seenNameAddr   = new Set();

    let processedTotal = 0;    // includes skipped profiles
    let streamedCount  = 0;    // excludes skipped profiles
    let suppressedSent = 0;    // how many we hid
    let idleCycles     = 0;

    const emitVisited = (totalCandidate) => {
      if (typeof totalCandidate === "number" && totalCandidate >= processedTotal) {
        knownTotal = Math.max(knownTotal, totalCandidate);
      }
      const totalPayload = knownTotal > 0 ? knownTotal : undefined;
      const totalKey = totalPayload == null ? -1 : totalPayload;
      if (lastVisitedReport.visited === processedTotal && lastVisitedReport.total === totalKey) return;
      lastVisitedReport = { visited: processedTotal, total: totalKey };
      const payload = {
        visited: processedTotal,
        skipped: suppressedSent,
        savedThisKeyword: streamedCount,
        savedTotal: streamedCount
      };
      if (totalPayload != null) payload.total = totalPayload;
      postEvent("SCRAPE_VISITED", payload);
    };

    const emitSkipped = () => {
      postEvent("SCRAPE_SKIPPED", {
        visited: processedTotal,
        skipped: suppressedSent,
        savedThisKeyword: streamedCount,
        savedTotal: streamedCount,
        total: knownTotal || undefined
      });
    };

    const emitSaved = () => {
      postEvent("SCRAPE_SAVED", {
        visited: processedTotal,
        skipped: suppressedSent,
        savedThisKeyword: streamedCount,
        savedTotal: streamedCount,
        total: knownTotal || undefined
      });
    };

    const reportCompletion = (status) => {
      const totalFinal = knownTotal > 0 ? knownTotal : processedTotal;
      if (totalFinal > knownTotal) knownTotal = totalFinal;
      emitVisited(totalFinal);
      postEvent("SCRAPE_DONE", {
        status,
        visited: processedTotal,
        skipped: suppressedSent,
        savedThisKeyword: streamedCount,
        savedTotal: streamedCount,
        total: totalFinal
      });
    };

    emitVisited();

    const throttleOpenMs   = 320 + Math.floor(Math.random()*260);
    const throttleBetween  = 560 + Math.floor(Math.random()*360);
    const throttleBack     = 820 + Math.floor(Math.random()*380);

    console.log("[SEQ] Starting sequential crawl…");

    if(countUnprocessed(done)===0){
      const out=await scrollUntilMoreOrEnd(done);
      if(out==="end"){
        console.log("[END] no cards found at start (end marker present)");
        reportCompletion(processedTotal > 0 ? "Complete" : "No results");
        return { done:true, processed: processedTotal, debugLines: dbg };
      }
    }

    while(true){
      // ---- pick the earliest visible unprocessed card (ordered) ----
      let pick = null;
      const ordered = orderedCandidatesFromFeed();
      for (const it of ordered){
        if (done.has(it.key)) continue;
        if (it.cid && seenCid.has(it.cid)) { done.add(it.key); continue; }
        if (it.approx && seenNameAddr.has(it.approx)) { done.add(it.key); continue; }
        pick = it; break;
      }

      if(!pick){
        idleCycles++;

        if (atEndOfList() && countUnprocessed(done) === 0){
          console.log("[END] explicit end-of-list marker & no unprocessed cards; stopping");
          reportCompletion(processedTotal > 0 ? "Complete" : "No results");
          return { done:true, processed: processedTotal, debugLines: dbg };
        }

        const state = await scrollUntilMoreOrEnd(done);
        if (state === "end") {
          if (countUnprocessed(done) === 0) {
            const found = await rescueSweepForUnprocessed(done);
            if (!found) {
              await gentleFeedNudge();
              if (countUnprocessed(done) === 0 && atEndOfList()){
                console.log("[END] end-of-list marker & none after rescue; stopping");
                reportCompletion(processedTotal > 0 ? "Complete" : "No results");
                return { done:true, processed: processedTotal, debugLines: dbg };
              }
            }
          }
        } else {
          idleCycles = 0;
        }

        if (idleCycles >= 2){ await gentleFeedNudge(); }
        if (idleCycles >= 4){
          const found = await rescueSweepForUnprocessed(done);
          if (!found && atEndOfList() && countUnprocessed(done)===0){
            console.log("[END] repeated idle & end marker; stopping");
            reportCompletion(processedTotal > 0 ? "Complete" : "No results");
            return { done:true, processed: processedTotal, debugLines: dbg };
          }
          idleCycles = 0;
        }
        continue;
      }

      // ---- open ----
      console.log(`[SEQ] Pick -> key=${pick.key} cid=${pick.cid||"-"} approx=${(pick.approx||"").slice(0,60)}`);
      const placeIntent = buildIntentFromPick(pick);
      let opened=false;
      for(let tries=0; tries<3 && !opened; tries++){
        try{ pick.node.scrollIntoView({block:"center",inline:"center"}); }catch{}
        try{
          const r=pick.node.getBoundingClientRect();
          ["pointerdown","mousedown","pointerup","mouseup","click"].forEach(t=>pick.node.dispatchEvent(new MouseEvent(t,{bubbles:true,cancelable:true,clientX:r.left+r.width/2,clientY:r.top+r.height/2})));
        }catch{}
        await sleep(throttleOpenMs);
        opened = await waitPlace(20000);
        console.log("[OPEN] try", tries+1, "=>", opened ? "ok" : "fail");
        if(!opened) await sleep(360);
      }
      if(!opened){
        const n=(openFails.get(pick.key)||0)+1;
        openFails.set(pick.key,n);
        console.warn("[RETRY] open failed", pick.key, "fails:", n);

        if(n>=3){
          const quick = scrapeCardQuickFromNode(pick.node);
          if (quick && (quick.name?.trim() || quick.gmbUrl?.trim())){
            processedTotal++;
            const suppress = processedTotal <= SUPPRESS_FIRST;
            if (suppress){
              suppressedSent++;
              console.log(`[SEQ] [SUPPRESS] #${processedTotal} quick "${quick.name||"(no-name)"}"`);
              emitSkipped();
            }else{
              postRow({ ...quick, placeKey: pick.key, _source: "listCard" });
              streamedCount++;
              console.log(`[SEQ] Streamed #${processedTotal} quick "${quick.name||"(no-name)"}" (streamed=${streamedCount})`);
              emitSaved();
            }
            const cidQuick = (quick.gmbUrl ? cidFromHref(quick.gmbUrl) : "");
            if (cidQuick) seenCid.add(cidQuick);
            const approxQuick = (quick.name ? (norm(quick.name)+"|") : "");
            if (approxQuick) seenNameAddr.add(approxQuick);
            emitVisited();
          }
          done.add(pick.key);
        }
        continue;
      }

      // ---- guard place view & scrape ----
      let guardStarted = false;
      let guardFailed = false;
      let row = null;
      try {
        const guardReady = await ensurePlaceGuard(placeIntent);
        if (!guardReady.ok){
          postLog(`[GUARD] unable to lock place view for ${placeIntent.key || pick.key}`);
          done.add(pick.key);
          continue;
        }
        guardStarted = true;
        await waitPlaceDetailsSettled(2200);
        updateIntentFromDocument(placeIntent);
        row = await scrapeOne();
      } finally {
        if (guardStarted){
          guardFailed = placeGuardFailed();
          stopPlaceGuard();
        }
      }

      if (guardFailed){
        postLog(`[GUARD] exhausted recovery for ${placeIntent.key || pick.key}; skipping`);
        done.add(pick.key);
        continue;
      }

      // ---- scrape ----
      if(row && (row.name?.trim() || row.gmbUrl?.trim())){
        processedTotal++;
        const suppress = processedTotal <= SUPPRESS_FIRST;

        if (suppress){
          suppressedSent++;
          console.log(`[SEQ] [SUPPRESS] #${processedTotal} "${row.name||"(no-name)"}" url=${row.gmbUrl||"-"}`);
          emitSkipped();
        }else{
          postRow({ ...row, placeKey: pick.key });
          streamedCount++;
          console.log(`[SEQ] Streamed #${processedTotal} "${row.name||"(no-name)"}" (streamed=${streamedCount})`);
          emitSaved();
        }

        const cid = (row.gmbUrl ? cidFromHref(row.gmbUrl) : "");
        if (cid) seenCid.add(cid);
        const approx = norm(row?.name||"")+"|"+norm(row?.address||"");
        if (approx.trim() !== "|") seenNameAddr.add(approx);

        emitVisited();
        done.add(pick.key);
      }else{
        const t=(invalidTries.get(pick.key)||0)+1;
        invalidTries.set(pick.key,t);
        console.warn("[SCRAPE] invalid/empty row; try#", t, pick.key);

        if(t>=2){
          const quick = scrapeCardQuickFromNode(pick.node);
          if (quick && (quick.name?.trim() || quick.gmbUrl?.trim())){
            processedTotal++;
            const suppress = processedTotal <= SUPPRESS_FIRST;
            if (suppress){
              suppressedSent++;
              console.log(`[SEQ] [SUPPRESS] #${processedTotal} quick2 "${quick.name||"(no-name)"}"`);
              emitSkipped();
            }else{
              postRow({ ...quick, placeKey: pick.key, _source: "listCard" });
              streamedCount++;
              console.log(`[SEQ] Streamed #${processedTotal} quick2 "${quick.name||"(no-name)"}" (streamed=${streamedCount})`);
              emitSaved();
            }
            const cidQuick = (quick.gmbUrl ? cidFromHref(quick.gmbUrl) : "");
            if (cidQuick) seenCid.add(cidQuick);
            const approxQuick = (quick.name ? (norm(quick.name)+"|") : "");
            if (approxQuick) seenNameAddr.add(approxQuick);
            emitVisited();
          }
          done.add(pick.key);
        }
      }

      // ---- back to list & continue ----
      const okBack = await backToListSmart();
      if(!okBack){
        postLog("back navigation failed; attempting hard reset");
        const resetOk = await hardResetToList();
        if (!resetOk){
          postLog("failed back via back-glyph/history; stopping");
          console.log(`[SEQ] Abort after #${processedTotal}. streamed=${streamedCount} suppressed=${suppressedSent}`);
          postEvent("SCRAPE_ERROR", { message: "back_navigation_failed" });
          reportCompletion("Error");
          return { done:true, processed: processedTotal, debugLines: dbg };
        }
        idleCycles = 0;
        continue;
      }

      await sleep(throttleBack);
      await ensureBackToList(9000);
      await gentleFeedNudge();

      if (atEndOfList() && countUnprocessed(done) === 0) {
        const found = await rescueSweepForUnprocessed(done);
        if (!found) { 
          postLog("[END] end-of-list on return & none after rescue; stopping");
          console.log(`[END] processed=${processedTotal} streamed=${streamedCount} suppressed=${suppressedSent}`);
          reportCompletion(processedTotal > 0 ? "Complete" : "No results");
          return { done:true, processed: processedTotal, debugLines: dbg };
        }
      }

      if(countUnprocessed(done) < 3){
        log("low-watermark reached; prefetch scrolling…");
        const state=await scrollUntilMoreOrEnd(done, 90);
        if(state==="end" && countUnprocessed(done)===0){
          postLog("[END] end-of-list marker after prefetch; stopping");
          console.log(`[END] processed=${processedTotal} streamed=${streamedCount} suppressed=${suppressedSent}`);
          reportCompletion(processedTotal > 0 ? "Complete" : "No results");
          return { done:true, processed: processedTotal, debugLines: dbg };
        }
      }

      await sleep(throttleBetween);
      if(options.maxItems && processedTotal>=options.maxItems) {
        console.log(`[END] hit maxItems=${options.maxItems}; processed=${processedTotal} streamed=${streamedCount} suppressed=${suppressedSent}`);
        reportCompletion("Complete");
        return { done:true, processed: processedTotal, debugLines: dbg };
      }
    }
  }

  return run();
}

/* =============== stream rows back to page =============== */
if (chromeRuntime?.onMessage?.addListener) {
  chromeRuntime.onMessage.addListener((msg, sender) => {
    if (msg && msg.tool === "mapsScraper" && !msg.__gmb_evt && !msg.__gmb_stream && !msg.__gmb_log) {
      const fromUrl = sender?.url || "";
      if (!isAllowedUrl(fromUrl)) {
        TAG("PORT", "message from disallowed url", { fromUrl });
        return;
      }
      const tabId = sender?.tab?.id ?? null;
      handlePortMessage({ port: null, tabId, fromUrl }, msg).catch((err) => {
        TAG("PORT", "handle message error", err?.message || err);
      });
      return;
    }

    if (msg && msg.__gmb_evt && msg.jobId) {
      ensureJob(msg.jobId).then((job) => {
        if (!job) {
          if (msg.targetTabId != null) {
            const fallbackEvent = Object.assign({}, msg.event || {});
            const fallbackQueue = fallbackEvent.queue ?? msg.queue ?? null;
            sendToTabDirect(msg.targetTabId, {
              type: "EXT_SCRAPER_EVENT",
              tool: "mapsScraper",
              queue: fallbackQueue,
              event: fallbackEvent,
            });
          }
          return;
        }

        const event = Object.assign({}, msg.event || {});
        if (!event.keyword && job.queueLabel) event.keyword = job.queueLabel;
        if (!event.queue && job.queueLabel) event.queue = job.queueLabel;

        if (event.type === "SCRAPE_INIT") {
          job.savedThisKeyword = 0;
        } else if (event.type === "SCRAPE_SAVED") {
          if (typeof event.savedThisKeyword === "number") {
            job.savedThisKeyword = event.savedThisKeyword;
          } else {
            job.savedThisKeyword = (job.savedThisKeyword || 0) + 1;
            event.savedThisKeyword = job.savedThisKeyword;
          }
          if (typeof event.savedTotal !== "number") {
            event.savedTotal = job.savedThisKeyword || 0;
          }
        } else if (event.type === "SCRAPE_DONE") {
          if (typeof event.savedThisKeyword !== "number") {
            event.savedThisKeyword = job.savedThisKeyword || 0;
          }
          if (typeof event.savedTotal !== "number") {
            event.savedTotal = job.savedThisKeyword || 0;
          }
        }

        sendToFrontend(job, { type: "EXT_SCRAPER_EVENT", tool: "mapsScraper", queue: job.queueLabel, event });
        syncJobSnapshot(job);

        if (event.type === "SCRAPE_DONE" || event.type === "SCRAPE_ERROR") {
          if (job._revived) {
            cleanupJob(job.id, { closePopupWindow: true }).catch((err) =>
              TAG("JOB", "cleanup error", err?.message || err)
            );
          }
        }
      });
      return;
    }

    if (msg && msg.__gmb_stream && msg.jobId) {
      ensureJob(msg.jobId).then((job) => {
        if (!job) {
          if (msg.targetTabId != null) {
            const queueLabel = msg.queue ?? null;
            sendToTabDirect(msg.targetTabId, {
              type: "EXT_SCRAPER_RESPONSE",
              tool: "mapsScraper",
              queue: queueLabel,
              stream: true,
              results: [msg.row],
            });
          }
          TAG("STREAM", "orphan row", msg.row?.name || "(no-name)");
          return;
        }
        sendToFrontend(job, { type: "EXT_SCRAPER_RESPONSE", tool: "mapsScraper", queue: job.queueLabel, stream: true, results: [msg.row] });
        TAG("STREAM", "row", msg.row?.name || "(no-name)");
        syncJobSnapshot(job);
      });
      return;
    }

    if (msg && msg.__gmb_log && msg.jobId) {
      ensureJob(msg.jobId).then((job) => {
        if (!job) {
          if (msg.targetTabId != null) {
            const queueLabel = msg.queue ?? null;
            sendToTabDirect(msg.targetTabId, {
              type: "EXT_SCRAPER_RESPONSE",
              tool: "mapsScraper",
              queue: queueLabel,
              stream: true,
              debugLines: [msg.msg],
            });
          }
          return;
        }
        sendToFrontend(job, { type: "EXT_SCRAPER_RESPONSE", tool: "mapsScraper", queue: job.queueLabel, stream: true, debugLines: [msg.msg] });
      });
    }
  });
}
async function cancelJobsForTab(tabId, reason = "cancelled"){
  const toCancel = [];
  for(const [jobId, job] of jobs){
    if(tabId != null && job.portTabId !== tabId) continue;
    toCancel.push(jobId);
    if(reason){
      notifyJobError(job, reason);
    }
    sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, done:true, cancelled:true, reason });
  }
  for(const jobId of toCancel){
    if(!jobs.has(jobId)) continue;
    await cleanupJob(jobId, { closePopupWindow: true });
  }
}

async function startScrapeJob(ctx, msg){
  const port = ctx.port;
  const tabId = ctx.tabId ?? null;
  const keywordRaw = (msg.keyword || msg.queue || "");
  const keyword = typeof keywordRaw === "string" ? keywordRaw.trim() : String(keywordRaw).trim();
  const targetUrl = msg.url || msg.searchUrl || msg.targetUrl || null;
  let queueLabel = (msg.queue && typeof msg.queue === "string") ? msg.queue.trim() : keyword;
  if (!queueLabel) queueLabel = keyword || (targetUrl ? String(targetUrl) : "");
  queueLabel = queueLabel ? String(queueLabel).trim() : "";

  const options = Object.assign({ maxItems: 1000 }, msg.options || {});
  const jobId = Date.now()+"_"+Math.random().toString(36).slice(2);

  const job = {
    id: jobId,
    queueLabel,
    keyword,
    options,
    portTabId: tabId,
    portRef: port,
    portSenderUrl: ctx.fromUrl,
    popupWindowId: null,
    popupTabId: null,
    popupTabWatcher: null,
    savedThisKeyword: 0,
    targetUrl: targetUrl || null,
    createdAt: Date.now(),
    _revived: false
  };

  jobs.set(jobId, job);
  activeJobCount++;
  updateKeepAliveAlarm();
  syncJobSnapshot(job);

  let popupOpened = false;
  let popupClosed = false;
  let completionSent = false;

  try{
    TAG("JOB","start", { targetUrl: targetUrl || null, queueLabel, options });

    const popup = await openPopupForKeyword({ keyword: keyword || queueLabel, targetUrl });
    if(popup.error){
      notifyJobError(job, popup.error);
      sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, error: popup.error, done:true });
      completionSent = true;
      return;
    }

    popupOpened = true;
    job.popupWindowId = popup.windowId || null;
    job.popupTabId = popup.tabId || null;
    job.targetUrl = popup.url;
    syncJobSnapshot(job);

    attachPopupTabWatcher(job, popup.url);
    if (job.popupTabId != null){
      try { await applyStealthMask(job.popupTabId); } catch {}
    }

    sendToFrontend(job, {
      type: "EXT_SCRAPER_EVENT",
      tool: "mapsScraper",
      queue: job.queueLabel,
      event: {
        type: "POPUP_OPENED",
        keyword: job.queueLabel,
        windowId: job.popupWindowId,
        tabId: job.popupTabId,
        url: popup.url
      }
    });

    if(!job.popupTabId){
      notifyJobError(job, "popup_tab_missing");
      sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, error:"popup_tab_missing", done:true });
      completionSent = true;
      return;
    }

    const loaded = await waitTabComplete(job.popupTabId, 55000);
    if(!loaded){
      notifyJobError(job, "maps load timeout");
      sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, error:"maps load timeout", done:true });
      completionSent = true;
      return;
    }

    const ensured = await ensureMapsContext(job.popupTabId, popup.url, 3);
    if (!ensured){
      const tabCheck = await safeGetTab(job.popupTabId);
      TAG("POPUP", "maps context failed", { url: tabCheck?.url || null });
      notifyJobError(job, "maps context not ready");
      sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, error:"maps context not ready", done:true });
      completionSent = true;
      return;
    }

    if (job.popupTabId != null){
      try { await applyStealthMask(job.popupTabId); } catch {}
    }

    // keep popup visible for stability; no refocus to avoid Chrome context changes

    const workerArgs = [
      Object.assign({}, options, {
        searchUrl: popup.url,
        keyword: job.keyword,
        queue: job.queueLabel,
        targetTabId: job.portTabId
      }),
      jobId,
      job.queueLabel,
      popup.url,
      job.portTabId
    ];
    const res = await injectWithRetry(job.popupTabId, MAPS_SEQUENTIAL_WORKER, workerArgs, "sequential_worker");

    if(job.popupWindowId || job.popupTabId){
      popupClosed = await closePopup(job.popupWindowId, job.popupTabId);
      if(popupClosed){
        job.popupWindowId = null;
        job.popupTabId = null;
      }
    } else {
      popupClosed = true;
    }

    if(res?.captcha){
      notifyJobError(job, "captcha");
      sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, error:"captcha", done:true });
    } else if(res?.error){
      notifyJobError(job, res.error);
      sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, error: res.error, done:true });
    } else {
      sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, done:true, total: res?.processed || 0, debugLines: res?.debugLines || [] });
    }
    completionSent = true;
  }catch(err){
    TAG("JOB","error", err?.message || err);
    if(!completionSent){
      notifyJobError(job, err?.message || String(err));
      sendToFrontend(job, { type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", queue: job.queueLabel, error: err?.message || String(err), done:true });
      completionSent = true;
    }
  }finally{
    if(popupOpened && !popupClosed && (job.popupWindowId || job.popupTabId)){
      const closedFinally = await closePopup(job.popupWindowId, job.popupTabId);
      if(closedFinally){
        job.popupWindowId = null;
        job.popupTabId = null;
      }
    }
    await cleanupJob(jobId, { closePopupWindow: true });
  }
}

async function handlePortMessage(ctx, msg){
  if(!msg || msg.tool !== "mapsScraper") return;

  const command = msg.command || (msg.url || msg.searchUrl ? "START_SCRAPE" : "");

  if(command === "HEARTBEAT"){
    TAG("PORT","heartbeat", { tabId: ctx.tabId });
    return;
  }

  if(command === "STOP_ALL" || command === "STOP" || command === "CANCEL"){
    await cancelJobsForTab(ctx.tabId, "stopped_by_user");
    return;
  }

  if(command === "START_SCRAPE" || command === ""){
    await startScrapeJob(ctx, msg);
    return;
  }

  TAG("PORT","unknown command", command);
}

/* ========================== port ========================== */
if (chromeRuntime?.onConnect?.addListener) {
chromeRuntime.onConnect.addListener((port)=>{
  if(port.name==="yelpScraperUi"){
    TAG('YELP','ui keep-alive', { stage: 'connected', senderUrl: port.sender?.url || null, tabId: port.sender?.tab?.id ?? null });
    port.onDisconnect.addListener(()=>{
      TAG('YELP','ui keep-alive', { stage: 'disconnected', senderUrl: port.sender?.url || null, tabId: port.sender?.tab?.id ?? null });
    });
    return;
  }
  if(port.name==="yellowPagesScraperUi"){
    TAG('YP','ui keep-alive', { stage: 'connected', senderUrl: port.sender?.url || null, tabId: port.sender?.tab?.id ?? null });
    port.onDisconnect.addListener(()=>{
      TAG('YP','ui keep-alive', { stage: 'disconnected', senderUrl: port.sender?.url || null, tabId: port.sender?.tab?.id ?? null });
    });
    return;
  }
  if(port.name!=="mapsScraper") return;

  const tabId = port.sender?.tab?.id ?? null;
  const fromUrl = port.sender?.url || "";

  if(!isAllowedUrl(fromUrl)){
    port.postMessage({ type:"EXT_SCRAPER_RESPONSE", tool:"mapsScraper", error:"forbidden", done:true });
    try{ port.disconnect(); }catch{};
    return;
  }

  if(tabId != null){
    portsByTab.set(tabId, port);
  }

  for(const job of jobs.values()){
    if(job.portTabId === tabId){
      job.portRef = port;
      job.portDisconnectedAt = null;
      syncJobSnapshot(job);
    }
  }

  try {
    port.postMessage({ type:"EXT_SCRAPER_ACK", tool:"mapsScraper", queue:null, tabId, state:"connected" });
  } catch (err) {
    TAG("PORT","ack send failed", err?.message || err);
  }

  TAG("PORT","connected", { tabId, fromUrl });

  port.onMessage.addListener(async (msg)=>{
    try{
      await handlePortMessage({ port, tabId, fromUrl }, msg);
    }catch(err){
      TAG("PORT","message handler error", err?.message || err);
    }
  });

  port.onDisconnect.addListener(()=>{
    const reason = chrome.runtime.lastError?.message;
    TAG("PORT","disconnected", { tabId, reason });
    if(tabId != null && portsByTab.get(tabId) === port){
      portsByTab.delete(tabId);
    }
    for(const job of jobs.values()){
      if(job.portRef === port) job.portRef = null;
      if(job.portTabId === tabId) {
        job.portDisconnectedAt = Date.now();
        syncJobSnapshot(job);
      }
    }
  });
});
}
