// ==UserScript==
// @name         Glassdoor Jobs → CSV
// @namespace    https://github.com/local/glassdoor-jobs-csv
// @version      1.0.0
// @description  Port of the "Glass Door" Automa workflow. Same extraction, same columns, no per-item block overhead.
// @author       you
// @match        https://*.glassdoor.com/*
// @match        https://*.glassdoor.co.uk/*
// @match        https://*.glassdoor.ca/*
// @match        https://*.glassdoor.co.in/*
// @match        https://*.glassdoor.com.au/*
// @match        https://*.glassdoor.ie/*
// @match        https://*.glassdoor.de/*
// @match        https://*.glassdoor.fr/*
// @match        https://*.glassdoor.nl/*
// @match        https://*.glassdoor.sg/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  /* ------------------------------------------------------------------ *
   * Config
   * ------------------------------------------------------------------ */
  const CFG = {
    defaultMax: 20,          // pre-filled value in the "Max jobs" box
    fileName: 'Glass Door',  // -> "Glass Door.csv"
    loadMoreTimeout: 9000,   // ms to wait for new cards after clicking "Show more"
    loadMoreRetries: 2,      // retries when a click yields nothing new
    scrollEachCard: false,   // true = scroll every card into view (slow, only if logos come back empty)
    closeModals: true,       // auto-dismiss the sign-up / job-alert popups that block loading
    dedupeByJobId: true,     // skip duplicates Glassdoor sometimes re-renders
  };

  // Column order — identical to the Automa `toRows(..., { headerOrder: cols })` block.
  const COLS = ['title', 'companyName', 'location', 'salaryMin', 'salaryMax',
                'salaryRaw', 'postedAge', 'easyApply', 'jobUrl'];

  const LIST_SELECTORS = [
    'ul[aria-label="Jobs List"]',
    'ul[aria-labelledby="left-column-heading"]',
    '[data-test="jobListings"] ul',
  ];
  const LOAD_MORE = 'button[data-test="load-more"], [data-test="load-more"] button';

  /* ------------------------------------------------------------------ *
   * Helpers
   * ------------------------------------------------------------------ */
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  async function waitFor(fn, timeout, step = 120) {
    const t0 = Date.now();
    while (Date.now() - t0 < timeout) {
      const v = fn();
      if (v) return v;
      await sleep(step);
    }
    return null;
  }

  const getList = () => LIST_SELECTORS.map((s) => document.querySelector(s)).find(Boolean) || null;

  function getCards() {
    const list = getList();
    if (!list) return [];
    return [...list.children].filter((li) => li.tagName === 'LI');
  }

  function dismissModals() {
    if (!CFG.closeModals) return;
    const sels = [
      '.modal_closeIcon', '[data-test="modal-close"]', 'button[aria-label="Close"]',
      '.CloseButton', '[alt="Close"]',
    ];
    for (const s of sels) {
      document.querySelectorAll(s).forEach((b) => { try { b.click(); } catch (_) {} });
    }
  }

  /* ------------------------------------------------------------------ *
   * Extraction — same field-for-field logic as the Automa JS block,
   * but takes the <li> directly instead of re-querying :nth-child(n).
   * ------------------------------------------------------------------ */
  function extractJob(li, index) {
    if (!li) return null;

    const txt = (sel) => li.querySelector(sel)?.textContent?.trim() || '';
    const attr = (sel, a) => li.querySelector(sel)?.getAttribute(a) || '';
    const abs = (u) => (u ? new URL(u, location.origin).href : '');

    const rawSalary = txt('[data-test="detailSalary"]').replace(/\s+/g, ' ');
    const toNum = (s) => {
      const n = parseFloat(s.replace(/[^0-9.]/g, ''));
      return /K/i.test(s) ? n * 1e3 : /M/i.test(s) ? n * 1e6 : n;
    };
    const parts = rawSalary.match(/\$[\d.,]+\s*[KM]?/gi) || [];

    return {
      index,
      jobId: li.dataset.jobid || attr('[data-test="job-title"]', 'id').replace('job-title-', ''),
      title: txt('[data-test="job-title"]'),
      jobUrl: abs(attr('[data-test="job-title"]', 'href')),
      trackingUrl: abs(attr('[data-test="job-link"]', 'href')),
      companyName: txt('[class*="compactEmployerName"], [class*="employerName"]'),
      companyRating: parseFloat(attr('[role="meter"]', 'aria-valuenow')) || '',
      companyLogo: attr('[class*="avatar-base_Image"], img', 'src'),
      location: txt('[data-test="emp-location"]'),
      salaryRaw: rawSalary,
      salaryMin: parts[0] ? toNum(parts[0]) : '',
      salaryMax: parts[1] ? toNum(parts[1]) : '',
      salaryEmployerProvided: /employer provided/i.test(rawSalary),
      salaryPerHour: /per hour|\/hr/i.test(rawSalary),
      postedAge: txt('[data-test="job-age"]'),
      badges: [...li.querySelectorAll('[class*="badgesContainer"] > *')]
        .map((b) => b.textContent.trim()).filter(Boolean).join(' | '),
      easyApply: /easy apply/i.test(li.textContent),
      isSelected: li.getAttribute('data-selected') === 'true',
    };
  }

  /* ------------------------------------------------------------------ *
   * CSV — mirrors flatten() + toRows() + Automa's csv writer
   * ------------------------------------------------------------------ */
  function cell(v) {
    if (v == null) return '';
    if (v instanceof Date) v = v.toISOString();
    else if (Array.isArray(v)) v = v.join(' | ');
    else if (typeof v === 'object') v = JSON.stringify(v);
    else v = String(v); // numbers & booleans keep their raw form: 121000 / true
    return /[",\n\r]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
  }

  function toCsv(rows, headers) {
    const lines = [headers.map(cell).join(',')];
    for (const r of rows) lines.push(headers.map((h) => cell(r[h])).join(','));
    return lines.join('\r\n');
  }

  function download(csv, name) {
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${name}.csv`;
    document.body.appendChild(a);
    a.click();
    a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 2000);
  }

  /* ------------------------------------------------------------------ *
   * Panel — same visual language as the Automa create-element block
   * ------------------------------------------------------------------ */
  const CSS = `
.gdp{position:fixed;top:12px;right:12px;z-index:2147483647;width:260px;
  padding:10px 12px;border-radius:8px;background:#fff;color:#1D2432;
  border:1px solid #E5E7EB;
  font:500 12px/1.4 -apple-system,"Segoe UI",Roboto,sans-serif;
  box-shadow:0 4px 16px rgba(29,36,50,.12)}
.gdp-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;gap:8px}
.gdp-label{font-weight:700}
.gdp-count{color:#505863;font-variant-numeric:tabular-nums}
.gdp-cur{color:#0CAA41;font-weight:700}
.gdp-track{height:6px;border-radius:99px;background:#E5E7EB;overflow:hidden}
.gdp-fill{height:100%;width:0;border-radius:99px;background:#0CAA41;transition:width .25s ease}
.gdp[data-done="1"] .gdp-fill{background:#1861BF}
.gdp-max-in{width:64px;padding:4px 6px;border:1px solid #E5E7EB;border-radius:6px;
  font:inherit;color:inherit;text-align:right}
.gdp-max-in:focus{outline:2px solid #0CAA41;outline-offset:-1px}
.gdp-btn{flex:1;padding:6px 8px;border:0;border-radius:6px;background:#0CAA41;color:#fff;
  font:700 12px/1 inherit;cursor:pointer}
.gdp-btn:hover{filter:brightness(.94)}
.gdp-btn[data-variant="ghost"]{background:#F1F3F5;color:#1D2432}
.gdp-note{margin-top:8px;color:#505863;min-height:14px}
.gdp-actions{display:flex;gap:6px;margin-top:8px}
`;

  const HTML = `
<div class="gdp-row">
  <span class="gdp-label">Glassdoor jobs</span>
  <span class="gdp-count"><b class="gdp-cur">0</b> / <span class="gdp-max">0</span></span>
</div>
<div class="gdp-track"><div class="gdp-fill"></div></div>
<div class="gdp-row" style="margin-top:8px">
  <label for="gdp-max-in">Max jobs</label>
  <input id="gdp-max-in" class="gdp-max-in" type="number" min="1" step="1">
</div>
<div class="gdp-actions">
  <button class="gdp-btn" data-act="start">Scrape</button>
  <button class="gdp-btn" data-act="stop" data-variant="ghost" hidden>Stop</button>
  <button class="gdp-btn" data-act="save" hidden>Download CSV</button>
</div>
<div class="gdp-note"></div>`;

  let ui = null;
  let running = false;
  let aborted = false;
  let results = [];

  function mount() {
    if (document.getElementById('gd-progress')) return;

    const style = document.createElement('style');
    style.id = 'gd-progress-css';
    style.textContent = CSS;
    document.head.appendChild(style);

    const root = document.createElement('div');
    root.id = 'gd-progress';
    root.className = 'gdp';
    root.innerHTML = HTML;
    document.body.appendChild(root);

    ui = {
      root,
      cur: root.querySelector('.gdp-cur'),
      max: root.querySelector('.gdp-max'),
      fill: root.querySelector('.gdp-fill'),
      input: root.querySelector('#gdp-max-in'),
      note: root.querySelector('.gdp-note'),
      start: root.querySelector('[data-act="start"]'),
      stop: root.querySelector('[data-act="stop"]'),
      save: root.querySelector('[data-act="save"]'),
    };

    ui.input.value = CFG.defaultMax;
    ui.max.textContent = CFG.defaultMax;
    ui.input.addEventListener('input', () => { ui.max.textContent = ui.input.value || 0; });
    ui.start.addEventListener('click', () => run(parseInt(ui.input.value, 10)));
    ui.stop.addEventListener('click', () => { aborted = true; note('Stopping…'); });
    ui.save.addEventListener('click', () => download(toCsv(results, COLS), CFG.fileName));
  }

  const note = (m) => { if (ui) ui.note.textContent = m; };

  function progress(cur, max) {
    if (!ui) return;
    ui.cur.textContent = cur;
    ui.max.textContent = max;
    ui.fill.style.width = Math.min(100, (cur / (max || 1)) * 100) + '%';
  }

  /* ------------------------------------------------------------------ *
   * Main loop — one pass over the DOM per batch instead of one
   * extension round-trip per row.
   * ------------------------------------------------------------------ */
  async function run(max) {
    if (running) return;
    if (!Number.isFinite(max) || max < 1) return note('Enter a max first.');
    if (!getList()) return note('No jobs list on this page.');

    running = true;
    aborted = false;
    results = [];
    const seen = new Set();
    let retries = 0;

    ui.root.removeAttribute('data-done');
    ui.start.hidden = true;
    ui.save.hidden = true;
    ui.stop.hidden = false;
    progress(0, max);
    note('Reading cards…');

    while (results.length < max && !aborted) {
      dismissModals();

      const cards = getCards();

      for (let i = 0; i < cards.length && results.length < max; i++) {
        if (CFG.scrollEachCard) {
          cards[i].scrollIntoView({ block: 'center' });
          await sleep(30);
        }

        const job = extractJob(cards[i], results.length + 1);
        if (!job || (!job.jobId && !job.title)) continue;

        const key = job.jobId || `${job.title}|${job.companyName}|${job.location}`;
        if (CFG.dedupeByJobId && seen.has(key)) continue;
        seen.add(key);

        results.push(job);
        if (results.length % 10 === 0) progress(results.length, max);
      }

      progress(results.length, max);
      if (results.length >= max || aborted) break;

      // Need more cards.
      const before = cards.length;
      const btn = document.querySelector(LOAD_MORE);

      if (!btn || btn.disabled) {
        note(`No more results — stopped at ${results.length}.`);
        break;
      }

      note(`Loading more… (${results.length}/${max})`);
      window.scrollTo(0, document.body.scrollHeight);
      btn.click();

      const grew = await waitFor(() => getCards().length > before, CFG.loadMoreTimeout);
      if (!grew) {
        if (++retries > CFG.loadMoreRetries) {
          note(`Page stopped loading — kept ${results.length}.`);
          break;
        }
        dismissModals();
        await sleep(600);
      } else {
        retries = 0;
      }
    }

    progress(results.length, max);
    ui.root.setAttribute('data-done', '1');
    ui.stop.hidden = true;
    ui.start.hidden = false;
    ui.start.textContent = 'Scrape again';
    running = false;

    if (!results.length) return note('Nothing extracted — check the selectors.');

    ui.save.hidden = false;
    note(aborted ? `Stopped — ${results.length} jobs ready.` : `Done — ${results.length} jobs.`);
    download(toCsv(results, COLS), CFG.fileName);

    // Full objects (all 18 fields) for the console, in case you want more columns.
    console.log('[Glassdoor] extracted', results);
    window.__gdJobs = results;
  }

  /* ------------------------------------------------------------------ *
   * Boot — the search page is a SPA, so re-mount if the panel is lost
   * ------------------------------------------------------------------ */
  mount();
  setInterval(() => { if (!document.getElementById('gd-progress')) { ui = null; mount(); } }, 2000);
})();