// LinkedIn platform adapter.
// Same message protocol as the FB content script (PUBLISH_POST, FIND_POST_URL,
// SCRAPE_POST_ENGAGEMENT) — different selectors.
//
// Surfaces supported in this version:
//   - Personal feed post (Start a post → modal → submit)
//   - LinkedIn Group post (same composer pattern)
//   - Detect outcome by URL change or post appearing in feed

const LI_SELECTORS = {
  // Composer trigger: "Start a post" / "התחלת פוסט"
  composerOpen: [
    '[aria-label*="Start a post" i]',
    '[aria-label*="התחלת פוסט"]',
    '[aria-label*="צרו פוסט"]',
    '.share-box-feed-entry__trigger',
    'button.share-box-feed-entry__trigger',
  ],
  // The modal that opens
  dialog: '[role="dialog"], .share-box',
  // The editable inside the modal
  editable: [
    '[role="dialog"] [contenteditable="true"][role="textbox"]',
    '[role="dialog"] [contenteditable="true"]',
    '.ql-editor[contenteditable="true"]',
    'div[data-placeholder][contenteditable="true"]',
  ],
  // Submit button: "Post" / "פרסם"
  submit: [
    '[role="dialog"] button[aria-label*="Post" i]:not([aria-label*="Save" i])',
    '[role="dialog"] button[aria-label*="פרסם"]',
    '.share-actions__primary-action',
    'button.artdeco-button--primary[type="button"]',
  ],
  // File input for media upload
  fileInput: [
    'input[type="file"][accept*="image"]',
    'input[type="file"][accept*="video"]',
  ],
  addMediaBtn: [
    '[aria-label*="Add media" i]',
    '[aria-label*="הוספת מדיה"]',
    '[aria-label*="Photo" i]',
  ],
};

// ---------- Utilities (shared with FB script logic) ----------

function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }

function querySelectorAny(selectors, root = document) {
  const arr = Array.isArray(selectors) ? selectors : [selectors];
  for (const sel of arr) {
    try {
      const el = root.querySelector(sel);
      if (el) return el;
    } catch {}
  }
  return null;
}

function waitFor(selectors, { timeout = 15000, root = document } = {}) {
  return new Promise((resolve, reject) => {
    const start = Date.now();
    const found = querySelectorAny(selectors, root);
    if (found) return resolve(found);
    const observer = new MutationObserver(() => {
      const el = querySelectorAny(selectors, root);
      if (el) { observer.disconnect(); resolve(el); }
      else if (Date.now() - start > timeout) {
        observer.disconnect();
        reject(new Error(`Timeout waiting for ${JSON.stringify(selectors)}`));
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  });
}

function reportStep(level, message, meta = {}) {
  console.log(`[reachlab-li][${level}] ${message}`, meta);
  try {
    chrome.runtime.sendMessage({ type: 'CONTENT_LOG', payload: { level, message, meta, platform: 'linkedin' } });
  } catch {}
}

// ---------- Humanlike typing ----------

function* typingPlan(text, cfg) {
  const min = cfg.typingMinMsPerChar ?? 40;
  const max = cfg.typingMaxMsPerChar ?? 140;
  const pauseChance = cfg.typingPauseChance ?? 0.08;
  for (const ch of text) {
    let d = min + Math.random() * (max - min);
    if (/[.,!?\n]/.test(ch)) d *= 2.5;
    if (Math.random() < pauseChance) d += 400 + Math.random() * 1400;
    yield { ch, delayMs: Math.floor(d) };
  }
}

function dispatchKeyChar(el, ch) {
  const keyCode = ch.length === 1 ? ch.charCodeAt(0) : 0;
  const opts = {
    key: ch, code: `Key${ch.toUpperCase()}`,
    keyCode, which: keyCode,
    bubbles: true, cancelable: true, composed: true,
  };
  el.dispatchEvent(new KeyboardEvent('keydown', opts));
  el.dispatchEvent(new InputEvent('beforeinput', {
    inputType: 'insertText', data: ch,
    bubbles: true, cancelable: true, composed: true,
  }));
  const ok = document.execCommand && document.execCommand('insertText', false, ch);
  if (!ok) {
    el.dispatchEvent(new InputEvent('input', {
      inputType: 'insertText', data: ch,
      bubbles: true, cancelable: true, composed: true,
    }));
  }
  el.dispatchEvent(new KeyboardEvent('keyup', opts));
}

async function typeIntoEditable(el, text, cfg) {
  el.focus();
  el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
  el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
  el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
  for (const { ch, delayMs } of typingPlan(text, cfg)) {
    dispatchKeyChar(el, ch);
    await sleep(delayMs);
  }
}

function randBetween(min, max) {
  return Math.floor(min + Math.random() * (max - min));
}

// ---------- Humanlike browsing simulation (LinkedIn-specific) ----------
// Real LinkedIn users scroll the feed, dwell on posts, sometimes hover/click
// before posting. This function simulates that pattern for `seconds` seconds.
//
// Pattern:
//   1. Random initial dwell at top (0.5-2s)
//   2. Scroll down in irregular chunks with reading pauses
//   3. Occasionally hover over a post (looking interested)
//   4. Sometimes scroll back up slightly (re-reading)
//   5. Return to feed top before opening composer
async function humanlikeLinkedInBrowse(minSec, maxSec) {
  const totalMs = (minSec + Math.random() * (maxSec - minSec)) * 1000;
  const start = Date.now();
  reportStep('info', `👤 Humanlike browse for ${Math.round(totalMs / 1000)}s before posting...`);

  // Initial pause "looking at the feed"
  await sleep(500 + Math.random() * 1500);

  while (Date.now() - start < totalMs) {
    // Scroll down by 300-800px in smooth manner
    const scrollDistance = 300 + Math.random() * 500;
    window.scrollBy({ top: scrollDistance, behavior: 'smooth' });
    // Reading pause — 2-6 seconds
    await sleep(2000 + Math.random() * 4000);

    // 25% chance to hover over a post (simulate interest)
    if (Math.random() < 0.25) {
      const visiblePosts = document.querySelectorAll('[role="article"], div.feed-shared-update-v2');
      if (visiblePosts.length) {
        const post = visiblePosts[Math.floor(Math.random() * Math.min(3, visiblePosts.length))];
        post.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
        post.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
        await sleep(1000 + Math.random() * 2000);
        post.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
      }
    }

    // 15% chance to scroll back up slightly ("re-reading")
    if (Math.random() < 0.15) {
      window.scrollBy({ top: -(100 + Math.random() * 300), behavior: 'smooth' });
      await sleep(1500 + Math.random() * 2000);
    }
  }

  // Return to top before opening composer (it's at the top)
  window.scrollTo({ top: 0, behavior: 'smooth' });
  await sleep(1500);
  reportStep('info', '👤 Browse simulation done — proceeding to compose');
}

// ---------- LinkedIn Polls ----------
// Polls have a dedicated UI: click composer → click "..." → "Create a poll"
// Or directly: click "Add a poll" button if visible.
async function publishLinkedInPoll({ pollQuestion, pollOptions, text, throttle }) {
  reportStep('info', `📊 LinkedIn Poll: "${pollQuestion}" (${pollOptions.length} options)`);
  await sleep(2000);

  const warn = detectLinkedInWarning();
  if (warn) return { ok: false, warning: warn };

  // Open composer first
  let opener = querySelectorAny(LI_SELECTORS.composerOpen);
  if (!opener) opener = await waitFor(LI_SELECTORS.composerOpen, { timeout: 8000 }).catch(() => null);
  if (!opener) return { ok: false, error: 'composer not found' };
  opener.click();
  await waitFor(LI_SELECTORS.dialog, { timeout: 10000 });
  await sleep(1500);

  // Click "Add a poll" button — usually in the toolbar at bottom
  const pollBtn = querySelectorAny([
    '[aria-label*="Add a poll" i]',
    '[aria-label*="הוספת סקר"]',
    'button[id*="poll" i]',
  ]);
  if (!pollBtn) {
    // Sometimes hidden behind "..." overflow
    const moreBtn = querySelectorAny(['[aria-label*="more" i]', '[aria-label*="עוד"]']);
    if (moreBtn) {
      moreBtn.click();
      await sleep(800);
    }
    const pollBtn2 = querySelectorAny([
      '[aria-label*="poll" i]', '[aria-label*="סקר"]',
    ]);
    if (!pollBtn2) {
      reportStep('error', '❌ Could not find poll button');
      return { ok: false, error: 'poll button not found' };
    }
    pollBtn2.click();
  } else {
    pollBtn.click();
  }
  await sleep(2000);

  // Now we're in poll editor. Fill question + options
  reportStep('info', '📊 Filling poll question...');
  const questionInput = await waitFor([
    'input[aria-label*="question" i]', 'input[aria-label*="שאלה"]',
    'textarea[aria-label*="question" i]',
  ], { timeout: 5000 }).catch(() => null);
  if (!questionInput) return { ok: false, error: 'poll question field not found' };

  questionInput.focus(); questionInput.click();
  await sleep(400);
  for (const ch of pollQuestion) {
    dispatchKeyChar(questionInput, ch);
    await sleep(50 + Math.random() * 100);
  }
  await sleep(600);

  // Fill options (LI starts with 2 inputs, can add up to 4)
  const optInputs = document.querySelectorAll('input[aria-label*="option" i], input[aria-label*="תשובה"]');
  reportStep('info', `📊 Filling ${pollOptions.length} options (${optInputs.length} input slots)`);
  for (let i = 0; i < Math.min(pollOptions.length, optInputs.length, 4); i++) {
    const inp = optInputs[i];
    inp.focus(); inp.click();
    await sleep(300);
    for (const ch of pollOptions[i]) {
      dispatchKeyChar(inp, ch);
      await sleep(40 + Math.random() * 80);
    }
    await sleep(500);
  }

  // If more options needed, click "Add option" button
  for (let i = optInputs.length; i < pollOptions.length && i < 4; i++) {
    const addBtn = querySelectorAny(['[aria-label*="Add" i][aria-label*="option" i]', '[aria-label*="הוספת תשובה"]']);
    if (!addBtn) break;
    addBtn.click();
    await sleep(800);
    const newInputs = document.querySelectorAll('input[aria-label*="option" i], input[aria-label*="תשובה"]');
    const newInp = newInputs[newInputs.length - 1];
    if (!newInp) break;
    newInp.focus();
    await sleep(300);
    for (const ch of pollOptions[i]) {
      dispatchKeyChar(newInp, ch);
      await sleep(40 + Math.random() * 80);
    }
    await sleep(500);
  }

  // Click "Done" or "Add to post" to confirm poll
  const doneBtn = querySelectorAny([
    '[aria-label="Done"]', '[aria-label="סיום"]', '[aria-label*="Add to post"]', '[aria-label*="Save"]',
  ]);
  if (doneBtn) {
    doneBtn.click();
    await sleep(2000);
  }

  // Optional: add commentary text above the poll
  if (text) {
    const editable = querySelectorAny(LI_SELECTORS.editable);
    if (editable) {
      editable.focus();
      await sleep(400);
      for (const ch of text) {
        dispatchKeyChar(editable, ch);
        await sleep(50 + Math.random() * 100);
      }
      await sleep(800);
    }
  }

  // Submit
  const submit = querySelectorAny(LI_SELECTORS.submit);
  if (!submit) return { ok: false, error: 'submit button not found after poll' };
  const enableStart = Date.now();
  while (submit.disabled || submit.getAttribute('aria-disabled') === 'true') {
    if (Date.now() - enableStart > 10000) return { ok: false, error: 'submit stayed disabled' };
    await sleep(300);
  }
  reportStep('info', '📊 ✓ Publishing poll');
  submit.click();
  await sleep(4000);
  return { ok: true, signal: 'poll-published', kind: 'poll' };
}

// ---------- LinkedIn Documents (PDF carousels) ----------
// Upload a PDF and LI turns it into a swipeable carousel — highest reach format in 2025-26.
async function publishLinkedInDocument({ text, media, throttle }) {
  reportStep('info', `📄 LinkedIn Document publish (${media.length} files)`);
  // Standard flow, but ensure the file input accepts PDF
  return await publishStandardLinkedInPost({ text, media, throttle });
}

// ---------- Pre-post engagement (like a few feed posts) ----------
// Real users browse + interact before posting. This signals "active engaged user"
// to LI's algorithm, which boosts your own post's distribution.
async function likeRandomFeedPosts(count = 3) {
  reportStep('info', `👍 Pre-engagement: liking ${count} feed posts`);
  const candidates = Array.from(document.querySelectorAll('[role="article"], div.feed-shared-update-v2'))
    .filter((el) => {
      // Skip posts I've already liked (button aria-label includes "Unlike" or "ביטול לייק")
      const likeBtn = el.querySelector('button[aria-label*="ike" i], button[aria-label*="לייק"]');
      if (!likeBtn) return false;
      const lbl = (likeBtn.getAttribute('aria-label') || '').toLowerCase();
      return !/unlike|ביטול/.test(lbl);
    })
    .slice(0, 15);

  if (!candidates.length) {
    reportStep('warn', 'No likeable posts in feed');
    return;
  }

  // Pick random posts to like (looks more human than always top 3)
  const shuffled = [...candidates].sort(() => Math.random() - 0.5).slice(0, count);

  for (let i = 0; i < shuffled.length; i++) {
    const post = shuffled[i];
    // Scroll post into view (humanlike)
    post.scrollIntoView({ behavior: 'smooth', block: 'center' });
    await sleep(1500 + Math.random() * 2000);

    const likeBtn = post.querySelector('button[aria-label*="ike" i], button[aria-label*="לייק"]');
    if (!likeBtn) continue;
    // Hover, then click
    likeBtn.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
    await sleep(300 + Math.random() * 500);
    likeBtn.click();
    reportStep('info', `  ✓ Liked post ${i + 1}/${count}`);
    await sleep(2000 + Math.random() * 3000); // pause between likes
  }

  // Scroll back to top for the composer
  window.scrollTo({ top: 0, behavior: 'smooth' });
  await sleep(1500);
}

// ---------- LinkedIn warning detection ----------

function detectLinkedInWarning() {
  const patterns = [
    // LinkedIn-specific phrases
    { kind: 'slow-down', regex: /try again later|too many requests|please slow down|you'?re sending requests too quickly/i },
    { kind: 'slow-down', regex: /נסה שוב מאוחר יותר|יותר מדי בקשות|אטה את הקצב/i },
    { kind: 'action-blocked', regex: /this action is restricted|action restricted|can'?t complete this action|something went wrong/i },
    { kind: 'action-blocked', regex: /פעולה מוגבלת|לא ניתן להשלים|משהו השתבש/i },
    { kind: 'account-limited', regex: /your account has been restricted|account limited|temporarily limited/i },
    { kind: 'account-limited', regex: /החשבון שלך הוגבל|הוגבל זמנית/i },
    { kind: 'captcha', regex: /security check|verify you'?re human|prove you'?re not a bot|complete this challenge/i },
    { kind: 'captcha', regex: /בדיקת אבטחה|אמת שאתה לא בוט|השלם את האתגר/i },
    { kind: 'rate-limit', regex: /you'?ve reached the limit|daily limit reached|you can'?t post more/i },
    { kind: 'rate-limit', regex: /הגעת למגבלה|מגבלה יומית|לא ניתן לפרסם יותר/i },
    // The dreaded "Are you a real person?" challenge
    { kind: 'identity-challenge', regex: /confirm your identity|please verify|are you a real person/i },
    { kind: 'identity-challenge', regex: /אשר את זהותך|אנא אמת|האם אתה אדם אמיתי/i },
  ];

  // Scan dialogs, alerts, and full-screen overlays
  const scope = [
    ...document.querySelectorAll('[role="dialog"], [role="alert"], [role="alertdialog"]'),
    // Sometimes LI shows a top banner instead of a dialog
    ...document.querySelectorAll('[class*="banner" i], [class*="notification" i]'),
  ];

  for (const el of scope) {
    const text = (el.innerText || '').slice(0, 1500);
    if (!text) continue;
    for (const p of patterns) {
      if (p.regex.test(text)) {
        return { kind: p.kind, message: text.slice(0, 200), pageUrl: location.href };
      }
    }
  }
  return null;
}

// ---------- Publish flow ----------

async function publishPost({ text, media = [], throttle = {}, postType = 'text', pollQuestion = null, pollOptions = null }) {
  // Route to specialized flows
  if (postType === 'poll' && pollQuestion && pollOptions?.length >= 2) {
    return await publishLinkedInPoll({ pollQuestion, pollOptions, text, throttle });
  }
  if (media.length && media.some((m) => (m.mimeType || '').includes('pdf'))) {
    return await publishLinkedInDocument({ text, media, throttle });
  }
  return await publishStandardLinkedInPost({ text, media, throttle });
}

async function publishStandardLinkedInPost({ text, media = [], throttle = {} }) {
  reportStep('info', `📍 LinkedIn publish on ${location.pathname}`);
  await sleep(2000);

  const warn = detectLinkedInWarning();
  if (warn) {
    reportStep('error', `⚠ LinkedIn warning before start: ${warn.kind}`);
    return { ok: false, warning: warn };
  }

  // 0) HUMANIZE — Real LI users don't open the app and immediately post.
  const preBrowseMin = throttle.preBrowseMinSec ?? 30;
  const preBrowseMax = throttle.preBrowseMaxSec ?? 90;
  if (preBrowseMin > 0) {
    await humanlikeLinkedInBrowse(preBrowseMin, preBrowseMax);
  }

  // 0.5) PRE-ENGAGEMENT — like 2-4 posts in the feed. LinkedIn's algorithm
  // rewards profiles that engage before posting (signals "active user").
  const preEngageEnabled = throttle.preEngageEnabled !== false;
  if (preEngageEnabled) {
    try { await likeRandomFeedPosts(2 + Math.floor(Math.random() * 3)); }
    catch (e) { reportStep('warn', `pre-engage failed: ${e.message}`); }
  }

  // 1) Open composer
  reportStep('info', 'Looking for composer button...');
  let opener = querySelectorAny(LI_SELECTORS.composerOpen);
  if (!opener) {
    try { opener = await waitFor(LI_SELECTORS.composerOpen, { timeout: 8000 }); }
    catch {}
  }
  if (!opener) {
    reportStep('error', '❌ LinkedIn composer trigger not found');
    return { ok: false, error: 'Composer trigger not found' };
  }
  reportStep('info', `✓ Found composer (label="${(opener.getAttribute('aria-label') || opener.innerText || '').slice(0, 40)}")`);
  await sleep(randBetween(throttle.preClickMinMs ?? 800, throttle.preClickMaxMs ?? 2500));
  opener.click();

  // 2) Wait for dialog
  reportStep('info', 'Waiting for dialog...');
  try { await waitFor(LI_SELECTORS.dialog, { timeout: 10000 }); }
  catch {
    reportStep('error', '❌ Dialog did not open');
    return { ok: false, error: 'Dialog did not open' };
  }
  reportStep('info', '✓ Dialog opened');

  // 3) Find editable
  reportStep('info', 'Looking for text input...');
  let editable;
  try { editable = await waitFor(LI_SELECTORS.editable, { timeout: 8000 }); }
  catch {
    reportStep('error', '❌ Text input not found');
    return { ok: false, error: 'Editable area not found' };
  }
  reportStep('info', '✓ Text input found, typing...');

  // 4) Type
  await sleep(randBetween(400, 1200));
  await typeIntoEditable(editable, text, throttle);
  reportStep('info', `✓ Typed ${text.length} characters`);

  // 5) Media upload (if any)
  if (media && media.length) {
    reportStep('info', `Attaching ${media.length} media files...`);
    try { await attachMediaToLI(media); reportStep('info', '✓ Media attached'); }
    catch (e) { reportStep('warn', `⚠ Media attach failed: ${e.message}`); }
  }

  // 6) Wait for Submit to enable + click
  reportStep('info', 'Waiting for Post button to enable...');
  await sleep(randBetween(throttle.preClickMinMs ?? 800, throttle.preClickMaxMs ?? 2500));
  let submit = querySelectorAny(LI_SELECTORS.submit);
  if (!submit) {
    reportStep('error', '❌ Post button not found');
    return { ok: false, error: 'Submit button not found' };
  }
  const enableStart = Date.now();
  while (submit.disabled || submit.getAttribute('aria-disabled') === 'true') {
    if (Date.now() - enableStart > 10000) {
      reportStep('error', '❌ Post button stayed disabled');
      return { ok: false, error: 'Submit disabled' };
    }
    await sleep(300);
    submit = querySelectorAny(LI_SELECTORS.submit) || submit;
  }
  reportStep('info', `✓ Clicking Post button`);
  const submittedDialog = document.querySelector(LI_SELECTORS.dialog);
  submit.click();

  // 7) Detect outcome — feed scan or dialog removal
  const snippet = text.replace(/\s+/g, ' ').trim().slice(0, 40);
  const outcome = await detectOutcome(submittedDialog, snippet, 15000);
  reportStep('info', `📤 Outcome: ${outcome.signal} (${outcome.success ? 'success' : 'failure'})`);
  if (!outcome.success) return { ok: false, error: outcome.signal };
  return { ok: true, signal: outcome.signal };
}

async function detectOutcome(submittedDialog, snippet, timeoutMs) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    // Strongest: text appears in the feed
    if (snippet && findPostInFeed(snippet)) {
      return { success: true, signal: 'post-visible-in-feed' };
    }
    // Dialog gone
    if (submittedDialog && !document.body.contains(submittedDialog)) {
      return { success: true, signal: 'dialog-removed' };
    }
    // Failure markers
    const dialog = document.querySelector(LI_SELECTORS.dialog);
    if (dialog && /failed|error|try again|נכשל|שגיאה/i.test(dialog.innerText || '')) {
      return { success: false, signal: 'failure-text' };
    }
    await sleep(400);
  }
  // Optimistic
  return { success: true, signal: 'optimistic-no-failure' };
}

function findPostInFeed(snippet) {
  const needle = snippet.replace(/\s+/g, ' ').trim();
  const articles = document.querySelectorAll('[role="article"], div.feed-shared-update-v2, .occludable-update');
  for (const el of articles) {
    if (el.closest('[role="dialog"]')) continue;
    if ((el.innerText || '').replace(/\s+/g, ' ').includes(needle)) return true;
  }
  return false;
}

// ---------- Media upload ----------

async function attachMediaToLI(mediaList) {
  const files = mediaList.map((m) => new File(
    [new Uint8Array(m.buffer)], m.name, { type: m.mimeType }
  ));

  let input = querySelectorAny(LI_SELECTORS.fileInput);
  if (!input) {
    const mediaBtn = querySelectorAny(LI_SELECTORS.addMediaBtn);
    if (mediaBtn) {
      mediaBtn.click();
      await sleep(800);
      input = querySelectorAny(LI_SELECTORS.fileInput);
    }
  }
  if (!input) throw new Error('file input not found');

  const dt = new DataTransfer();
  for (const f of files) dt.items.add(f);
  input.files = dt.files;
  input.dispatchEvent(new Event('change', { bubbles: true }));
  // LinkedIn shows an editor — auto-confirm
  await sleep(3000);
  // Click "Done" / "סיום" if appears
  const doneBtn = querySelectorAny([
    'button[aria-label*="Done" i]', 'button[aria-label*="סיום"]',
    'button[aria-label*="Next" i]', 'button[aria-label*="הבא"]',
  ]);
  if (doneBtn && !doneBtn.disabled) doneBtn.click();
  await sleep(1500);
}

// ---------- Engagement scraping ----------

function scrapePostEngagement() {
  const text = document.body.innerText.slice(0, 3000);
  const out = { reactions: 0, comments: 0, shares: 0, views: null };
  const m1 = text.match(/([\d,.]+\s*[KMכ]?)\s*(?:reactions|likes|תגובות לפוסט|לייקים)/i);
  if (m1) out.reactions = parseLICount(m1[1]);
  const m2 = text.match(/([\d,.]+\s*[KMכ]?)\s*(?:comments?|תגוב)/i);
  if (m2) out.comments = parseLICount(m2[1]);
  const m3 = text.match(/([\d,.]+\s*[KMכ]?)\s*(?:reposts?|shares?|שיתופים)/i);
  if (m3) out.shares = parseLICount(m3[1]);
  const m4 = text.match(/([\d,.]+\s*[KMכ]?)\s*(?:impressions|views|צפיות)/i);
  if (m4) out.views = parseLICount(m4[1]);
  return out;
}

function parseLICount(s) {
  if (!s) return 0;
  s = s.trim().replace(/,/g, '');
  if (/K|כ/i.test(s)) return Math.round(parseFloat(s) * 1000) || 0;
  if (/M/i.test(s)) return Math.round(parseFloat(s) * 1000000) || 0;
  return parseInt(s, 10) || 0;
}

// ---------- Comment harvest (leads pipeline parity with FB) ----------
// LI loads comments lazily. Multi-pass scroll to coax them out, then dedupe.
// Returns array of {author, authorUrl, text, timestamp}.
const COMMENT_SELECTORS = [
  'article.comments-comment-entity',
  '[class*="comments-comment-entity"]',
  'div.comments-comment-item',
  '[class*="comments-comment-item"]',
  '[data-id*="urn:li:comment"]',
  'article[data-id*="comment"]',
];

async function loadMoreCommentsIfPossible() {
  // LI sometimes shows a "Load more comments" or "Previous comments" button
  const labels = [
    /load more comments/i, /show more comments/i, /previous comments/i, /view more comments/i,
    /טען עוד תגובות/, /הצג עוד תגובות/, /תגובות קודמות/, /הצג תגובות נוספות/,
  ];
  const btn = Array.from(document.querySelectorAll('button')).find(b => {
    const t = (b.innerText || '').trim();
    if (!t || t.length > 80) return false;
    return labels.some(re => re.test(t));
  });
  if (btn && !btn.disabled) { btn.click(); await sleep(1500); return true; }
  return false;
}

async function harvestComments({ maxComments = 200, maxScrollPasses = 6 } = {}) {
  reportStep('info', '💬 LinkedIn: harvesting comments...');
  // 1) Try clicking the comment count to expand if needed
  const expandBtn = Array.from(document.querySelectorAll('button[aria-label]')).find(b => {
    const l = (b.getAttribute('aria-label') || '').toLowerCase();
    return /^\d+\s+(comments?|תגוב)/i.test(l) || /view.*comments?/i.test(l);
  });
  if (expandBtn) { try { expandBtn.click(); await sleep(1200); } catch {} }

  // 2) Scroll passes to lazy-load comments
  for (let pass = 0; pass < maxScrollPasses; pass++) {
    window.scrollBy({ top: 600 + Math.random() * 400, behavior: 'smooth' });
    await sleep(800 + Math.random() * 600);
    await loadMoreCommentsIfPossible();
  }

  // 3) Collect all comment nodes (any selector that hits)
  const nodes = new Set();
  for (const sel of COMMENT_SELECTORS) {
    try { document.querySelectorAll(sel).forEach(n => nodes.add(n)); } catch {}
  }

  const out = [];
  const seen = new Set();
  for (const c of nodes) {
    if (c.closest('[role="dialog"]')) continue;
    // Author link
    const authorAnchor = c.querySelector('a[href*="/in/"], a[href*="/company/"]');
    const authorUrl = authorAnchor ? (authorAnchor.href || '').split('?')[0] : null;
    // Author name — prefer aria-hidden span next to the link
    let author = '';
    if (authorAnchor) {
      const nameSpan = authorAnchor.querySelector('span[dir="ltr"], span[aria-hidden="true"]')
        || c.querySelector('.comments-comment-meta__description-title, [class*="post-meta__name-text"]');
      author = (nameSpan?.innerText || authorAnchor.innerText || '').trim().split('\n')[0].slice(0, 80);
    }
    if (!author) {
      // Fallback: first non-empty strong text inside the comment
      const strong = c.querySelector('strong, h3, [class*="name"]');
      author = (strong?.innerText || '').trim().slice(0, 80);
    }
    if (!author) continue;

    // Comment text — try several known wrappers, then fall back to full text minus author
    let text = '';
    const bodyEl = c.querySelector('.comments-comment-item__main-content, [class*="comment-item__main-content"], [class*="comment-item-content-body"], .update-components-text, [dir="ltr"][class*="text"]');
    if (bodyEl) text = (bodyEl.innerText || '').trim();
    if (!text) {
      const full = (c.innerText || '').trim();
      text = full.replace(author, '').replace(/^\s*[•·•\-—\s]+/, '').trim();
    }
    // Clean trailing "Like · Reply · 2d" footer noise
    text = text.split(/\n(?:Like|Reply|אהבתי|הגב|תגובה)/i)[0].trim().slice(0, 1500);
    if (!text || text.length < 2) continue;

    const key = (authorUrl || author) + '::' + text.slice(0, 60);
    if (seen.has(key)) continue;
    seen.add(key);
    out.push({ author, authorUrl, text, timestamp: Date.now() });
    if (out.length >= maxComments) break;
  }
  reportStep('info', `💬 Harvested ${out.length} comments`);
  return out;
}

// ---------- Post a comment on the currently loaded LinkedIn post ----------
async function postCommentOnCurrentPage(text, throttle = {}) {
  if (!text || text.length < 1) return { ok: false, error: 'empty text' };
  reportStep('info', `💬 LinkedIn: commenting (${text.length} chars)`);

  // Ensure the comment composer is visible — click "Comment" action if needed
  await sleep(800);
  let composer = querySelectorAny([
    '.comments-comment-box .ql-editor',
    '[contenteditable="true"][role="textbox"][aria-label*="comment" i]',
    '[contenteditable="true"][role="textbox"][aria-label*="תגוב"]',
    '.ql-editor[contenteditable="true"]',
  ]);
  if (!composer) {
    // Find a Comment action button on the visible post and click it
    const post = document.querySelector('[data-urn^="urn:li:activity:"], article');
    const commentActionBtn = post && Array.from(post.querySelectorAll('button[aria-label]')).find(b => {
      const l = (b.getAttribute('aria-label') || '').trim();
      return /^Comment$/i.test(l) || /^הגב/i.test(l) || /^תגובה/i.test(l);
    });
    if (commentActionBtn) { commentActionBtn.click(); await sleep(1500); }
    composer = querySelectorAny([
      '.comments-comment-box .ql-editor',
      '[contenteditable="true"][role="textbox"][aria-label*="comment" i]',
      '[contenteditable="true"][role="textbox"][aria-label*="תגוב"]',
      '.ql-editor[contenteditable="true"]',
    ]);
  }
  if (!composer) return { ok: false, error: 'comment composer not found' };

  composer.focus();
  composer.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
  composer.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
  composer.dispatchEvent(new MouseEvent('click', { bubbles: true }));
  await sleep(400 + Math.random() * 600);
  await typeIntoEditable(composer, text, throttle);
  await sleep(800);

  // Find the post-comment submit button — usually <button class="comments-comment-box__submit-button"> with text "Comment" / "Post"
  // Walk up from the composer to find its container, then look for the submit
  let container = composer.closest('form, .comments-comment-box, [class*="comments-comment-box"]') || composer.parentElement;
  for (let d = 0; d < 6 && container && container.parentElement; d++) {
    if (container.querySelector('button.comments-comment-box__submit-button, [class*="comments-comment-box__submit"]')) break;
    container = container.parentElement;
  }
  let submit = container && container.querySelector('button.comments-comment-box__submit-button, [class*="comments-comment-box__submit"]');
  if (!submit) {
    submit = container && Array.from(container.querySelectorAll('button')).find(b => {
      const t = (b.innerText || '').trim();
      return /^(Comment|Post|הגב|פרסם|תגובה)$/i.test(t);
    });
  }
  if (!submit) return { ok: false, error: 'comment submit not found' };

  const enableStart = Date.now();
  while (submit.disabled || submit.getAttribute('aria-disabled') === 'true') {
    if (Date.now() - enableStart > 6000) return { ok: false, error: 'comment submit disabled' };
    await sleep(300);
  }
  submit.click();
  await sleep(2500);

  const warn = detectLinkedInWarning();
  if (warn) return { ok: false, error: `linkedin-warning:${warn.kind}`, warning: warn };
  reportStep('info', '💬 ✅ Comment submitted');
  return { ok: true };
}

// ---------- React Like on the currently loaded post ----------
// LinkedIn's primary engagement — much safer than commenting, daily-cap-friendly.
async function reactLikeOnCurrentPost() {
  const post = document.querySelector('[data-urn^="urn:li:activity:"], article');
  if (!post) return { ok: false, error: 'no post on page' };
  const likeBtn = post.querySelector('button[aria-label="React Like"], button[aria-label*="Like" i]:not([aria-label*="Unlike" i])')
    || Array.from(post.querySelectorAll('button[aria-label]')).find(b => /^(react )?like$/i.test(b.getAttribute('aria-label') || ''));
  if (!likeBtn) return { ok: false, error: 'like button not found' };
  if (likeBtn.getAttribute('aria-pressed') === 'true') return { ok: true, signal: 'already-liked' };
  likeBtn.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
  await sleep(300 + Math.random() * 500);
  likeBtn.click();
  await sleep(1200);
  return { ok: true, signal: 'liked' };
}

// ---------- Generic humanlike browse (matches FB content protocol) ----------
async function humanBrowsePage(durationMs = 10000) {
  const end = Date.now() + Math.max(3000, durationMs);
  try {
    while (Date.now() < end) {
      const step = 200 + Math.random() * 600;
      window.scrollBy({ top: step, behavior: 'smooth' });
      if (Math.random() < 0.2) {
        await sleep(500 + Math.random() * 1500);
        window.scrollBy({ top: -(80 + Math.random() * 200), behavior: 'smooth' });
      }
      try {
        document.dispatchEvent(new MouseEvent('mousemove', {
          bubbles: true,
          clientX: Math.random() * window.innerWidth,
          clientY: Math.random() * window.innerHeight,
        }));
      } catch {}
      await sleep(800 + Math.random() * 2400);
    }
    window.scrollTo({ top: 0, behavior: 'smooth' });
    await sleep(1500);
  } catch {}
  return { ok: true };
}

// ---------- Scrape user's LinkedIn Groups ----------
// Run on /groups/ — collects {name, url, platform: 'linkedin', memberCount?}
async function scrapeMyGroups() {
  reportStep('info', '👥 LinkedIn: scraping joined groups...');
  // Scroll a few times to lazy-load
  for (let i = 0; i < 5; i++) {
    window.scrollBy({ top: 800, behavior: 'smooth' });
    await sleep(1000);
  }
  // Group cards link to /groups/<id>/
  const anchors = Array.from(document.querySelectorAll('a[href*="/groups/"]'))
    .filter(a => /\/groups\/\d+/.test(a.getAttribute('href') || ''));
  const out = [];
  const seen = new Set();
  for (const a of anchors) {
    const m = (a.getAttribute('href') || '').match(/\/groups\/(\d+)/);
    if (!m) continue;
    const id = m[1];
    if (seen.has(id)) continue;
    seen.add(id);
    // Name: try the anchor text first, fall back to nearest heading
    let name = (a.innerText || '').trim().split('\n')[0].slice(0, 200);
    if (!name || name.length < 2) {
      const card = a.closest('li, article, div');
      name = card?.querySelector('h2, h3, [class*="title"], [class*="name"]')?.innerText?.trim().slice(0, 200) || '';
    }
    if (!name) continue;
    // Member count from nearby text
    const card = a.closest('li, article, div');
    let memberCount = null;
    if (card) {
      const mm = (card.innerText || '').match(/([\d,]+)\s*(?:members?|חברים)/i);
      if (mm) memberCount = parseInt(mm[1].replace(/,/g, ''), 10) || null;
    }
    out.push({
      id: 'li-' + id,
      groupId: id,
      name,
      url: `https://www.linkedin.com/groups/${id}/`,
      platform: 'linkedin',
      memberCount,
    });
    if (out.length >= 200) break;
  }
  reportStep('info', `👥 Found ${out.length} LinkedIn groups`);
  return { ok: true, groups: out };
}

// ---------- LinkedIn group metadata scrape (run on /groups/<id>/) ----------
function scrapeGroupMetadata() {
  const meta = {
    memberCount: null, privacy: 'unknown', avatarUrl: null, coverUrl: null,
    description: null, rulesText: null, adminCount: null, postsPerDay: null,
    language: null, location: null, restrictionLevel: null, topics: [],
    lastScrapedAt: Date.now(),
  };
  const pageText = document.body.innerText.slice(0, 8000);
  const mm = pageText.match(/([\d,]+)\s*(?:members?|חברים)/i);
  if (mm) meta.memberCount = parseInt(mm[1].replace(/,/g, ''), 10) || null;
  if (/listed group|public group|קבוצה ציבורית/i.test(pageText)) meta.privacy = 'public';
  else if (/private group|unlisted group|קבוצה פרטית/i.test(pageText)) meta.privacy = 'private';
  // Hebrew detection
  const hebrewChars = (pageText.match(/[֐-׿]/g) || []).length;
  meta.language = hebrewChars > 100 ? 'he' : 'en';
  // Avatar / cover
  try {
    const heroImgs = document.querySelectorAll('[class*="group-header"] img, header img');
    let best = null, bestArea = 0;
    for (const img of heroImgs) {
      if (!img.src?.startsWith('http')) continue;
      const a = (img.naturalWidth || img.width || 0) * (img.naturalHeight || img.height || 0);
      if (a > bestArea) { bestArea = a; best = img; }
    }
    if (best) meta.avatarUrl = best.src;
  } catch {}
  return meta;
}

// ---------- Reply to a SPECIFIC commenter on the currently loaded post ----------
// Used by the LinkedIn auto-reply pass. Finds the comment whose author matches
// (by name + comment snippet) and submits a reply through ITS Reply button.
// Returns { ok, error?, warning? }.
async function replyToCommentByAuthor({ authorMatch = '', authorUrl = '', commentSnippet = '', replyText = '', throttle = {} } = {}) {
  if (!replyText || replyText.trim().length < 3) return { ok: false, error: 'empty reply' };
  reportStep('info', `💬↩ LinkedIn: replying to ${authorMatch} (${replyText.length} chars)`);

  // Make sure comments are loaded
  await sleep(1000);
  for (let i = 0; i < 4; i++) {
    window.scrollBy({ top: 700, behavior: 'smooth' });
    await sleep(800);
  }

  // Collect candidate comment cards
  const commentNodes = new Set();
  for (const sel of COMMENT_SELECTORS) {
    try { document.querySelectorAll(sel).forEach(n => commentNodes.add(n)); } catch {}
  }
  if (!commentNodes.size) return { ok: false, error: 'no comment cards found' };

  const wantUrl = (authorUrl || '').toLowerCase().split('?')[0];
  const wantSnippet = (commentSnippet || '').replace(/\s+/g, ' ').trim().slice(0, 40).toLowerCase();
  const wantAuthor = (authorMatch || '').trim().toLowerCase();

  let target = null;
  for (const c of commentNodes) {
    if (c.closest('[role="dialog"]')) continue;
    const a = c.querySelector('a[href*="/in/"], a[href*="/company/"]');
    const aUrl = a ? (a.href || '').toLowerCase().split('?')[0] : '';
    const text = (c.innerText || '').replace(/\s+/g, ' ').toLowerCase();
    let score = 0;
    if (wantUrl && aUrl && aUrl === wantUrl) score += 5;
    if (wantAuthor && text.includes(wantAuthor)) score += 2;
    if (wantSnippet && wantSnippet.length >= 10 && text.includes(wantSnippet)) score += 3;
    if (score >= 3) { target = c; break; }
  }
  if (!target) return { ok: false, error: 'matching comment not found on page' };
  reportStep('info', '💬↩ ✓ Matched the comment card');

  // Find the Reply button INSIDE this comment card
  const replyBtn = Array.from(target.querySelectorAll('button')).find(b => {
    const t = (b.innerText || '').trim();
    const a = (b.getAttribute('aria-label') || '').trim();
    return /^Reply$/i.test(t) || /^Reply$/i.test(a) || /^הגב$/i.test(t) || /^תגובה$/i.test(t);
  });
  if (!replyBtn) return { ok: false, error: 'reply button not found in comment' };

  // Click Reply, then wait for the reply composer (a NEW .ql-editor that opens
  // inline INSIDE this comment card).
  target.scrollIntoView({ behavior: 'smooth', block: 'center' });
  await sleep(1200 + Math.random() * 1200);
  replyBtn.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
  await sleep(300 + Math.random() * 500);
  replyBtn.click();
  await sleep(1500);

  // The reply composer should now be inside `target`. Find the freshly-opened editor.
  const composer = target.querySelector('.ql-editor[contenteditable="true"], [contenteditable="true"][role="textbox"]');
  if (!composer) return { ok: false, error: 'reply composer did not open' };

  composer.focus();
  composer.dispatchEvent(new MouseEvent('click', { bubbles: true }));
  await sleep(500 + Math.random() * 700);
  await typeIntoEditable(composer, replyText, throttle);
  await sleep(800);

  // Find the submit button inside the reply composer (sibling of the editor).
  // LinkedIn uses .comments-comment-box__submit-button (same as top-level
  // comment) — there can be multiple on the page, so we pick the one nearest
  // to OUR composer.
  let container = composer.closest('form, .comments-comment-box, [class*="comments-comment-box"]') || composer.parentElement;
  for (let d = 0; d < 6 && container && container.parentElement; d++) {
    if (container.querySelector('button.comments-comment-box__submit-button, [class*="comments-comment-box__submit"]')) break;
    container = container.parentElement;
  }
  let submit = container && container.querySelector('button.comments-comment-box__submit-button, [class*="comments-comment-box__submit"]');
  if (!submit) {
    submit = container && Array.from(container.querySelectorAll('button')).find(b => {
      const t = (b.innerText || '').trim();
      return /^(Reply|Post|Comment|הגב|פרסם)$/i.test(t);
    });
  }
  if (!submit) return { ok: false, error: 'reply submit button not found' };

  const enableStart = Date.now();
  while (submit.disabled || submit.getAttribute('aria-disabled') === 'true') {
    if (Date.now() - enableStart > 6000) return { ok: false, error: 'reply submit disabled' };
    await sleep(300);
  }
  submit.click();
  await sleep(2500);

  const warn = detectLinkedInWarning();
  if (warn) return { ok: false, error: `linkedin-warning:${warn.kind}`, warning: warn };
  reportStep('info', '💬↩ ✅ Reply submitted');
  return { ok: true };
}

// ---------- Keyword/Hashtag search engagement (the growth flywheel) ----------
// THE highest-leverage organic growth tactic in 2026: find HOT recent posts on
// a keyword/hashtag, leave value-add comments → 7x more profile views.
//
// MUST be invoked AFTER the SW navigated the tab to:
//   https://www.linkedin.com/search/results/content/?keywords=<URL-encoded>&sortBy=%22date_posted%22
//
// New LinkedIn 2026 search UI has obfuscated class names and NO data-urn on
// post containers. We identify post cards STRUCTURALLY: walk up from each
// Reaction button until we find a container that also has an author link.

function findSearchResultPostCards() {
  const reactBtns = document.querySelectorAll(
    'button[aria-label="React Like"], button[aria-label*="Reaction button" i]'
  );
  const cards = [];
  const seen = new Set();
  for (const btn of reactBtns) {
    let card = btn;
    let depth = 0;
    while (card.parentElement && depth < 18) {
      card = card.parentElement;
      depth++;
      const hasAuthor = card.querySelector('a[href*="/in/"], a[href*="/company/"]');
      const hasComment = Array.from(card.querySelectorAll('button[aria-label]')).some(b =>
        /^Comment$/i.test(b.getAttribute('aria-label') || ''));
      if (hasAuthor && hasComment && card.contains(btn)) {
        // Heuristic: post card is the smallest ancestor containing all three signals
        break;
      }
    }
    if (seen.has(card)) continue;
    seen.add(card);
    cards.push({ card, reactBtn: btn });
    if (cards.length >= 25) break;
  }
  return cards;
}

function extractPostCardMeta({ card }) {
  const authorAnchor = card.querySelector('a[href*="/in/"], a[href*="/company/"]');
  const author = (authorAnchor?.innerText || '').split('\n')[0].trim().slice(0, 80);
  const authorUrl = authorAnchor ? (authorAnchor.href || '').split('?')[0] : null;
  // Body text — try several wrappers, fall back to card text minus author
  const bodyEl = card.querySelector(
    '.update-components-text, [class*="feed-shared-update-v2__description"], [class*="update-components-text"], span[dir="ltr"]'
  );
  let text = (bodyEl?.innerText || '').trim();
  if (!text || text.length < 20) {
    const full = (card.innerText || '').trim();
    text = full.replace(author, '').replace(/^\s*[•·•\-—\s]+/, '').trim();
  }
  text = text.split(/\nLike\nComment\nRepost/i)[0].trim().slice(0, 600);
  return { author, authorUrl, text };
}

// Ask the background SW to generate a per-post LinkedIn comment via Gemini.
// Returns the text on success, null on any failure (callers must fall back).
async function requestAiCommentForPost(postText, authorName) {
  try {
    const r = await chrome.runtime.sendMessage({
      type: 'GENERATE_LI_COMMENT',
      payload: { postText, authorName },
    });
    if (r?.ok && r.text && r.text.length >= 12) return r.text;
  } catch {}
  return null;
}

async function humanlikeSearchEngage({
  maxLikes = 3,
  maxComments = 0,
  commentText = '',
  aiCommentEnabled = false,
  skipAuthorUrls = [],
  minBodyChars = 60,
  throttle = {},
} = {}) {
  reportStep('info', `🔍 Search engagement: scrolling to load posts...`);
  // Multi-pass scroll to lazy-load
  for (let i = 0; i < 6; i++) {
    window.scrollBy({ top: 600 + Math.random() * 400, behavior: 'smooth' });
    await sleep(1200 + Math.random() * 800);
  }
  window.scrollTo({ top: 0, behavior: 'smooth' });
  await sleep(1500);

  const cards = findSearchResultPostCards();
  reportStep('info', `🔍 Found ${cards.length} post cards`);
  if (cards.length === 0) return { ok: false, error: 'no posts found' };

  // Filter: skip known authors, skip too-short posts, skip already-liked
  const candidates = [];
  for (const c of cards) {
    const meta = extractPostCardMeta(c);
    if (!meta.author) continue;
    if (meta.text.length < minBodyChars) continue;
    if (skipAuthorUrls.includes(meta.authorUrl)) continue;
    const already = c.reactBtn.getAttribute('aria-pressed') === 'true'
      || /unlike/i.test(c.reactBtn.getAttribute('aria-label') || '');
    if (already) continue;
    candidates.push({ ...c, ...meta });
    if (candidates.length >= 12) break;
  }

  reportStep('info', `🔍 ${candidates.length} engageable candidates`);
  if (candidates.length === 0) return { ok: false, error: 'no candidates after filtering' };

  // Shuffle to look human (don't always engage with top-of-list)
  const shuffled = [...candidates].sort(() => Math.random() - 0.5);

  const results = { liked: [], commented: [], skipped: [] };

  // Phase 1: like up to maxLikes posts
  for (let i = 0; i < Math.min(maxLikes, shuffled.length); i++) {
    const c = shuffled[i];
    try {
      c.card.scrollIntoView({ behavior: 'smooth', block: 'center' });
      await sleep(2000 + Math.random() * 2500); // dwell — "reading"
      c.reactBtn.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
      await sleep(400 + Math.random() * 600);
      c.reactBtn.click();
      results.liked.push({ author: c.author, authorUrl: c.authorUrl, textPreview: c.text.slice(0, 100) });
      reportStep('info', `  👍 Liked ${i + 1}/${maxLikes}: ${c.author}`);
      await sleep(3500 + Math.random() * 4000); // pause between actions

      const warn = detectLinkedInWarning();
      if (warn) {
        reportStep('error', `⚠ Warning after like: ${warn.kind} — aborting`);
        return { ok: false, error: `linkedin-warning:${warn.kind}`, warning: warn, results };
      }
    } catch (e) {
      results.skipped.push({ reason: e.message, author: c.author });
    }
  }

  // Phase 2: comment on ONE post if requested
  // Two modes:
  //   (a) aiCommentEnabled — ask Gemini per-post for a unique value-add comment
  //   (b) static commentText — same template every time (less effective, safer)
  const canComment = maxComments > 0 && (aiCommentEnabled || (commentText && commentText.trim().length >= 5));
  if (canComment) {
    // Pick a fresh candidate we haven't liked (engagement diversity)
    const commentTarget = shuffled[Math.min(maxLikes, shuffled.length - 1)] || shuffled[0];
    if (commentTarget) {
      try {
        // Resolve the comment text
        let textToPost = commentText;
        if (aiCommentEnabled) {
          reportStep('info', `🧠 Requesting AI comment for post by ${commentTarget.author}...`);
          const aiText = await requestAiCommentForPost(commentTarget.text, commentTarget.author);
          if (aiText) {
            textToPost = aiText;
            reportStep('info', `🧠 ✓ AI returned: "${aiText.slice(0, 80)}${aiText.length > 80 ? '…' : ''}"`);
          } else if (!commentText) {
            reportStep('warn', '🧠 AI comment failed and no fallback text — skipping comment phase');
            return { ok: true, results };
          } else {
            reportStep('warn', '🧠 AI comment failed — using static fallback template');
          }
        }
        if (!textToPost || textToPost.trim().length < 5) {
          reportStep('warn', '💬 no usable comment text — skipping comment phase');
          return { ok: true, results };
        }

        commentTarget.card.scrollIntoView({ behavior: 'smooth', block: 'center' });
        await sleep(2500 + Math.random() * 2500);
        // Find the Comment action button INSIDE this card
        const commentActionBtn = Array.from(commentTarget.card.querySelectorAll('button[aria-label]')).find(b =>
          /^Comment$/i.test(b.getAttribute('aria-label') || '') || /^הגב/i.test(b.getAttribute('aria-label') || ''));
        if (!commentActionBtn) throw new Error('no comment action button');
        commentActionBtn.click();
        await sleep(1800);

        // Find the comment composer that opened — should be inside this card
        const composer = commentTarget.card.querySelector('.ql-editor[contenteditable="true"], [contenteditable="true"][role="textbox"]');
        if (!composer) throw new Error('no comment composer opened');

        composer.focus();
        composer.dispatchEvent(new MouseEvent('click', { bubbles: true }));
        await sleep(500 + Math.random() * 700);
        await typeIntoEditable(composer, textToPost, throttle);
        await sleep(900);

        // Find the submit button inside the card
        const submit = Array.from(commentTarget.card.querySelectorAll('button')).find(b => {
          const t = (b.innerText || '').trim();
          return /^(Comment|Post|הגב|פרסם)$/i.test(t) && !b.disabled;
        }) || commentTarget.card.querySelector('button.comments-comment-box__submit-button, [class*="comments-comment-box__submit"]');
        if (!submit) throw new Error('no comment submit');

        const enableStart = Date.now();
        while (submit.disabled || submit.getAttribute('aria-disabled') === 'true') {
          if (Date.now() - enableStart > 5000) throw new Error('comment submit disabled');
          await sleep(300);
        }
        submit.click();
        await sleep(2500);

        const warn = detectLinkedInWarning();
        if (warn) {
          reportStep('error', `⚠ Warning after comment: ${warn.kind}`);
          return { ok: false, error: `linkedin-warning:${warn.kind}`, warning: warn, results };
        }
        results.commented.push({ author: commentTarget.author, authorUrl: commentTarget.authorUrl, text: textToPost, aiGenerated: aiCommentEnabled && textToPost !== commentText });
        reportStep('info', `  💬 Commented on: ${commentTarget.author}`);
      } catch (e) {
        results.skipped.push({ reason: 'comment-failed:' + e.message, author: commentTarget.author });
      }
    }
  }

  reportStep('info', `🔍 Done. liked=${results.liked.length} commented=${results.commented.length}`);
  return { ok: true, results };
}

// ---------- Follow user (warm-up surface, strict cap) ----------
// Run on a /in/<handle>/ profile. Click Follow if not already, optionally like
// one of their pinned/recent posts. NEVER sends connection requests or DMs.
async function followCurrentProfile({ alsoLikeRecentPost = true } = {}) {
  reportStep('info', '👤 LinkedIn: follow profile flow');
  if (!/^\/in\//.test(location.pathname)) return { ok: false, error: 'not on a profile page' };

  // The Follow button on a profile is labeled "Follow <Name>" or just "Follow".
  // After following it becomes "Following" / aria-pressed=true.
  await sleep(1500);
  let followBtn = Array.from(document.querySelectorAll('button')).find(b => {
    const t = (b.innerText || '').trim();
    const a = (b.getAttribute('aria-label') || '').trim();
    if (/^Follow$/i.test(t) || /^Follow /i.test(a)) return true;
    return false;
  });
  // Sometimes Follow is hidden in the "More" overflow menu
  if (!followBtn) {
    const moreBtn = Array.from(document.querySelectorAll('button[aria-label*="More" i]')).find(b =>
      b.closest('.pv-top-card, [class*="profile-top"]'));
    if (moreBtn) {
      moreBtn.click();
      await sleep(800);
      followBtn = Array.from(document.querySelectorAll('[role="menuitem"]')).find(mi =>
        /^Follow$/i.test((mi.innerText || '').trim()));
    }
  }
  if (!followBtn) return { ok: false, error: 'Follow button not found (already following?)' };

  const pressed = followBtn.getAttribute('aria-pressed') === 'true';
  if (!pressed) {
    followBtn.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
    await sleep(400 + Math.random() * 600);
    followBtn.click();
    await sleep(1500);
    reportStep('info', '  ✓ Followed profile');
  } else {
    reportStep('info', '  ⚠ Already following');
  }

  const warn = detectLinkedInWarning();
  if (warn) return { ok: false, error: `linkedin-warning:${warn.kind}`, warning: warn };

  // Optionally like one of their recent posts to send a stronger warm signal
  if (alsoLikeRecentPost) {
    try {
      // Scroll to the Activity section
      window.scrollBy({ top: 1200, behavior: 'smooth' });
      await sleep(2500);
      const firstReaction = document.querySelector('button[aria-label="React Like"]:not([aria-pressed="true"])');
      if (firstReaction) {
        firstReaction.scrollIntoView({ behavior: 'smooth', block: 'center' });
        await sleep(1500 + Math.random() * 1500);
        firstReaction.click();
        await sleep(1500);
        reportStep('info', '  👍 Liked a recent post');
      }
    } catch (e) {
      reportStep('warn', `like-recent failed: ${e.message}`);
    }
  }

  return { ok: true, signal: pressed ? 'already-following' : 'now-following' };
}

// ---------- Profile Auditor — scrape your OWN profile for a Gemini audit ----------
// Must be invoked on a /in/<handle>/ page. We scrape the key sections and
// return them as structured data; the SW asks Gemini to score + suggest fixes.
function scrapeProfileForAudit() {
  if (!/^\/in\//.test(location.pathname)) {
    return { ok: false, error: 'not on a profile page (expected /in/...)' };
  }

  const grab = (sel, root = document) => {
    const el = root.querySelector(sel);
    return el ? (el.innerText || '').trim() : '';
  };
  const grabAll = (sel, root = document) =>
    Array.from(root.querySelectorAll(sel)).map(el => (el.innerText || '').trim()).filter(Boolean);

  // Name + headline live in the top card
  const topCard = document.querySelector('section.pv-top-card, .ph5.pb5, main section:first-of-type');
  const name = grab('h1', topCard || document).split('\n')[0];
  const headline = grab('.text-body-medium.break-words, [data-generated-suggestion-target] + div, .pv-top-card .text-body-medium');
  const location_ = grab('.text-body-small.inline.t-black--light.break-words, .pv-top-card .text-body-small');

  // Banner / avatar present?
  const hasAvatar = !!document.querySelector('.pv-top-card-profile-picture img, .profile-photo-edit__preview-photo, .presence-entity img');
  const avatarSrc = document.querySelector('.pv-top-card-profile-picture img, .pv-top-card__photo')?.src || '';
  // Banner: detect a custom (non-default) banner — default is a gradient blob
  const bannerSrc = document.querySelector('.profile-background-image img, .pv-top-card__cover-image')?.src || '';
  const hasCustomBanner = !!bannerSrc && !/default|profile-displaybackgroundimage/i.test(bannerSrc);

  // Contact info — usually behind a modal, just check if button exists
  const hasContactInfo = !!Array.from(document.querySelectorAll('a, button')).find(el =>
    /contact info|פרטי קשר/i.test((el.innerText || el.getAttribute('aria-label') || '')));

  // About / Summary — first big text block under "About"
  let about = '';
  const sections = document.querySelectorAll('section.artdeco-card, section.pv-profile-card, main section');
  for (const s of sections) {
    const h2 = s.querySelector('h2, .pv-profile-card__heading');
    const heading = (h2?.innerText || '').toLowerCase();
    if (/^about/.test(heading) || /^אודות/.test(heading) || /^summary/.test(heading)) {
      const body = s.querySelector('[class*="inline-show-more-text"], .pv-shared-text-with-see-more, .pv-about__summary-text');
      about = (body?.innerText || s.innerText || '').replace(/^About\n*|^אודות\n*|^Summary\n*/i, '').trim();
      // Strip "...see more" suffix
      about = about.replace(/…\s*(?:see more|הצג עוד).*$/i, '').slice(0, 3000);
      break;
    }
  }

  // Experience entries — title + company + duration
  const experiences = [];
  for (const s of sections) {
    const h2 = (s.querySelector('h2')?.innerText || '').toLowerCase();
    if (!(/^experience/.test(h2) || /^ניסיון/.test(h2))) continue;
    const items = s.querySelectorAll('li, .pv-position-entity, .pvs-list__paged-list-item');
    for (const li of items) {
      const lines = (li.innerText || '').split('\n').map(s => s.trim()).filter(Boolean);
      // Dedup obvious noise lines
      const filtered = lines.filter(l => l.length < 200 && !/^(helpful|מועיל|reactions|תגובות)$/i.test(l));
      if (filtered.length >= 2) experiences.push(filtered.slice(0, 5).join(' · '));
      if (experiences.length >= 8) break;
    }
    if (experiences.length) break;
  }

  // Education entries
  const education = [];
  for (const s of sections) {
    const h2 = (s.querySelector('h2')?.innerText || '').toLowerCase();
    if (!(/^education/.test(h2) || /^השכלה/.test(h2))) continue;
    const items = s.querySelectorAll('li, .pvs-list__paged-list-item');
    for (const li of items) {
      const lines = (li.innerText || '').split('\n').map(s => s.trim()).filter(Boolean);
      if (lines.length >= 1) education.push(lines.slice(0, 4).join(' · '));
      if (education.length >= 5) break;
    }
    if (education.length) break;
  }

  // Skills — count + first 10
  const skills = [];
  for (const s of sections) {
    const h2 = (s.querySelector('h2')?.innerText || '').toLowerCase();
    if (!(/^skills/.test(h2) || /^כישורים/.test(h2))) continue;
    const items = s.querySelectorAll('li, .pvs-list__paged-list-item');
    for (const li of items) {
      const t = (li.innerText || '').split('\n')[0].trim();
      if (t && t.length < 80) skills.push(t);
      if (skills.length >= 20) break;
    }
    break;
  }

  // Featured section — present or not (huge signal)
  const hasFeatured = !!Array.from(sections).find(s => {
    const h = (s.querySelector('h2')?.innerText || '').toLowerCase();
    return /^featured/.test(h) || /^מודגש/.test(h);
  });

  // Recommendations
  let recommendationCount = 0;
  for (const s of sections) {
    const h2 = (s.querySelector('h2')?.innerText || '').toLowerCase();
    if (/^recommendations/.test(h2) || /^המלצות/.test(h2)) {
      recommendationCount = s.querySelectorAll('li, .pvs-list__paged-list-item').length;
      break;
    }
  }

  // Activity/Posts count (rough — counts posts visible in activity section)
  let activityVisible = 0;
  for (const s of sections) {
    const h2 = (s.querySelector('h2')?.innerText || '').toLowerCase();
    if (/^activity/.test(h2) || /^פעילות/.test(h2)) {
      activityVisible = s.querySelectorAll('li, [data-id^="urn:li:activity"]').length;
      break;
    }
  }

  return {
    ok: true,
    profile: {
      name, headline, location: location_,
      hasAvatar, hasCustomBanner, hasContactInfo,
      about, aboutLength: about.length,
      experiences, experienceCount: experiences.length,
      education, educationCount: education.length,
      skills: skills.slice(0, 15), skillCount: skills.length,
      hasFeatured, recommendationCount,
      activityVisible,
      profileUrl: location.href.split('?')[0],
      scannedAt: Date.now(),
    },
  };
}

// ---------- Find OUR most recent post URL by snippet ----------
// LI post permalink shape: https://www.linkedin.com/feed/update/urn:li:activity:NNN/
function findPostUrlInFeed(snippet) {
  if (!snippet) return null;
  const needle = snippet.replace(/\s+/g, ' ').trim();
  if (needle.length < 15) return null;
  const articles = document.querySelectorAll('[data-urn^="urn:li:activity:"], [data-id^="urn:li:activity:"]');
  for (const art of articles) {
    if (art.closest('[role="dialog"]')) continue;
    if ((art.innerText || '').replace(/\s+/g, ' ').includes(needle)) {
      const urn = art.getAttribute('data-urn') || art.getAttribute('data-id');
      if (urn) return `https://www.linkedin.com/feed/update/${urn}/`;
      // Try permalink anchor
      const a = art.querySelector('a[href*="/feed/update/"], a[href*="/posts/"]');
      if (a) {
        const h = a.getAttribute('href');
        return h.startsWith('http') ? h : `https://www.linkedin.com${h}`;
      }
    }
  }
  return null;
}

// ---------- Message handler ----------

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
  (async () => {
    try {
      if (msg.type === 'PUBLISH_POST') {
        const r = await publishPost(msg.payload || {});
        return sendResponse(r);
      }
      if (msg.type === 'FIND_POST_URL') {
        const url = findPostUrlInFeed(msg.payload?.snippet || '');
        return sendResponse({ ok: true, url: url || location.href });
      }
      if (msg.type === 'SCRAPE_POST_ENGAGEMENT') {
        return sendResponse({ ok: true, data: scrapePostEngagement() });
      }
      if (msg.type === 'HARVEST_COMMENTS') {
        const comments = await harvestComments(msg.payload || {});
        return sendResponse({ ok: true, comments });
      }
      if (msg.type === 'POST_COMMENT') {
        const r = await postCommentOnCurrentPage(msg.payload?.text || '', msg.payload?.throttle || {});
        return sendResponse(r);
      }
      if (msg.type === 'REACT_LIKE') {
        const r = await reactLikeOnCurrentPost();
        return sendResponse(r);
      }
      if (msg.type === 'HUMAN_BROWSE_PAGE') {
        const r = await humanBrowsePage(msg.durationMs || 10000);
        return sendResponse(r);
      }
      if (msg.type === 'SCRAPE_MY_GROUPS') {
        const r = await scrapeMyGroups();
        if (r.ok) {
          try {
            await chrome.runtime.sendMessage({ type: 'SCRAPE_MY_GROUPS_RESULT', payload: r.groups });
          } catch {}
        }
        return sendResponse(r);
      }
      if (msg.type === 'SCRAPE_GROUP_METADATA') {
        return sendResponse({ ok: true, meta: scrapeGroupMetadata() });
      }
      if (msg.type === 'AUDIT_PROFILE') {
        // Scrapes the currently-open LinkedIn profile and returns structured
        // data. The SW takes it from here to Gemini for the actual audit.
        return sendResponse(scrapeProfileForAudit());
      }
      if (msg.type === 'REPLY_TO_COMMENT') {
        const r = await replyToCommentByAuthor(msg.payload || {});
        return sendResponse(r);
      }
      if (msg.type === 'SEARCH_ENGAGE') {
        const r = await humanlikeSearchEngage(msg.payload || {});
        return sendResponse(r);
      }
      if (msg.type === 'FOLLOW_USER') {
        const r = await followCurrentProfile(msg.payload || {});
        return sendResponse(r);
      }
      if (msg.type === 'DEBUG_PROBE') {
        const opener = querySelectorAny(LI_SELECTORS.composerOpen);
        return sendResponse({
          ok: true,
          platform: 'linkedin',
          url: location.href,
          pathname: location.pathname,
          composerFound: !!opener,
          composerText: opener?.innerText?.slice(0, 80) || opener?.getAttribute('aria-label') || null,
          dialogOpen: !!document.querySelector(LI_SELECTORS.dialog),
          onGroupPage: /^\/groups\//.test(location.pathname),
          onPostPage: /\/feed\/update\//.test(location.pathname),
        });
      }
      sendResponse({ ok: false, error: 'unknown message type' });
    } catch (e) {
      sendResponse({ ok: false, error: String(e?.message || e) });
    }
  })();
  return true;
});

console.log('[reachlab-li] content script loaded on', location.pathname);
