// Twitter / X platform adapter.
// X uses data-testid attributes pervasively — much more stable selectors
// than FB or LinkedIn.

const TW_SELECTORS = {
  // Composer trigger (the floating + button at bottom right or sidebar "Post" button)
  composerOpen: [
    '[data-testid="SideNav_NewTweet_Button"]',
    '[data-testid="FloatingActionButton"]',
    'a[href="/compose/post"]',
    '[aria-label="Post"][role="link"]',
    '[aria-label="פוסט"][role="link"]',
  ],
  // The composer dialog
  dialog: '[role="dialog"][aria-labelledby], [aria-modal="true"]',
  // The editable area
  editable: [
    '[data-testid="tweetTextarea_0"]',
    '[data-testid="tweetTextarea_0_label"]',
    'div[contenteditable="true"][data-testid*="tweetTextarea"]',
    'div[role="textbox"][contenteditable="true"]',
  ],
  // Submit button
  submit: [
    '[data-testid="tweetButton"]',
    '[data-testid="tweetButtonInline"]',
  ],
  // File input
  fileInput: [
    '[data-testid="fileInput"]',
    'input[type="file"]',
  ],
  // Character count (X shows e.g. "240/280" or just remaining)
  charCount: '[data-testid="tweetTextarea_0CharacterCount"]',
};

// Max chars for free X — premium goes to 25k, but assume free
const MAX_TWEET_CHARS = 280;

// ---------- Utilities ----------

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: ${JSON.stringify(selectors)}`));
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });
  });
}

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

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

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

function* typingPlan(text, cfg) {
  const min = cfg.typingMinMsPerChar ?? 30;
  const max = cfg.typingMaxMsPerChar ?? 100;
  const pauseChance = cfg.typingPauseChance ?? 0.06;
  for (const ch of text) {
    let d = min + Math.random() * (max - min);
    if (/[.,!?\n]/.test(ch)) d *= 2;
    if (Math.random() < pauseChance) d += 200 + Math.random() * 800;
    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.click();
  for (const { ch, delayMs } of typingPlan(text, cfg)) {
    dispatchKeyChar(el, ch);
    await sleep(delayMs);
  }
}

// ---------- Humanlike browsing (Twitter-paced — faster than LinkedIn) ----------

async function humanlikeTwitterBrowse(minSec, maxSec) {
  const totalMs = (minSec + Math.random() * (maxSec - minSec)) * 1000;
  const start = Date.now();
  reportStep('info', `👤 Twitter browse for ${Math.round(totalMs / 1000)}s`);

  await sleep(300 + Math.random() * 800);
  while (Date.now() - start < totalMs) {
    // Twitter feed scrolls fast — smaller chunks, shorter pauses
    window.scrollBy({ top: 200 + Math.random() * 400, behavior: 'smooth' });
    await sleep(1500 + Math.random() * 2500);
    // Occasional hover
    if (Math.random() < 0.2) {
      const tweets = document.querySelectorAll('article[data-testid="tweet"]');
      if (tweets.length) {
        const t = tweets[Math.floor(Math.random() * Math.min(3, tweets.length))];
        t.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
        await sleep(800 + Math.random() * 1200);
        t.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
      }
    }
  }
  window.scrollTo({ top: 0, behavior: 'smooth' });
  await sleep(1000);
  reportStep('info', '👤 Done browsing');
}

// ---------- Warning detection ----------

function detectTwitterWarning() {
  const patterns = [
    { kind: 'rate-limit', regex: /you are over the daily limit|rate limit exceeded|too many requests/i },
    { kind: 'rate-limit', regex: /הגעת למגבלה|חרגת מהמגבלה/i },
    { kind: 'action-blocked', regex: /this action is restricted|action blocked|something went wrong/i },
    { kind: 'duplicate', regex: /duplicate|status is a duplicate|already posted/i },
    { kind: 'duplicate', regex: /כפול|פוסט זהה/i },
    { kind: 'captcha', regex: /verify you'?re human|prove you'?re a human|security check/i },
    { kind: 'captcha', regex: /אמת שאתה לא בוט|בדיקת אבטחה/i },
    { kind: 'suspended', regex: /your account is suspended|account locked|account has been locked/i },
    { kind: 'suspended', regex: /החשבון הושעה|נעול/i },
  ];
  const scope = [
    ...document.querySelectorAll('[role="dialog"], [role="alert"]'),
    ...document.querySelectorAll('[data-testid*="error" i], [data-testid*="toast" i]'),
  ];
  for (const el of scope) {
    const text = (el.innerText || '').slice(0, 1000);
    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 = {} }) {
  reportStep('info', `📍 Twitter publish on ${location.pathname}`);
  await sleep(1500);

  const warn = detectTwitterWarning();
  if (warn) {
    reportStep('error', `⚠ Twitter warning: ${warn.kind}`);
    return { ok: false, warning: warn };
  }

  // Enforce length (free X account)
  if (text.length > MAX_TWEET_CHARS) {
    reportStep('warn', `Tweet too long (${text.length} > ${MAX_TWEET_CHARS}), truncating`);
    text = text.slice(0, MAX_TWEET_CHARS - 3) + '...';
  }

  // 0) Pre-post browsing
  const browseMin = throttle.preBrowseMinSec ?? 15;
  const browseMax = throttle.preBrowseMaxSec ?? 45;
  if (browseMin > 0) await humanlikeTwitterBrowse(browseMin, browseMax);

  // 1) Open composer (or use inline composer if on home feed)
  reportStep('info', 'Opening composer...');
  let editable = querySelectorAny(TW_SELECTORS.editable);

  // If no inline composer visible, click the floating button
  if (!editable) {
    const opener = querySelectorAny(TW_SELECTORS.composerOpen);
    if (!opener) {
      reportStep('error', '❌ No composer trigger found');
      return { ok: false, error: 'composer not found' };
    }
    await sleep(randBetween(500, 1500));
    opener.click();
    await sleep(1500);
    try { editable = await waitFor(TW_SELECTORS.editable, { timeout: 8000 }); }
    catch {
      reportStep('error', '❌ Editable not found after opening composer');
      return { ok: false, error: 'editable not found' };
    }
  }
  reportStep('info', '✓ Composer ready');

  // 2) Type
  await sleep(randBetween(300, 1000));
  await typeIntoEditable(editable, text, throttle);
  reportStep('info', `✓ Typed ${text.length} chars`);

  // 3) Media
  if (media && media.length) {
    reportStep('info', `Attaching ${media.length} media...`);
    try { await attachTwitterMedia(media); reportStep('info', '✓ Media attached'); }
    catch (e) { reportStep('warn', `Media failed: ${e.message}`); }
  }

  // 4) Submit
  await sleep(randBetween(800, 2000));
  let submit = querySelectorAny(TW_SELECTORS.submit);
  if (!submit) {
    reportStep('error', '❌ Tweet button not found');
    return { ok: false, error: 'submit not found' };
  }
  const start = Date.now();
  while (submit.getAttribute('aria-disabled') === 'true' || submit.disabled) {
    if (Date.now() - start > 8000) {
      reportStep('error', '❌ Tweet button stayed disabled');
      return { ok: false, error: 'submit disabled' };
    }
    await sleep(300);
    submit = querySelectorAny(TW_SELECTORS.submit) || submit;
  }
  reportStep('info', '✓ Clicking Tweet/Post button');
  submit.click();

  // 5) Outcome detection — Twitter typically shows toast "Your post was sent"
  // and the composer closes. URL doesn't change.
  const outcome = await detectOutcome(text, 10000);
  reportStep('info', `📤 Outcome: ${outcome.signal}`);
  if (!outcome.success) return { ok: false, error: outcome.signal };
  return { ok: true, signal: outcome.signal };
}

async function detectOutcome(text, timeoutMs) {
  const snippet = text.replace(/\s+/g, ' ').trim().slice(0, 30);
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    // Look for success toast
    const status = document.querySelector('[data-testid="toast"], [role="alert"]');
    if (status) {
      const t = (status.innerText || '').toLowerCase();
      if (/your post was sent|tweet sent|פוסט נשלח/.test(t)) {
        return { success: true, signal: 'toast-success' };
      }
      if (/failed|error|נכשל|שגיאה/.test(t)) {
        return { success: false, signal: `toast-failure: ${t.slice(0, 60)}` };
      }
    }
    // Look for tweet in feed
    if (snippet && findTweetInFeed(snippet)) {
      return { success: true, signal: 'tweet-visible' };
    }
    // Composer closed (no dialog, no editable visible)
    if (!document.querySelector(TW_SELECTORS.editable) && !document.querySelector(TW_SELECTORS.dialog)) {
      return { success: true, signal: 'composer-closed' };
    }
    await sleep(400);
  }
  return { success: true, signal: 'optimistic-no-failure' };
}

function findTweetInFeed(snippet) {
  const needle = snippet.replace(/\s+/g, ' ').trim();
  const tweets = document.querySelectorAll('article[data-testid="tweet"]');
  for (const t of tweets) {
    if ((t.innerText || '').replace(/\s+/g, ' ').includes(needle)) return true;
  }
  return false;
}

async function attachTwitterMedia(mediaList) {
  const files = mediaList.map((m) => new File([new Uint8Array(m.buffer)], m.name, { type: m.mimeType }));
  let input = querySelectorAny(TW_SELECTORS.fileInput);
  if (!input) {
    // X requires clicking the media button to reveal the input
    const mediaBtn = document.querySelector('[data-testid="attachments"]');
    if (mediaBtn) {
      mediaBtn.click();
      await sleep(600);
      input = querySelectorAny(TW_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 }));
  await sleep(3000);
}

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

function scrapePostEngagement() {
  const out = { reactions: 0, comments: 0, shares: 0, views: null };
  // Look at the focal tweet's action bar
  const actionBars = document.querySelectorAll('[role="group"][aria-label*="repl" i], [role="group"][aria-label*="retweet" i]');
  // Twitter bundles counts in aria-labels: "1.2K replies, 532 reposts, 8.1K likes, 47K views"
  const labelEl = document.querySelector('[role="group"][aria-label]');
  if (labelEl) {
    const lbl = labelEl.getAttribute('aria-label') || '';
    const re = (kw) => {
      const m = lbl.match(new RegExp(`([\\d,.]+\\s*[KM]?)\\s*${kw}`, 'i'));
      return m ? parseTwCount(m[1]) : 0;
    };
    out.comments = re('repl');
    out.shares = re('repost|retweet');
    out.reactions = re('like');
    out.views = re('view') || null;
  }
  return out;
}

function parseTwCount(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;
}

// ---------- Search-engage growth flywheel (X/Twitter) ----------
// Mirrors the LinkedIn pattern. Twitter is more selector-stable than LI
// (data-testid="tweet", data-testid="like", data-testid="reply"), so the
// implementation is simpler. Conservative caps: like-only by default since
// X penalizes repetitive engagement patterns aggressively in 2026.
async function humanlikeTwitterSearchEngage({
  maxLikes = 5, skipHandles = [], minBodyChars = 40,
} = {}) {
  // Scroll several times to lazy-load
  for (let i = 0; i < 5; i++) {
    window.scrollBy({ top: 700 + Math.random() * 400, behavior: 'smooth' });
    await sleep(1200 + Math.random() * 800);
  }
  window.scrollTo({ top: 0, behavior: 'smooth' });
  await sleep(1200);

  const tweets = document.querySelectorAll('article[data-testid="tweet"]');
  const candidates = [];
  for (const t of tweets) {
    const likeBtn = t.querySelector('button[data-testid="like"]');
    if (!likeBtn) continue; // already liked = data-testid="unlike"
    const authorLink = t.querySelector('a[role="link"][href^="/"]:not([href*="/status/"])');
    const handle = authorLink ? (authorLink.getAttribute('href') || '').slice(1) : null;
    if (!handle || skipHandles.includes(handle)) continue;
    const textEl = t.querySelector('[data-testid="tweetText"]');
    const text = (textEl?.innerText || '').trim();
    if (text.length < minBodyChars) continue;
    candidates.push({ tweet: t, likeBtn, handle, text });
    if (candidates.length >= 15) break;
  }
  if (!candidates.length) return { ok: false, error: 'no candidates' };

  const shuffled = candidates.sort(() => Math.random() - 0.5);
  const results = { liked: [] };
  for (let i = 0; i < Math.min(maxLikes, shuffled.length); i++) {
    const c = shuffled[i];
    try {
      c.tweet.scrollIntoView({ behavior: 'smooth', block: 'center' });
      await sleep(2000 + Math.random() * 2500);
      c.likeBtn.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
      await sleep(300 + Math.random() * 600);
      c.likeBtn.click();
      results.liked.push({ handle: c.handle, textPreview: c.text.slice(0, 100) });
      await sleep(3500 + Math.random() * 4000);
      // Bail on any visible rate-limit toast
      const toast = document.body.innerText.match(/rate limit|too many|try again later|please wait/i);
      if (toast) return { ok: false, error: 'rate-limit-detected', results };
    } catch (e) { /* skip and continue */ }
  }
  return { ok: true, results };
}

// ---------- Comment harvest (leads pipeline) ----------
// X/Twitter replies on a post page are <article data-testid="tweet"> nodes
// nested under the conversation. Each contains an author link and the reply text.
async function harvestComments({ maxComments = 200 } = {}) {
  // Multi-pass scroll to lazy-load
  for (let i = 0; i < 6; i++) {
    window.scrollBy({ top: 800 + Math.random() * 400, behavior: 'smooth' });
    await sleep(900 + Math.random() * 500);
  }
  const articles = document.querySelectorAll('article[data-testid="tweet"], article[role="article"]');
  const out = [];
  const seen = new Set();
  // Skip the FIRST article (it's the parent post)
  let idx = -1;
  for (const a of articles) {
    idx++;
    if (idx === 0) continue;
    const authorLink = a.querySelector('a[role="link"][href^="/"]:not([href*="/status/"])');
    const handle = authorLink ? (authorLink.getAttribute('href') || '').slice(1) : null;
    if (!handle) continue;
    const author = handle;
    const authorUrl = `https://x.com/${handle}`;
    const textEl = a.querySelector('[data-testid="tweetText"], div[lang]');
    const text = (textEl?.innerText || '').trim().slice(0, 1500);
    if (!text || text.length < 2) continue;
    const key = handle + '::' + 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;
  }
  return out;
}

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

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
  (async () => {
    try {
      if (msg.type === 'PUBLISH_POST') {
        return sendResponse(await publishPost(msg.payload || {}));
      }
      if (msg.type === 'FIND_POST_URL') {
        return sendResponse({ ok: true, 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 === 'SEARCH_ENGAGE') {
        return sendResponse(await humanlikeTwitterSearchEngage(msg.payload || {}));
      }
      if (msg.type === 'DEBUG_PROBE') {
        return sendResponse({
          ok: true, platform: 'twitter', url: location.href, pathname: location.pathname,
          composerFound: !!querySelectorAny(TW_SELECTORS.composerOpen),
        });
      }
      sendResponse({ ok: false, error: 'unknown message type' });
    } catch (e) {
      sendResponse({ ok: false, error: String(e?.message || e) });
    }
  })();
  return true;
});

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