// Popup: token-driven login + dialer settings.
//
// One account per extension install. The auth token is the user's identity —
// pasting it triggers a /api/me lookup that auto-fills email / name / balance
// / activation status / agent cell / VM-drop URL. Edits to agentCell and
// vmDropUrl push back to the user's record on the worker so they follow the
// account across machines.
//
// Storage layout (chrome.storage.local) — content.js reads from here, so
// local is a server-synced cache:
//   pd_account     : { authToken, email, name, agentCell, vmDropUrl }
//   pd_worker_base : "https://app.goalpowerdialer.com"
//   pd_skin        : "phone" | "goal"

const DEFAULTS = {
  // v1.29.50 — Added holdMusicPreset: a slug picked from the curated
  // server-side list. Resolved to a URL on the worker side so we never
  // accept arbitrary URLs from the client.
  pd_account:     { authToken: "", email: "", name: "", agentCell: "", vmDropUrl: "", holdMusicPreset: "off" },
  pd_worker_base: "https://app.goalpowerdialer.com",
  pd_skin:        "phone",
  pd_dial_mode:   "cell",    // "cell" | "mobile_web" | "desktop" — three park-source modes
                             // (legacy values "phone" → "cell" migrated on load)
  pd_mic_granted: false,     // sticky flag set after first successful getUserMedia
};

// Backwards-compat: pre-1.6 versions stored config in pd_profiles.{me|partner}.
// On first load we read those legacy keys and migrate the active slot into
// pd_account so users don't have to re-paste their token after upgrading.
const LEGACY_KEYS = ["pd_active_profile", "pd_profiles"];

const $ = (id) => document.getElementById(id);

let account     = { ...DEFAULTS.pd_account };
let workerBase  = DEFAULTS.pd_worker_base;
let skin        = DEFAULTS.pd_skin;
let dialMode    = DEFAULTS.pd_dial_mode;
let micGranted  = DEFAULTS.pd_mic_granted;
// v1.29.50 — Hold music picker state (declared up top so load() can
// reference them without hitting a temporal-dead-zone error).
let hmPresets     = [];
let hmCurrentSlug = "off";   // displayed selection (may be unsaved preview)
let hmSavedSlug   = "off";   // last committed (server-persisted) slug
let hmAuditing    = false;

async function load() {
  const data = await chrome.storage.local.get([
    ...Object.keys(DEFAULTS), ...LEGACY_KEYS,
  ]);

  // Migrate from legacy dual-profile schema if present and the new key isn't.
  if (!data.pd_account && data.pd_profiles) {
    const slot = data.pd_active_profile || "me";
    const legacy = data.pd_profiles[slot] || {};
    account = {
      authToken: legacy.authToken || "",
      email:     "",                       // populated by /api/me
      name:      legacy.name      || "",
      agentCell: legacy.agentCell || "",
      vmDropUrl: legacy.vmDropUrl || "",
    };
    // Persist new shape, but keep the legacy keys around in case we need to
    // roll back. They'll get cleaned up on next save below.
    await chrome.storage.local.set({ pd_account: account });
  } else {
    account = { ...DEFAULTS.pd_account, ...(data.pd_account || {}) };
  }

  workerBase = data.pd_worker_base || DEFAULTS.pd_worker_base;
  // Migrate legacy skin names: "medium" → "phone", "soccer-medium" → "goal"
  {
    const raw = data.pd_skin;
    if (raw === "medium") skin = "phone";
    else if (raw === "soccer-medium") skin = "goal";
    else skin = (raw === "goal" || raw === "phone") ? raw : "phone";
  }
  // Park-source mode — three options now: cell | mobile_web | desktop.
  // Migrate legacy values: "phone" → "cell" (back-compat with pre-1.27).
  const rawMode = data.pd_dial_mode;
  if (rawMode === "cell" || rawMode === "mobile_web" || rawMode === "desktop") {
    dialMode = rawMode;
  } else if (rawMode === "phone") {
    dialMode = "cell";   // legacy migration
  } else {
    dialMode = "cell";   // default
  }
  micGranted = !!data.pd_mic_granted;

  $("authToken").value = account.authToken || "";
  $("agentCell").value = account.agentCell || "";
  $("vmDropUrl").value = account.vmDropUrl || "";
  // Hold music picker hydrates from the cached preset slug; the preset
  // list itself is fetched async by initHoldMusicPicker() below.
  hmCurrentSlug = account.holdMusicPreset || "off";
  hmSavedSlug   = hmCurrentSlug;
  renderHoldMusicDisplay();
  $("workerBase").value = workerBase;
  const skinRadio = document.getElementById("skin-" + skin);
  if (skinRadio) skinRadio.checked = true;
  // Map dialMode → radio ID (mode-cell / mode-mobile / mode-desktop)
  const radioId = dialMode === "mobile_web" ? "mode-mobile" : "mode-" + dialMode;
  const modeRadio = document.getElementById(radioId);
  if (modeRadio) modeRadio.checked = true;
  applyAuthView();
  applyDialModeView();
  // If we boot in desktop mode and we already have a token + mic grant,
  // ask background for the current desktop status so the pip is fresh.
  refreshDesktopStatus();
}

// Toggle between signed-out (email + code) and signed-in (account card)
// views based on whether there's a token in storage.
function applyAuthView() {
  const signedIn = !!(account.authToken || "").trim();
  $("signin-pane").style.display    = signedIn ? "none"  : "block";
  $("signedin-pane").style.display  = signedIn ? "block" : "none";
}

function flashSaved() {
  const el = $("saved");
  el.classList.add("show");
  clearTimeout(flashSaved._t);
  flashSaved._t = setTimeout(() => el.classList.remove("show"), 1100);
}

async function persist() {
  await chrome.storage.local.set({
    pd_account:     account,
    pd_worker_base: workerBase,
    pd_skin:        skin,
    pd_dial_mode:   dialMode,
    pd_mic_granted: micGranted,
    // Mirror into the legacy schema so content.js (which still reads
    // pd_profiles + pd_active_profile) continues to work without changes.
    pd_active_profile: "me",
    pd_profiles: {
      me:      { ...account },
      partner: { authToken: "", name: "", agentCell: "", vmDropUrl: "" },
    },
  });
  flashSaved();
}

// ── Desktop mode UI ──────────────────────────────────────────────────
// Toggles the mic-grant + desktop-status panel under the radio row.
function applyDialModeView() {
  const pane = $("desktop-pane");
  if (!pane) return;
  pane.style.display = dialMode === "desktop" ? "block" : "none";
  const grantBtn = $("grant-mic");
  if (grantBtn) {
    grantBtn.textContent = micGranted ? "✓ Mic granted (re-grant)" : "Grant microphone access";
  }
}

// Persist the park-source choice on the user record so all surfaces
// (/mobile, /desktop, dialer panel) stay in sync. Fire-and-forget;
// next /api/me poll on any surface pulls the new value.
async function pushParkSourceToServer(source) {
  const token = (account.authToken || "").trim();
  if (!token) return;
  try {
    await fetch(workerUrl("/api/me/update"), {
      method:  "POST",
      headers: { "content-type": "application/json" },
      body:    JSON.stringify({ token, activeParkSource: source }),
    });
  } catch (_e) { /* silent — best effort */ }
}

function setDeskStatus(label, kind) {
  const el = $("deskStatus");
  if (!el) return;
  el.classList.remove("ok", "err");
  if (kind === "ok")  el.classList.add("ok");
  if (kind === "err") el.classList.add("err");
  const txt = el.querySelector("span:last-child");
  if (txt) txt.textContent = label;
}

// Asks background for the latest pd_off_status snapshot.
function refreshDesktopStatus() {
  if (dialMode !== "desktop") {
    setDeskStatus("Desktop mode off", null);
    return;
  }
  try {
    chrome.runtime.sendMessage({ type: "pd_desktop_status" }, (resp) => {
      void chrome.runtime.lastError;
      const s = resp?.status?.state || "absent";
      paintDeskStatus(s, resp?.status?.error);
    });
  } catch {
    setDeskStatus("Background unavailable", "err");
  }
}

function paintDeskStatus(state, error) {
  switch (state) {
    case "absent":     return setDeskStatus("Off — flip switch on to enable", null);
    case "idle":       return setDeskStatus("Loading…", null);
    case "connecting": return setDeskStatus("Connecting…", null);
    case "ready":      return setDeskStatus("✓ Ready — calls will land in your headset", "ok");
    case "ringing":    return setDeskStatus("📞 Incoming call", "ok");
    case "active":     return setDeskStatus("📞 On call", "ok");
    case "error":      return setDeskStatus("Error: " + (error || "unknown"), "err");
    default:           return setDeskStatus("Status: " + state, null);
  }
}

// Listen for background's broadcasts so the pip updates live without
// the popup needing to poll. Cheap because the popup only exists when
// the user has it open.
try {
  chrome.runtime.onMessage.addListener((msg) => {
    if (!msg || msg.type !== "pd_off_status") return;
    paintDeskStatus(msg.state, msg.error);
  });
} catch {}

// Tells background to spin up (or tear down) the offscreen WebRTC sandbox.
function pushDesktopMode() {
  try {
    if (dialMode === "desktop") {
      chrome.runtime.sendMessage({
        type:       "pd_desktop_enable",
        token:      account.authToken,
        workerBase: workerBase,
      }, () => { void chrome.runtime.lastError; });
    } else {
      chrome.runtime.sendMessage({ type: "pd_desktop_disable" }, () => {
        void chrome.runtime.lastError;
      });
    }
  } catch {}
}

// ── Token-driven login ────────────────────────────────────────────────
let activeAccount = null;          // last successful /api/me result
let lookupTimer   = null;

function setLoginCard(state, fields) {
  const card = $("login-card");
  if (!card) return;
  card.classList.remove("lc-ok", "lc-bad", "lc-empty");
  if (state) card.classList.add(state);
  for (const [k, v] of Object.entries(fields || {})) {
    const el = $("lc-" + k);
    if (el) el.textContent = (v == null ? "—" : v);
  }
}

function workerUrl(path) {
  return (workerBase || DEFAULTS.pd_worker_base).replace(/\/+$/, "") + path;
}

async function lookupAccount(token) {
  const t = (token || "").trim();
  if (!t) {
    setLoginCard("lc-empty", { email: "Paste a token to sign in", name: "—", balance: "—", activation: "—" });
    activeAccount = null;
    return;
  }
  setLoginCard(null, { email: "Looking up…", name: "—", balance: "—", activation: "—" });
  try {
    const res = await fetch(workerUrl("/api/me?token=" + encodeURIComponent(t)), { cache: "no-store" });
    const j = await res.json().catch(() => ({}));
    if (!res.ok || !j.ok) {
      setLoginCard("lc-bad", {
        email:      "Invalid token",
        name:       "—",
        balance:    "—",
        activation: (j.error || ("HTTP " + res.status)).slice(0, 40),
      });
      activeAccount = null;
      return;
    }
    const u = j.user;
    activeAccount = u;
    const balance = "$" + ((u.balanceCents | 0) / 100).toFixed(2);
    const activated = u.setupFeePaid || u.setupFeeWaived;
    const setupDollars = "$" + ((j.setupFeeCents | 0) / 100).toFixed(0);
    setLoginCard("lc-ok", {
      email:      u.email,
      name:       u.name || "(no name on file)",
      balance:    balance,
      activation: activated
        ? (u.setupFeeWaived ? "✓ Waived" : "✓ Paid")
        : `Owes ${setupDollars} setup fee`,
    });
    // Mirror server values into the local cache so content.js sees them.
    account = {
      authToken:       t,
      email:           u.email           || "",
      name:            u.name            || "",
      agentCell:       u.agentCell       || "",
      vmDropUrl:       u.vmDropUrl       || "",
      holdMusicPreset: u.holdMusicPreset || "off",
    };
    // Server is authoritative — refresh the inputs to match.
    $("agentCell").value = u.agentCell || "";
    $("vmDropUrl").value = u.vmDropUrl || "";
    hmCurrentSlug = u.holdMusicPreset || "off";
    hmSavedSlug   = hmCurrentSlug;
    renderHoldMusicDisplay();
    await persist();
    applyAuthView();
    // Apply force-Desktop lock if admin set it on this account.
    applyForceDesktopMode(!!u.forceDesktopMode);
    // v1.29.77 — Highlight the cell-phone input + scroll it into view if
    // it's empty (pre-v1.29.61 accounts can be missing this field). The
    // dialer works without a cell, but auto-transfer + callback routing
    // need it. Visual nudge instead of a hard block so the agent isn't
    // stuck out — they can fill it in and just-keep-working.
    if (!u.agentCell || !u.agentCell.trim()) {
      const cellInput = $("agentCell");
      if (cellInput) {
        try {
          cellInput.classList.add("pd-needs-cell");
          cellInput.placeholder = "Add cell — 216-402-3686 (enables auto-transfer)";
          cellInput.scrollIntoView({ behavior: "smooth", block: "center" });
          setTimeout(() => cellInput.focus(), 300);
        } catch (_e) {}
      }
    }
  } catch (err) {
    setLoginCard("lc-bad", {
      email:      "Network error",
      name:       "—",
      balance:    "—",
      activation: (err.message || "fetch failed").slice(0, 40),
    });
    activeAccount = null;
  }
}

// Push agent cell + VM drop URL up to the user's account record.
async function pushDialerConfigToServer() {
  if (!activeAccount) return;
  const t = account.authToken;
  if (!t) return;
  try {
    const res = await fetch(workerUrl("/api/me/update"), {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        token:           t,
        agentCell:       $("agentCell").value.trim(),
        vmDropUrl:       $("vmDropUrl").value.trim(),
        holdMusicPreset: hmCurrentSlug || "off",
      }),
    });
    if (!res.ok) {
      const j = await res.json().catch(() => ({}));
      console.warn("config save:", j.error || res.status);
    }
  } catch (err) {
    console.warn("config save error:", err.message);
  }
}

// Field bindings ─────────────────────────────────────────────────────
function bindAccountField(id, key) {
  $(id).addEventListener("input", () => {
    account[key] = $(id).value.trim();
    persist();
  });
}
bindAccountField("agentCell", "agentCell");
bindAccountField("vmDropUrl", "vmDropUrl");

// Push edits to the server on blur — avoids spamming the API on every keystroke
$("agentCell").addEventListener("blur", pushDialerConfigToServer);
$("vmDropUrl").addEventListener("blur", pushDialerConfigToServer);

// v1.29.50 — Hold-music picker. Fetches the curated preset list from
// the worker and lets the agent cycle through it with ▲ / ▼. Only a
// slug (e.g. "jazz") is sent to /api/me/update — the worker resolves
// it to a URL from its locked-down preset table, so there is no path
// for a free-text URL to reach the dialer's <audio src=>.
// (hmPresets / hmCurrentSlug / hmAuditing declared at the top.)

function renderHoldMusicDisplay() {
  const el = $("hm-display-text");
  if (!el) return;
  if (!hmPresets.length) {
    el.textContent = "Loading…";
    return;
  }
  const p = hmPresets.find((x) => x.slug === hmCurrentSlug) || hmPresets[0];
  hmCurrentSlug = p.slug;
  el.textContent = `${p.emoji || ""} ${p.label || p.slug}`.trim();
  // Visual unsaved-change cue: highlight the display and enable Choose.
  const unsaved = hmCurrentSlug !== hmSavedSlug;
  const wrap = $("hm-display");
  if (wrap) wrap.style.borderColor = unsaved ? "rgba(255,200,80,0.75)" : "";
  const chooseBtn = $("hm-choose");
  if (chooseBtn) {
    chooseBtn.disabled = !unsaved;
    chooseBtn.style.opacity = unsaved ? "1" : "0.55";
  }
}

function cycleHoldMusic(delta) {
  if (!hmPresets.length) return;
  const i = Math.max(0, hmPresets.findIndex((p) => p.slug === hmCurrentSlug));
  const n = hmPresets.length;
  // Wrap around so cycling never gets "stuck" at either end.
  const next = (i + delta + n) % n;
  hmCurrentSlug = hmPresets[next].slug;
  renderHoldMusicDisplay();
  // Stop any in-progress audition so the next ▶ click matches the new pick.
  stopHoldMusicAudition();
  // NB: no auto-save — agent must press ✓ Choose to commit. This stops
  // arrow-misclicks from silently overwriting a saved selection.
}

async function commitHoldMusicChoice() {
  if (hmCurrentSlug === hmSavedSlug) return;
  hmSavedSlug = hmCurrentSlug;
  account.holdMusicPreset = hmCurrentSlug;
  await persist();
  await pushDialerConfigToServer();
  const statusEl = $("hm-audition-status");
  const p = hmPresets.find((x) => x.slug === hmCurrentSlug);
  if (statusEl) {
    statusEl.textContent = `Saved · ${p ? (p.emoji + " " + p.label) : hmCurrentSlug}`;
    statusEl.style.color = "";
    clearTimeout(commitHoldMusicChoice._t);
    commitHoldMusicChoice._t = setTimeout(() => { if (statusEl) statusEl.textContent = ""; }, 3000);
  }
  renderHoldMusicDisplay();
}

async function initHoldMusicPicker() {
  try {
    const r = await fetch(workerUrl("/api/public/settings"), { cache: "no-store" });
    const j = await r.json().catch(() => ({}));
    const list = (j && j.settings && j.settings.holdMusicPresets) || [];
    if (Array.isArray(list) && list.length) {
      hmPresets = list;
    } else {
      hmPresets = [{ slug: "off", emoji: "🔇", label: "Off (silence)", url: "" }];
    }
  } catch (_e) {
    hmPresets = [{ slug: "off", emoji: "🔇", label: "Off (silence)", url: "" }];
  }
  // If the user's saved slug isn't in the list anymore (preset removed
  // server-side), fall back to "off" without saving — they can pick
  // again, and stale slugs auto-resolve on the worker too.
  if (!hmPresets.find((p) => p.slug === hmCurrentSlug)) {
    hmCurrentSlug = "off";
  }
  renderHoldMusicDisplay();
}

function stopHoldMusicAudition() {
  const audioEl = $("hm-audition-audio");
  const playBtn = $("hm-audition");
  const stopBtn = $("hm-audition-stop");
  const statusEl = $("hm-audition-status");
  if (audioEl) { try { audioEl.pause(); audioEl.currentTime = 0; } catch (_e) {} }
  if (playBtn) playBtn.style.display = "";
  if (stopBtn) stopBtn.style.display = "none";
  if (statusEl) statusEl.textContent = "";
  hmAuditing = false;
}

(function wireHoldMusicPicker() {
  const upBtn     = $("hm-up");
  const dnBtn     = $("hm-down");
  const chooseBtn = $("hm-choose");
  const playBtn   = $("hm-audition");
  const stopBtn   = $("hm-audition-stop");
  const audioEl   = $("hm-audition-audio");
  const statusEl  = $("hm-audition-status");
  if (upBtn)     upBtn.addEventListener("click", () => cycleHoldMusic(-1));
  if (dnBtn)     dnBtn.addEventListener("click", () => cycleHoldMusic(+1));
  if (chooseBtn) chooseBtn.addEventListener("click", commitHoldMusicChoice);
  if (playBtn && audioEl) {
    playBtn.addEventListener("click", () => {
      const preset = hmPresets.find((p) => p.slug === hmCurrentSlug);
      const url = preset && preset.url;
      if (!url) {
        if (statusEl) statusEl.textContent = "Nothing to play (Off)";
        return;
      }
      try {
        audioEl.src = url;
        audioEl.load();
        const p = audioEl.play();
        if (p && typeof p.then === "function") {
          p.then(() => {
            hmAuditing = true;
            playBtn.style.display = "none";
            stopBtn.style.display = "";
            if (statusEl) { statusEl.textContent = "Playing…"; statusEl.style.color = "var(--muted)"; }
          }).catch((err) => {
            stopHoldMusicAudition();
            if (statusEl) { statusEl.textContent = "Couldn't play: " + (err?.message || "blocked"); statusEl.style.color = "#ff8a92"; }
          });
        }
      } catch (err) {
        stopHoldMusicAudition();
        if (statusEl) { statusEl.textContent = "Couldn't play"; statusEl.style.color = "#ff8a92"; }
      }
    });
  }
  if (stopBtn) stopBtn.addEventListener("click", stopHoldMusicAudition);
  if (audioEl) {
    audioEl.addEventListener("ended", stopHoldMusicAudition);
    audioEl.addEventListener("error", () => {
      stopHoldMusicAudition();
      if (statusEl) { statusEl.textContent = "Stream failed — try another station"; statusEl.style.color = "#ff8a92"; }
    });
  }
  // Kick the catalog fetch — runs alongside the rest of popup init.
  initHoldMusicPicker();
})();

// ── Email-code login (primary flow) ──────────────────────────────────
let pendingEmail = "";

function setSigninMsg(id, text, kind) {
  const el = $(id);
  if (!el) return;
  el.textContent = text || "";
  el.classList.remove("ok", "err");
  if (kind) el.classList.add(kind);
}

function showCodeStep(email) {
  pendingEmail = email;
  $("code-target-email").textContent = email;
  $("signin-step-email").style.display = "none";
  $("signin-step-code").style.display  = "block";
  setTimeout(() => $("signinCode").focus(), 50);
}
function showEmailStep() {
  pendingEmail = "";
  $("signinCode").value = "";
  $("signin-step-email").style.display = "block";
  $("signin-step-code").style.display  = "none";
  setSigninMsg("verify-msg", "");
  setTimeout(() => $("signinEmail").focus(), 50);
}

async function sendLoginCode(email) {
  if (!email || !email.includes("@")) {
    setSigninMsg("signin-msg", "Enter a valid email address.", "err");
    return false;
  }
  setSigninMsg("signin-msg", "Sending code…");
  try {
    const res = await fetch(workerUrl("/api/auth/code/start"), {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email }),
    });
    const j = await res.json().catch(() => ({}));
    if (!res.ok || !j.ok) {
      setSigninMsg("signin-msg", j.error || `Couldn't send code (HTTP ${res.status})`, "err");
      return false;
    }
    setSigninMsg("signin-msg", "Code sent — check your email.", "ok");
    return true;
  } catch (err) {
    setSigninMsg("signin-msg", "Network error: " + (err.message || "couldn't reach worker"), "err");
    return false;
  }
}

async function verifyLoginCode(email, code) {
  setSigninMsg("verify-msg", "Verifying…");
  try {
    const res = await fetch(workerUrl("/api/auth/code/verify"), {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email, code }),
    });
    const j = await res.json().catch(() => ({}));
    if (!res.ok || !j.ok) {
      setSigninMsg("verify-msg", j.error || `Couldn't verify (HTTP ${res.status})`, "err");
      return false;
    }
    const u = j.user;
    // Cache full account locally — content.js reads from pd_profiles via persist().
    account = {
      authToken:       u.token           || "",
      email:           u.email           || "",
      name:            u.name            || "",
      agentCell:       u.agentCell       || "",
      vmDropUrl:       u.vmDropUrl       || "",
      holdMusicPreset: u.holdMusicPreset || "off",
    };
    activeAccount = u;
    $("authToken").value = account.authToken;
    $("agentCell").value = account.agentCell;
    $("vmDropUrl").value = account.vmDropUrl;
    hmCurrentSlug = account.holdMusicPreset;
    renderHoldMusicDisplay();
    await persist();
    // Populate the signed-in card and flip to signed-in view
    const balance = "$" + ((u.balanceCents | 0) / 100).toFixed(2);
    const activated = u.setupFeePaid || u.setupFeeWaived;
    setLoginCard("lc-ok", {
      email:      u.email,
      name:       u.name || "(no name on file)",
      balance,
      activation: activated
        ? (u.setupFeeWaived ? "✓ Waived" : "✓ Paid")
        : "Owes setup fee — open dashboard",
    });
    applyAuthView();
    setSigninMsg("verify-msg", "");
    return true;
  } catch (err) {
    setSigninMsg("verify-msg", "Network error: " + (err.message || "couldn't reach worker"), "err");
    return false;
  }
}

// v1.30.1 — Popup dual-login: Password tab vs Email code tab.
// Visual styling matches /login, /dashboard, /admin (same tab paddings,
// same active/idle colors, same forgot-password link). Tunes only the
// font/padding scale down by ~1px to fit a 320px popup.
function setPopupLoginMethod(name) {
  const tabPw   = $("popup-tab-pw");
  const tabCode = $("popup-tab-code");
  const formPw  = $("popup-form-password");
  const stepEm  = $("signin-step-email");
  const stepCd  = $("signin-step-code");
  const baseStyle   = "flex:1;padding:7px 10px;border:0;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;";
  const activeStyle = "background:rgba(255,255,255,0.08);color:#fff;";
  const idleStyle   = "background:transparent;color:var(--muted);";
  if (name === "password") {
    if (tabPw)   tabPw.style.cssText   = baseStyle + activeStyle;
    if (tabCode) tabCode.style.cssText = baseStyle + idleStyle;
    if (formPw) formPw.style.display = "";
    if (stepEm) stepEm.style.display = "none";
    if (stepCd) stepCd.style.display = "none";
    setTimeout(() => $("popupPwEmail")?.focus(), 50);
  } else {
    if (tabPw)   tabPw.style.cssText   = baseStyle + idleStyle;
    if (tabCode) tabCode.style.cssText = baseStyle + activeStyle;
    if (formPw) formPw.style.display = "none";
    if (stepEm) stepEm.style.display = "";
    if (stepCd) stepCd.style.display = "none";
    setTimeout(() => $("signinEmail")?.focus(), 50);
  }
  try { localStorage.setItem("gpd_popup_login_method", name); } catch {}
}
// v1.33.7.2 — Email-code retired from the popup; password is the only method.
// "Forgot / set password" reveals a reset-link panel (POST /api/auth/forgot-password),
// matching /login, /dashboard, and the extension Setup drawer.
function popupShowForgot(show) {
  const formPw = $("popup-form-password");
  const panel  = $("popup-forgot-pane");
  if (!formPw || !panel) return;
  if (show) {
    const v = ($("popupPwEmail")?.value || "").trim();
    const fe = $("popup-forgot-email");
    if (v && fe && !fe.value) fe.value = v;
    formPw.style.display = "none";
    panel.style.display = "block";
    setTimeout(() => $("popup-forgot-email")?.focus(), 50);
  } else {
    panel.style.display = "none";
    formPw.style.display = "";
    setTimeout(() => $("popupPwEmail")?.focus(), 50);
  }
}
$("popup-forgot-pw")?.addEventListener("click", (e) => { e.preventDefault(); popupShowForgot(true); });
$("popup-forgot-back")?.addEventListener("click", () => popupShowForgot(false));
$("popup-forgot-send")?.addEventListener("click", async () => {
  const email = ($("popup-forgot-email").value || "").trim().toLowerCase();
  if (!email || !email.includes("@")) { setSigninMsg("popup-forgot-msg", "Enter a valid email address.", "err"); return; }
  setSigninMsg("popup-forgot-msg", "Sending…");
  try {
    const res = await fetch(workerUrl("/api/auth/forgot-password"), {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email }),
    });
    const j = await res.json().catch(() => ({}));
    if (res.ok && j.ok) {
      setSigninMsg("popup-forgot-msg", (j.message || "If that email is on file, a reset link is on its way.") + " Check your inbox.", "ok");
    } else {
      setSigninMsg("popup-forgot-msg", j.error || `Couldn't send reset link (HTTP ${res.status})`, "err");
    }
  } catch (err) {
    setSigninMsg("popup-forgot-msg", "Network error: " + (err.message || "couldn't reach worker"), "err");
  }
});

// v1.30.0 — Password sign-in path. Calls /api/auth/login, then reuses
// the existing token-handoff path (verifyLoginCode already does this).
$("popup-form-password")?.addEventListener("submit", async (e) => {
  e.preventDefault();
  const email = ($("popupPwEmail").value || "").trim().toLowerCase();
  const password = $("popupPwPassword").value || "";
  // v1.33 Phase 2 — Phone is the per-session routing identifier. Sent
  // to /api/auth/login; worker normalizes + appends to user.phones[].
  // Returned in the response so the popup can persist it locally and
  // include it in WS URLs as ?phone=X for deterministic DO routing.
  const phone = ($("popupPwPhone")?.value || "").trim();
  if (!email || !email.includes("@")) {
    setSigninMsg("popup-pw-msg", "Enter a valid email.", "err");
    return;
  }
  if (!password) {
    setSigninMsg("popup-pw-msg", "Enter your password.", "err");
    return;
  }
  if (!phone || phone.replace(/[^\d]/g, "").length < 10) {
    setSigninMsg("popup-pw-msg", "Enter your phone (10+ digits — the callback number you want prospects to reach).", "err");
    return;
  }
  setSigninMsg("popup-pw-msg", "Signing in…");
  try {
    const res = await fetch(workerUrl("/api/auth/login"), {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email, password, phone }),
    });
    const j = await res.json().catch(() => ({}));
    if (!res.ok || !j.ok) {
      setSigninMsg("popup-pw-msg", j.error || `Wrong email or password (HTTP ${res.status})`, "err");
      return;
    }
    const u = j.user;
    // Same handoff as verifyLoginCode — cache + populate UI + flip view
    account = {
      authToken:       u.token           || "",
      email:           u.email           || "",
      name:            u.name            || "",
      agentCell:       u.agentCell       || "",
      // v1.33 Phase 2 — Phone identifier for WS routing. content.js
      // reads activeProfile().phone and appends ?phone=X to WS URLs.
      phone:           u.phone           || "",
      vmDropUrl:       u.vmDropUrl       || "",
      holdMusicPreset: u.holdMusicPreset || "off",
    };
    activeAccount = u;
    $("authToken").value = account.authToken;
    $("agentCell").value = account.agentCell;
    $("vmDropUrl").value = account.vmDropUrl;
    hmCurrentSlug = account.holdMusicPreset;
    renderHoldMusicDisplay();
    await persist();
    const balance = "$" + ((u.balanceCents | 0) / 100).toFixed(2);
    const activated = u.setupFeePaid || u.setupFeeWaived;
    setLoginCard("lc-ok", {
      email:      u.email,
      name:       u.name || "(no name on file)",
      balance,
      activation: activated ? (u.setupFeeWaived ? "✓ Waived" : "✓ Paid") : "Owes setup fee — open dashboard",
    });
    applyAuthView();
    setSigninMsg("popup-pw-msg", "");
  } catch (err) {
    setSigninMsg("popup-pw-msg", "Network error: " + (err.message || "couldn't reach worker"), "err");
  }
});

$("send-code")?.addEventListener("click", async () => {
  const email = ($("signinEmail").value || "").trim().toLowerCase();
  const ok = await sendLoginCode(email);
  if (ok) showCodeStep(email);
});
$("signinEmail")?.addEventListener("keydown", (e) => {
  if (e.key === "Enter") $("send-code").click();
});

$("verify-code")?.addEventListener("click", async () => {
  const code = ($("signinCode").value || "").trim();
  if (!/^\d{6}$/.test(code)) {
    setSigninMsg("verify-msg", "Enter the 6-digit code from your email.", "err");
    return;
  }
  await verifyLoginCode(pendingEmail, code);
});
$("signinCode")?.addEventListener("keydown", (e) => {
  if (e.key === "Enter") $("verify-code").click();
});
// Auto-submit when 6 digits are entered (paste-friendly)
$("signinCode")?.addEventListener("input", () => {
  const v = $("signinCode").value.replace(/\D/g, "").slice(0, 6);
  $("signinCode").value = v;
  if (v.length === 6) {
    setTimeout(() => $("verify-code").click(), 50);
  }
});

$("resend-code")?.addEventListener("click", async () => {
  if (!pendingEmail) return showEmailStep();
  setSigninMsg("verify-msg", "Resending…");
  const ok = await sendLoginCode(pendingEmail);
  if (ok) setSigninMsg("verify-msg", "New code sent.", "ok");
});
$("change-email")?.addEventListener("click", showEmailStep);

$("sign-out")?.addEventListener("click", async () => {
  account = { ...DEFAULTS.pd_account };
  activeAccount = null;
  $("authToken").value = "";
  $("agentCell").value = "";
  $("vmDropUrl").value = "";
  hmCurrentSlug = "off";
  hmSavedSlug   = "off";
  renderHoldMusicDisplay();
  stopHoldMusicAudition();
  await persist();
  applyAuthView();
  showEmailStep();
});

// ── Token field (Advanced) ──
// Token field — local save + debounced /api/me lookup
$("authToken").addEventListener("input", () => {
  const v = $("authToken").value.trim();
  account.authToken = v;
  persist();
  if (lookupTimer) clearTimeout(lookupTimer);
  lookupTimer = setTimeout(() => lookupAccount(v), 450);
});

$("workerBase").addEventListener("input", () => {
  workerBase = $("workerBase").value.trim();
  persist();
});

document.querySelectorAll('input[name="skin"]').forEach((el) => {
  el.addEventListener("change", () => {
    if (!el.checked) return;
    skin = (el.value === "goal") ? "goal" : "phone";
    persist();
  });
});

// Force-Desktop mode applier — admin can lock a user (typically an
// international agent) into Desktop mode regardless of what they pick
// in the popup. When set, hide the Phone option and force-switch to
// Desktop if they're currently on Phone.
function applyForceDesktopMode(force) {
  const cellLabel  = $("mode-phone-label"); // (this is also the "cell" label wrapper)
  const lockedHint = $("mode-locked-hint");
  const cellRadio  = $("mode-cell");
  const mobileRadio  = $("mode-mobile");
  const desktopRadio = $("mode-desktop");
  if (force) {
    // Lock to Desktop (WebRTC). Hide Cell, disable both Cell + Mobile —
    // only Desktop is allowed for admin-locked accounts.
    if (cellLabel)   cellLabel.style.display = "none";
    if (lockedHint)  lockedHint.style.display = "block";
    if (cellRadio)   cellRadio.disabled = true;
    if (mobileRadio) mobileRadio.disabled = true;
    if (dialMode !== "desktop") {
      dialMode = "desktop";
      if (desktopRadio) desktopRadio.checked = true;
      persist();
      pushParkSourceToServer("desktop").catch(() => {});
      try { applyDialModeView(); } catch (_e) {}
    }
  } else {
    if (cellLabel)   cellLabel.style.display = "";
    if (lockedHint)  lockedHint.style.display = "none";
    if (cellRadio)   cellRadio.disabled = false;
    if (mobileRadio) mobileRadio.disabled = false;
  }
}

// Park-source radios — three options now (cell, mobile_web, desktop).
// "cell" = Telnyx calls the agent's mobile via PSTN.
// "mobile_web" = the /mobile site WebRTC takes the agent leg.
// "desktop" = the extension's embedded WebRTC sandbox (or /desktop site).
document.querySelectorAll('input[name="dial-mode"]').forEach((el) => {
  el.addEventListener("change", () => {
    if (!el.checked) return;
    if (el.value === "cell" || el.value === "mobile_web" || el.value === "desktop") {
      dialMode = el.value;
    } else {
      dialMode = "cell"; // safety default
    }
    applyDialModeView();
    persist();
    refreshDesktopStatus();
    // Also push the choice up to the user record so /mobile, /desktop,
    // and the in-page panel all see the same activeParkSource.
    pushParkSourceToServer(dialMode).catch(() => {});
    if (dialMode === "desktop") {
      // Need a token AND a mic grant before we can usefully start the
      // offscreen doc. Mic grant happens via the dedicated button below;
      // we don't auto-enable until both are in place.
      if (!(account.authToken || "").trim()) {
        setDeskStatus("Sign in first (above)", "err");
        return;
      }
      if (!micGranted) {
        setDeskStatus("Grant microphone access ↓", null);
        return;
      }
      pushDesktopMode();
    } else {
      pushDesktopMode();
    }
  });
});

// Grant microphone access — must run under a user gesture (this click)
// so the browser actually shows the prompt. Once granted at the
// extension origin, the offscreen doc's getUserMedia() works without a
// further prompt forever (until the user revokes mic in chrome://settings).
$("grant-mic")?.addEventListener("click", async () => {
  setDeskStatus("Requesting mic…", null);
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true },
      video: false,
    });
    // Stop the tracks immediately — we just needed the prompt to fire.
    // The actual mic capture happens inside the offscreen doc.
    stream.getTracks().forEach((t) => t.stop());
    micGranted = true;
    persist();
    applyDialModeView();
    setDeskStatus("Mic granted — starting Desktop client…", "ok");
    // If we're already in desktop mode and signed in, start the
    // offscreen doc now that we have everything we need.
    if (dialMode === "desktop" && (account.authToken || "").trim()) {
      pushDesktopMode();
    }
  } catch (err) {
    setDeskStatus("Mic denied: " + (err.message || err.name), "err");
  }
});

// Worker connectivity check
$("ping").addEventListener("click", async () => {
  const status = $("connStatus");
  status.classList.remove("ok", "err");
  status.querySelector("span:last-child").textContent = "Pinging…";
  if (!workerBase) {
    status.classList.add("err");
    status.querySelector("span:last-child").textContent = "Set Worker base URL";
    return;
  }
  try {
    const tok = (account.authToken || "").trim();
    const qs = tok ? `?token=${encodeURIComponent(tok)}` : "";
    const url = workerBase.replace(/\/+$/, "") + "/healthz" + qs;
    const res = await fetch(url, { method: "GET" });
    if (res.ok) {
      status.classList.add("ok");
      status.querySelector("span:last-child").textContent = "Worker reachable";
    } else {
      status.classList.add("err");
      status.querySelector("span:last-child").textContent = `HTTP ${res.status}`;
    }
  } catch (err) {
    status.classList.add("err");
    status.querySelector("span:last-child").textContent = err.message || "Unreachable";
  }
});

function send(type) {
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    if (!tabs[0]) return;
    chrome.tabs.sendMessage(tabs[0].id, { type }, () => {
      void chrome.runtime.lastError;
    });
  });
}
$("show").addEventListener("click", () => send("pd_show"));
$("reset-pos").addEventListener("click", () => send("pd_reset_position"));
$("show-tour")?.addEventListener("click", () => { send("pd_show"); send("pd_show_tour"); });

// The Web Store extension ID. If our chrome.runtime.id matches this,
// we're the published Web Store version (auto-updates). Anything else
// is a side-loaded developer install — show a nudge to switch.
const WEB_STORE_EXT_ID = "nlhjlboobaoifibjagenpemokijicdaf";

// v1.33.7.2 — The Chrome Web Store listing is NOT live yet, so the "install
// the Web Store version" nudge was telling side-loaded agents to install
// something that doesn't exist. Keep this false until the listing is actually
// published, then flip it to true.
const WEB_STORE_LIVE = false;

function maybeShowSideloadNudge() {
  try {
    if (!WEB_STORE_LIVE) return;   // not published yet — never show the nudge
    const nudge = $("sideload-nudge");
    if (!nudge) return;
    const id = chrome.runtime && chrome.runtime.id;
    if (id && id !== WEB_STORE_EXT_ID) {
      nudge.style.display = "block";
    }
  } catch (_e) {}
}

load().then(() => {
  maybeShowSideloadNudge();
  // If a token is already cached, do an initial lookup so the login-card
  // is populated the moment the popup opens; otherwise focus the email
  // input on the signed-out pane.
  if ((account.authToken || "").trim()) {
    lookupAccount(account.authToken);
  } else {
    setTimeout(() => $("signinEmail")?.focus(), 50);
  }
});
