// getbearer.js — Browserless /function (Puppeteer)
// output = "png" | "json" (default: "json")
// - "png": nach Schulwahl + Loginversuch Screenshot als image/png
// - "json": bearer/diag/seenAuth + screenshot_b64

export default async function ({ page, context }) {
    const {
      url = "https://mese.webuntis.com/WebUntis/?school=LMG+Crailsheim",
      username = "LehmanLuc",
      password = "",
      school   = "LMG Crailsheim",     // ggf. "Lise-Meitner-Gymnasium"
      debug = true,
      timeoutMs = 10000,
      maxTimeout = 3000,
      screenshotFullPage = false,
      output = "json"                  // "png" | "json"
    } = context || {};
  
    const sleep = (ms) => new Promise(r => setTimeout(r, ms));
    const diag = { steps: [], schoolHint: null, frameUrls: [], errors: [] };
    const seenAuth = [];
    let bearer = null;
  
    // ---------- Hardening ----------
    try { await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"); } catch {}
    try { await page.setViewport({ width: 1366, height: 900, deviceScaleFactor: 1 }); } catch {}
    try { page.setDefaultTimeout(Math.max(maxTimeout, timeoutMs)); } catch {}
  
    // ---------- Network taps ----------
    page.on("request", (req) => {
      try {
        const a = req.headers()["authorization"];
        if (!bearer && a?.startsWith("Bearer ")) {
          bearer = a.slice(7);
          seenAuth.push({ t: "req", url: req.url() });
        }
      } catch {}
    });
    page.on("response", async (res) => {
      try {
        if (bearer) return;
        const a = res.request().headers()["authorization"];
        if (a?.startsWith("Bearer ")) {
          bearer = a.slice(7);
          seenAuth.push({ t: "res-reqhdr", url: res.url() });
          return;
        }
        const ct = (res.headers()["content-type"] || "").toLowerCase();
        if (ct.includes("application/json")) {
          const data = await res.json().catch(() => null);
          const tok = data?.access_token || data?.token || data?.idToken || data?.id_token;
          if (tok) { bearer = tok; seenAuth.push({ t: "res-json", url: res.url() }); }
        }
      } catch {}
    });
  
    // ---------- Helpers ----------
    async function queryDeepHandle(frame, predicateFn) {
      const handle = await frame.evaluateHandle((predSrc) => {
        const pred = eval(predSrc);
        const visit = (root) => {
          for (const el of root.querySelectorAll("*")) {
            try { if (pred(el)) return el; } catch {}
            if (el.shadowRoot) { const f = visit(el.shadowRoot); if (f) return f; }
          }
          return null;
        };
        return visit(document);
      }, predicateFn.toString());
      return handle.asElement();
    }
  
    async function takePngBuffer() {
      try { return await page.screenshot({ type: "png", fullPage: screenshotFullPage }); }
      catch { return null; }
    }
  
    const typeInto = async (el, val) => {
      await el.focus().catch(() => {});
      await el.click({ clickCount: 3 }).catch(() => {});
      await el.type(val, { delay: 15 }).catch(() => {});
      try {
        await el.evaluate((e,v)=>{
          const last = e.value;
          e.value = v;
          e.dispatchEvent(new Event('input', {bubbles:true}));
          if (last !== v) e.dispatchEvent(new Event('change', {bubbles:true}));
          if (e.form) e.form.dispatchEvent(new Event('input', {bubbles:true}));
        }, val);
      } catch {}
    };
  
    // Aktion + Navigation gekoppelt ausführen (verhindert "execution context destroyed")
    async function withNav(page, action, waitMs = maxTimeout) {
      const navP  = page.waitForNavigation({ waitUntil: "domcontentloaded", timeout: waitMs }).catch(() => {});
      const hostP = page.waitForFunction(() => {
        try {
          const h = location.hostname;
          return /^[^.]+\.(webuntis|untis)\.com$/i.test(h) && !/^(www\.)?webuntis\.com$/i.test(h);
        } catch { return false; }
      }, { timeout: waitMs }).catch(() => {});
      const actP = (async () => { try { await action(); } catch {} })();
      await Promise.race([navP, hostP]);
      await Promise.allSettled([navP, hostP, actP]);
    }
  
    async function waitForRedirectOrPwd(page, timeout = 20000) {
      const start = Date.now();
      while (Date.now() - start < timeout) {
        try {
          const host = new URL(page.url()).hostname;
          if (/^[^.]+\.(webuntis|untis)\.com$/i.test(host) && !/^(www\.)?webuntis\.com$/i.test(host)) return true;
        } catch {}
        for (const fr of page.frames()) {
          const pwd = await queryDeepHandle(fr, (el) => el.tagName === "INPUT" && (el.type?.toLowerCase?.() === "password"));
          if (pwd) return true;
        }
        await sleep(300);
      }
      return false;
    }
  
    // Robuste Schulwahl
    async function pickSchool(page, schoolHint, diag) {
      if (!schoolHint) { diag.steps.push("no-school-hint"); return false; }
  
      // 1) Suchfeld finden (shadowDOM tauglich)
      let input = null;
      for (const fr of page.frames()) {
        const h = await queryDeepHandle(fr, (el) => {
          if (el.tagName !== "INPUT") return false;
          const type = (el.getAttribute("type") || "").toLowerCase();
          const role = (el.getAttribute("role") || "").toLowerCase();
          const ph   = (el.getAttribute("placeholder") || "").toLowerCase();
          const ar   = (el.getAttribute("aria-label") || "").toLowerCase();
          return type === "search" || role === "combobox" || /search|schule|school/.test(ph+ar);
        });
        if (h) { input = h; break; }
      }
      if (!input) { diag.steps.push("school-input-not-found"); return false; }
  
      // 2) Tippen
      diag.steps.push("school-input-found");
      try { await input.focus(); } catch {}
      try { await input.click({ clickCount: 3 }); } catch {}
      await page.keyboard.type(String(schoolHint), { delay: 25 });
      await sleep(700);
  
      // 3) Erste sichtbare Suggestion unter dem Input per BBox ermitteln
      async function getBox(elHandle) { try { return await elHandle.boundingBox(); } catch { return null; } }
      async function isVisible(el) {
        try {
          return await el.evaluate((e) => {
            const s = window.getComputedStyle(e);
            const r = e.getBoundingClientRect();
            return s && s.display !== "none" && s.visibility !== "hidden" && r.width > 1 && r.height > 1;
          });
        } catch { return false; }
      }
  
      const inputBox = await getBox(input);
      let best = null; // { frame, handle, box, text, key }
      const until = Date.now() + 6000;
  
      while (!best && Date.now() < until) {
        for (const fr of page.frames()) {
          let handles = [];
          for (const sel of ['[role="option"]', '[role="listitem"]', 'li', 'div[role="option"]']) {
            try { const arr = await fr.$$(sel); if (arr?.length) handles.push(...arr); } catch {}
          }
          for (const h of handles) {
            try {
              if (!(await isVisible(h))) { await h.dispose().catch(()=>{}); continue; }
              const box = await getBox(h);
              if (!box || !inputBox) { await h.dispose().catch(()=>{}); continue; }
              if (box.y < inputBox.y + inputBox.height - 2) { await h.dispose().catch(()=>{}); continue; }
              const dist = Math.abs(box.y - (inputBox.y + inputBox.height));
              const text = (await h.evaluate(e => (e.textContent || "").trim())).toLowerCase();
              const hint = String(schoolHint).toLowerCase();
              const aliases = ["lise", "meitner", "lise-meitner", "gymnasium", "lmg", "crailsheim"];
              const score = (text.includes(hint) || aliases.some(a => text.includes(a))) ? 0 : 1;
              const key = score * 100000 + dist;
              if (!best || key < best.key) best = { frame: fr, handle: h, box, text, key };
              else await h.dispose().catch(()=>{});
            } catch { try { await h.dispose(); } catch {} }
          }
        }
        if (!best) await sleep(120);
      }
  
      let clicked = false;
      if (best?.handle) {
        try {
          const { x, y, width, height } = best.box;
          await withNav(page, async () => {
            await page.mouse.move(Math.round(x + width / 2), Math.round(y + height / 2));
            await page.mouse.click(Math.round(x + width / 2), Math.round(y + height / 2));
          }, maxTimeout);
          clicked = true;
          diag.steps.push("school-suggestion-clicked-bbox:" + (best.text || ""));
        } catch (e) {
          diag.errors.push("bbox-click-failed:" + (e?.message || e));
          try {
            await withNav(page, async () => { await best.handle.click(); }, maxTimeout);
            clicked = true;
            diag.steps.push("school-suggestion-clicked-handle");
          } catch {}
        } finally {
          try { await best.handle.dispose(); } catch {}
        }
      }
  
      if (!clicked) {
        // Sicherstellen, dass das Feld Fokus hat; dann ArrowDown+Enter + Nav abwarten
        try { await input.focus(); } catch {}
        await withNav(page, async () => {
          await page.keyboard.press("ArrowDown");
          await sleep(120);
          await page.keyboard.press("Enter");
        }, maxTimeout);
        diag.steps.push("school-enter-fallback");
      }
  
      await sleep(500); // nach Navigation alte Handles verwerfen
      return await waitForRedirectOrPwd(page, 20000);
    }
  
    // ---- Login-Felder finden & ausfüllen (robust) ----
    // ---- Login-Felder finden & ausfüllen (robust, ohne :has-text) ----
  async function findLoginElements() {
    // 1) direkte, einfache Selektoren
    const userSel = [
      'input[placeholder*="Benutzer" i]',
      'input[aria-label*="Benutzer" i]',
      'input[name*="user" i]',
      'input[id*="user" i]',
      'input[type="email"]',
      'input[type="text"]'
    ].join(',');
  
    const passSel = [
      'input[placeholder*="Passwort" i]',
      'input[aria-label*="Passwort" i]',
      'input[type="password"]'
    ].join(',');
  
    let u = await page.$(userSel);
    let p = await page.$(passSel);
  
    // 2) Shadow-DOM-Fallback
    if (!u || !p) {
      for (const fr of page.frames()) {
        if (!u) u = await queryDeepHandle(fr, (el) => {
          if (el.tagName !== "INPUT") return false;
          const t = (el.getAttribute("type")||"").toLowerCase();
          const txt = ((el.getAttribute("placeholder")||"") + " " + (el.getAttribute("aria-label")||"")).toLowerCase();
          return (t==="text"||t==="email"||t==="") && /user|benutzer|name/.test(txt);
        });
        if (!p) p = await queryDeepHandle(fr, (el) => el.tagName==="INPUT" && (el.type||"").toLowerCase()==="password");
        if (u && p) break;
      }
    }
  
    // 3) Submit-Button: zuerst type=submit, sonst per Textinhalt
    let b = await page.$('button[type="submit"], input[type="submit"]');
  
    if (!b) {
      // scannt sichtbare Buttons und matcht Textinhalt
      const btnByText = async (root) => {
        const LABELS = ["login","anmelden","einloggen","weiter","sign in","absenden"];
        const nodes = root.querySelectorAll('button, input[type="submit"]');
        for (const n of nodes) {
          let txt = (n.innerText || n.textContent || "").toLowerCase().trim();
          if (!txt && n.getAttribute) txt = (n.getAttribute("value")||"").toLowerCase().trim();
          if (!txt) continue;
          if (LABELS.some(l=>txt.includes(l))) {
            const cs = getComputedStyle(n);
            if (cs.display!=="none" && cs.visibility!=="hidden" && !n.disabled) return n;
          }
        }
        return null;
      };
  
      // zuerst im Haupt-DOM
      b = await page.evaluateHandle(btnByText, document).then(h=>h.asElement()).catch(()=>null);
  
      // Shadow-DOM-Fallback
      if (!b) {
        for (const fr of page.frames()) {
          const h = await fr.evaluateHandle(() => {
            const visit = (root) => {
              const el = (function(root){
                const LABELS = ["login","anmelden","einloggen","weiter","sign in","absenden"];
                const nodes = root.querySelectorAll('button, input[type="submit"]');
                for (const n of nodes) {
                  let txt = (n.innerText || n.textContent || "").toLowerCase().trim();
                  if (!txt && n.getAttribute) txt = (n.getAttribute("value")||"").toLowerCase().trim();
                  if (!txt) continue;
                  if (LABELS.some(l=>txt.includes(l))) {
                    const cs = getComputedStyle(n);
                    if (cs.display!=="none" && cs.visibility!=="hidden" && !n.disabled) return n;
                  }
                }
                return null;
              })(root);
              if (el) return el;
              for (const e of root.querySelectorAll("*")) {
                if (e.shadowRoot) {
                  const f = visit(e.shadowRoot);
                  if (f) return f;
                }
              }
              return null;
            };
            return visit(document);
          }).catch(()=>null);
          if (h) { b = h.asElement(); break; }
        }
      }
  
      // 4) Wenn noch kein Button: im selben <form> wie Passwortfeld
      if (!b && p) {
        const form = await p.evaluateHandle(e => e.closest('form'));
        if (form) {
          const hb = await form.asElement().$('button, input[type="submit"]');
          if (hb) b = hb;
          await form.dispose().catch(()=>{});
        }
      }
    }
  
    return { u, p, b };
  }
  
  
    // ---------- Main flow ----------
    try {
      // Load (Blank-Heuristik)
      await page.goto(url, { waitUntil: "domcontentloaded", timeout: timeoutMs }).catch(() => {});
      await sleep(700);
      try {
        const isBlank = await page.evaluate(() => document.body && document.body.children.length < 2);
        if (isBlank) { diag.steps.push("blank-reload"); await page.reload({ waitUntil: "domcontentloaded" }).catch(()=>{}); await sleep(500); }
      } catch {}
  
      // Schul-Hint bestimmen
      let schoolHint = school || null;
      try {
        const u = new URL(url);
        const m = u.hostname.match(/^([^.]+)\.webuntis\.com$/i);
        if (!schoolHint && m) schoolHint = m[1];
      } catch {}
      diag.schoolHint = schoolHint || null;
  
      // Consent (best effort)
      for (const sel of [
        'button[aria-label*="Akzept"]','button[title*="Akzept"]',
        'button[aria-label*="Accept" i]','button[title*="Accept" i]'
      ]) { try { await page.click(sel, { timeout: 500 }); diag.steps.push("clicked-consent"); break; } catch {} }
  
      // Passwortfeld schon sichtbar?
      let hasPwd = false;
      for (const fr of page.frames()) {
        const pwd = await queryDeepHandle(fr, (el) => el.tagName === "INPUT" && (el.type?.toLowerCase?.() === "password"));
        if (pwd) { hasPwd = true; break; }
      }
      if (!hasPwd) {
        const ok = await pickSchool(page, schoolHint, diag);
        if (!ok) diag.steps.push("school-pick-maybe-failed");
        await sleep(600);
      }
  
      // Login-Felder und Submit
      let { u: userEl, p: passEl, b: btnEl } = await findLoginElements();
      if (!userEl || !passEl) {
        diag.steps.push("login-fields-missing");
      } else {
        await typeInto(userEl, username);
        await typeInto(passEl, password);
  
        const submitAction = async () => {
          if (btnEl) {
            try { await btnEl.click(); }
            catch { try { await passEl.press("Enter"); } catch {} }
          } else {
            try { await passEl.press("Enter"); } catch {}
          }
        };
  
        const navP = page.waitForNavigation({ waitUntil: "networkidle0", timeout: maxTimeout }).catch(()=>{});
        const bearerP = new Promise(resolve=>{
          const t = setTimeout(()=>resolve(false), maxTimeout);
          const off = page.on("request", req=>{
            const a = req.headers()?.authorization||"";
            if (a.startsWith("Bearer ")) { clearTimeout(t); resolve(true); }
          });
        });
        const errBannerP = page.waitForSelector('[role="alert"], .error, .un-input-group__error', { timeout: 12000 })
                              .then(()=>true).catch(()=>false);
  
        await submitAction();
        const results = await Promise.allSettled([navP, bearerP, errBannerP]);
        const hadErr = results[2]?.value === true;
        if (hadErr) diag.steps.push("login-error-banner-seen");
      }
  
      // Storage-Fallback
      if (!bearer) {
        try {
          for (const fr of page.frames()) {
            const tok = await fr.evaluate(() => {
              const scan = (s) => {
                for (let i=0;i<s.length;i++){
                  const v = s.getItem(s.key(i));
                  if (!v) continue;
                  try {
                    const j = JSON.parse(v);
                    const t = j?.access_token || j?.token || j?.idToken || j?.id_token;
                    if (t) return t;
                  } catch {}
                  if (/^Bearer\s+/.test(v)) return v.replace(/^Bearer\s+/, "");
                  if (/eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*\./.test(v)) return v;
                }
                return null;
              };
              return scan(localStorage) || scan(sessionStorage);
            }).catch(()=>null);
            if (tok) { bearer = tok; diag.steps.push("token-from-storage"); break; }
          }
        } catch {}
      }
  
      // ---------- OUTPUT ----------
      if (output === "png") {
        const buf = await takePngBuffer();
        return { data: buf || Buffer.from([]), type: "image/png" };
      }
  
      
      
      
      const buf = await takePngBuffer();
      const screenshot_b64 = buf ? "data:image/png;base64," + buf.toString("base64") : null;
      
      const json = debug
        ? { bearer: bearer || null, seenAuth, diag, screenshot_b64 }
        : { bearer: bearer || null, screenshot_b64 };
      return { data: json, type: "application/json" };
  
    } catch (err) {
      diag.errors.push(err?.message || String(err));
      const buf = await takePngBuffer();
      if (output === "png") return { data: buf || Buffer.from([]), type: "image/png" };
      const screenshot_b64 = buf ? "data:image/png;base64," + buf.toString("base64") : null;
      return { data: { error: err?.message || String(err), diag, seenAuth, bearer: null, screenshot_b64 }, type: "application/json" };
    }
  }
  