// credential_vault.js — HTTP Basic Auth credential vault for pup sessions.
//
// Why this exists
// ───────────────
// When a pup tab navigates to a host that issues an HTTP Basic Auth challenge
// (nxp.componentsearchengine.com, mouser.componentsearchengine.com, several
// vendor design portals), Chrome shows a NATIVE auth dialog. That dialog
// lives outside the page's DOM and CANNOT be driven via browser_eval / DOM
// manipulation. Without this vault, the user has to manually type the same
// email + password every single time. Users have called this "heinous."
//
// Puppeteer exposes `page.authenticate({username, password})`, which
// intercepts basic-auth challenges BEFORE the dialog shows and supplies
// stored creds programmatically. This module owns the storage + lookup
// half of that flow.
//
// Storage
// ───────
// Two parts:
//   1. The PASSWORD lives in the OS keychain via `keytar` (Windows DPAPI /
//      macOS Keychain / Linux libsecret). Service name `adom-desktop-pup`.
//      Account name is the host pattern. The OS encrypts at rest using the
//      logged-in-user's session key, so even disk theft can't extract creds.
//   2. The INDEX (host pattern + username + timestamps, NEVER passwords)
//      lives at `plugins/puppeteer/credentials.index.json` so we can list
//      entries fast without poking keytar 50 times.
//
// Host matching
// ─────────────
// Patterns support a simple glob:
//   "*.componentsearchengine.com"  → matches any subdomain (suffix match)
//   "componentsearchengine.com"    → exact match only
//   "secure.example.com"           → exact match only
// Best-match wins (longest non-glob suffix beats shorter glob).
// For a navigation to https://nxp.componentsearchengine.com/preview.php,
// host = "nxp.componentsearchengine.com" — would match "*.componentsearchengine.com".
//
// What this module does NOT do
// ────────────────────────────
//   - Ship its own crypto. Never. OS keychain only.
//   - Echo passwords back via list/get APIs. Index returns username + host only.
//   - Persist anything to the wiki / cloud. All local, all per-machine.

const fs = require('fs');
const path = require('path');

const KEYTAR_SERVICE = 'adom-desktop-pup';
const INDEX_PATH = path.join(__dirname, 'credentials.index.json');

let keytar = null;
let keytarLoadError = null;
try {
  // keytar is a native module; load lazily so the bridge starts even if it's
  // missing. The first credential_set call will surface a clear error.
  keytar = require('keytar');
} catch (e) {
  keytarLoadError = e.message;
  console.log(`[credential_vault] keytar load failed (will be required for credential_set): ${e.message}`);
}

// ── Index helpers ──────────────────────────────────────────────────────

function readIndex() {
  try {
    const raw = fs.readFileSync(INDEX_PATH, 'utf8');
    const parsed = JSON.parse(raw);
    if (Array.isArray(parsed.entries)) return parsed.entries;
  } catch {}
  return [];
}

function writeIndex(entries) {
  const data = { version: 1, entries };
  fs.writeFileSync(INDEX_PATH, JSON.stringify(data, null, 2));
}

// ── Glob matching ──────────────────────────────────────────────────────

function hostMatchesPattern(host, pattern) {
  if (!host || !pattern) return false;
  const h = host.toLowerCase();
  const p = pattern.toLowerCase();
  if (p === h) return true;
  if (p.startsWith('*.')) {
    const suffix = p.slice(1); // ".componentsearchengine.com"
    if (h === suffix.slice(1)) return true;     // bare apex is a valid match
    if (h.endsWith(suffix)) return true;
  }
  return false;
}

// "Best match" = longest non-glob suffix wins. Exact match beats glob match
// of the same length. So "nxp.componentsearchengine.com" entry would win
// over "*.componentsearchengine.com" entry for that exact host.
function scorePattern(pattern, host) {
  if (!hostMatchesPattern(host, pattern)) return -1;
  const exact = !pattern.startsWith('*.');
  return (exact ? 1000 : 0) + pattern.length;
}

// ── Public API ─────────────────────────────────────────────────────────

async function setCredential({ host, username, password }) {
  if (!host || !username || !password) {
    throw new Error('host, username, and password are all required');
  }
  if (!keytar) {
    throw new Error(
      `keytar is not installed in plugins/puppeteer/. Install: cd plugins/puppeteer && npm install keytar. Original load error: ${keytarLoadError}`
    );
  }
  await keytar.setPassword(KEYTAR_SERVICE, host, password);
  const entries = readIndex();
  const idx = entries.findIndex(e => e.host === host);
  const now = new Date().toISOString();
  if (idx >= 0) {
    entries[idx] = { ...entries[idx], username, updatedAt: now };
  } else {
    entries.push({ host, username, addedAt: now, updatedAt: now });
  }
  writeIndex(entries);
  return { ok: true, host, username };
}

// The keytar account key that holds THIS entry's password. Imported (identity-tagged)
// entries use a per-entry composite so BOTH sides of a host+user conflict from different
// source accounts can coexist; legacy single-host entries fall back to the host key.
function keytarAccountFor(entry) { return entry.keytarAccount || entry.host; }

// v1.9.325: a host can now have MULTIPLE tagged credentials (personal + work + media, from
// the browser import). Pick the best: best host-pattern match first, then the preferred
// identity if the caller gave one (opts.preferAccount — e.g. the pup window's owner), then
// most-recently-used. Returns the chosen entry's sourceAccount so the caller knows the persona.
async function getCredentialForUrl(url, opts = {}) {
  if (!keytar) return null;
  let host;
  try {
    host = new URL(url).hostname;
  } catch {
    return null;
  }
  const entries = readIndex();
  const matches = [];
  for (const e of entries) {
    const s = scorePattern(e.host, host);
    if (s >= 0) matches.push({ e, s });
  }
  if (!matches.length) return null;
  const prefer = opts.preferAccount ? String(opts.preferAccount).toLowerCase() : null;
  matches.sort((a, b) => {
    if (b.s !== a.s) return b.s - a.s;                                  // best host pattern
    if (prefer) {
      const ap = (a.e.sourceAccount || '').toLowerCase() === prefer ? 1 : 0;
      const bp = (b.e.sourceAccount || '').toLowerCase() === prefer ? 1 : 0;
      if (bp !== ap) return bp - ap;                                    // preferred identity
    }
    const at = a.e.lastUsed || a.e.updatedAt || '';
    const bt = b.e.lastUsed || b.e.updatedAt || '';
    return String(bt).localeCompare(String(at));                       // most-recently-used
  });
  const best = matches[0].e;
  const password = await keytar.getPassword(KEYTAR_SERVICE, keytarAccountFor(best));
  if (!password) return null;
  return { host: best.host, username: best.username, password, sourceAccount: best.sourceAccount || null };
}

// ── Bulk import from the user's REAL browsers (the adom-you seed) ────────────
// Rows come from AD's desktop_decrypt_browser_credentials (the SYSTEM-elevated ABE decrypt,
// AD >= 1.9.220). Each row: {origin, username, password, sourceBrowser, sourceProfile,
// sourceAccount, lastUsed}. DECISION (locked with John): keep BOTH sides of a (host,username)
// conflict when they come from DIFFERENT source accounts, so the agent can act as the RIGHT
// persona per site; same (host,username,sourceAccount) is an update (newest wins). Passwords go
// to keytar under a per-entry composite account; the index NEVER holds a password. The caller
// hands us plaintext ONCE and must drop it after — we return COUNTS ONLY, never plaintext.
function hostFromOrigin(origin) {
  if (!origin) return null;
  try { return new URL(origin).hostname || String(origin); } catch { return String(origin); }
}
function importAccountKey(host, username, sourceAccount) {
  return `imp${host}${username || ''}${sourceAccount || ''}`;
}
async function mergeImportedCredentials(rows) {
  if (!keytar) throw new Error(`keytar is not installed in the bridge dir. Original load error: ${keytarLoadError}`);
  if (!Array.isArray(rows)) return { added: 0, updated: 0, skipped: 0, total: 0 };
  const entries = readIndex();
  const now = new Date().toISOString();
  let added = 0, updated = 0, skipped = 0;
  for (const r of rows) {
    try {
      const host = hostFromOrigin(r && (r.origin || r.host));
      const username = (r && r.username) || '';
      const password = r && r.password;
      if (!host || !password) { skipped++; continue; }
      const acct = importAccountKey(host, username, r.sourceAccount);
      await keytar.setPassword(KEYTAR_SERVICE, acct, password);
      const idx = entries.findIndex(e => keytarAccountFor(e) === acct);
      const meta = {
        host, username, keytarAccount: acct,
        sourceAccount: r.sourceAccount || null,
        sourceBrowser: r.sourceBrowser || null,
        sourceProfile: r.sourceProfile || null,
        imported: true,
        lastUsed: r.lastUsed || null,
      };
      if (idx >= 0) { entries[idx] = { ...entries[idx], ...meta, updatedAt: now }; updated++; }
      else { entries.push({ ...meta, addedAt: now, updatedAt: now }); added++; }
    } catch (e) { skipped++; }
  }
  writeIndex(entries);
  return { added, updated, skipped, total: rows.length };
}

function listCredentials() {
  // Index already excludes passwords by construction. Now also surfaces the identity tags
  // from an import so the dashboard can show WHICH persona each saved login belongs to.
  return readIndex().map(e => ({
    host: e.host,
    username: e.username,
    sourceAccount: e.sourceAccount || null,
    sourceBrowser: e.sourceBrowser || null,
    sourceProfile: e.sourceProfile || null,
    imported: !!e.imported,
    addedAt: e.addedAt,
    updatedAt: e.updatedAt,
  }));
}

async function deleteCredential(host) {
  if (!host) throw new Error('host required');
  const entries = readIndex();
  const toRemove = entries.filter(e => e.host === host);
  let removed = false;
  if (keytar) {
    try {
      if (toRemove.length) {
        for (const e of toRemove) {
          const r = await keytar.deletePassword(KEYTAR_SERVICE, keytarAccountFor(e));
          removed = removed || r;
        }
      } else {
        removed = await keytar.deletePassword(KEYTAR_SERVICE, host);   // legacy host-keyed
      }
    } catch (e) {
      console.log(`[credential_vault] keytar deletePassword error: ${e.message}`);
    }
  }
  const next = entries.filter(e => e.host !== host);
  const removedFromIndex = next.length !== entries.length;
  if (removedFromIndex) writeIndex(next);
  return { ok: true, host, removedFromKeychain: removed, removedFromIndex };
}

// Apply stored creds to a page given the target URL it's about to load.
// Idempotent: if no match, no-op. Returns the matched host pattern (or null).
async function applyCredentialsToPage(page, url) {
  const creds = await getCredentialForUrl(url);
  if (!creds) return null;
  try {
    await page.authenticate({ username: creds.username, password: creds.password });
    return creds.host;
  } catch (e) {
    console.log(`[credential_vault] page.authenticate failed for ${creds.host}: ${e.message}`);
    return null;
  }
}

// ── HTML login FORM autofill (v1.9.124) ─────────────────────────────────
// applyCredentialsToPage above handles the NATIVE Basic-Auth dialog. This
// handles the other, more common case John asked about: an HTML <form> login,
// where Chrome's own password manager would pop the floaty autofill dropdown —
// a NATIVE popup pup can't click without foregrounding the window. Because pup
// owns the creds, we skip Chrome's UI entirely: find the fields and fill them
// over CDP, in the background, no dropdown, no focus steal. React/Vue-safe
// (native value setter + bubbled input/change events, the standard technique).
//
// Runs entirely in the page; returns { ok, filledUsername, filledPassword,
// submitted, usernameValue } and NEVER returns the password.
async function fillLoginForm(page, { username, password, submit = false } = {}) {
  if (!password) return { ok: false, reason: 'no_password_supplied' };
  const result = await page.evaluate((USERNAME, PASSWORD, SUBMIT) => {
    const isVisible = (el) => !!el && !el.disabled && el.offsetParent !== null && el.getClientRects().length > 0;
    const pwd = [...document.querySelectorAll('input[type="password"]')].find(isVisible);
    if (!pwd) return { ok: false, reason: 'no_password_field' };
    // Username: prefer autocomplete=username, then email, else the last visible
    // text-ish input that appears BEFORE the password field in DOM order.
    const allInputs = [...document.querySelectorAll('input')];
    const pi = allInputs.indexOf(pwd);
    let user = [...document.querySelectorAll('input[autocomplete="username"], input[type="email"]')].find(isVisible);
    if (!user) {
      const cands = [...document.querySelectorAll('input[type="text"], input[type="tel"], input:not([type])')].filter(isVisible);
      user = [...cands].reverse().find((c) => allInputs.indexOf(c) < pi) || cands[0] || null;
    }
    const setVal = (el, val) => {
      const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
      const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
      try { el.focus(); } catch (e) {}
      setter.call(el, val);
      el.dispatchEvent(new Event('input', { bubbles: true }));
      el.dispatchEvent(new Event('change', { bubbles: true }));
    };
    if (user && USERNAME) setVal(user, USERNAME);
    setVal(pwd, PASSWORD);
    let submitted = false;
    if (SUBMIT) {
      const form = pwd.form;
      const btn = form && [...form.querySelectorAll('button[type="submit"], input[type="submit"], button')].find(isVisible);
      if (btn) { btn.click(); submitted = true; }
      else if (form) { (form.requestSubmit ? form.requestSubmit() : form.submit()); submitted = true; }
    }
    return { ok: true, filledUsername: !!(user && USERNAME), filledPassword: true, submitted, usernameValue: user ? user.value : null };
  }, username || '', password, !!submit);
  return result;
}

// Pull a stored cred for the page's URL and fill its login form in the
// background. No-op (returns null) if no vault match or no login form.
async function autofillFormFromVault(page, url, { submit = false } = {}) {
  const creds = await getCredentialForUrl(url);
  if (!creds) return null;
  try {
    const r = await fillLoginForm(page, { username: creds.username, password: creds.password, submit });
    if (r && r.ok) return { host: creds.host, username: creds.username, submitted: !!r.submitted };
    return null;
  } catch (e) {
    console.log(`[credential_vault] autofillFormFromVault failed for ${creds.host}: ${e.message}`);
    return null;
  }
}

module.exports = {
  setCredential,
  mergeImportedCredentials,
  getCredentialForUrl,
  listCredentials,
  deleteCredential,
  applyCredentialsToPage,
  fillLoginForm,
  autofillFormFromVault,
  // Exposed for tests + diagnostics
  hostMatchesPattern,
  scorePattern,
};