/* Settings page data binder.
 * Wires the segment buttons + dropdowns in the design to the user's
 * config.json. Cleanup level applies live; hotkey + whisper model need
 * an app restart (we show a hint badge after the change).
 */

(function () {
  function esc(s) {
    return String(s == null ? "" : s)
      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
  }

  // Field-row label → setting key + value-to-button-label map.
  var FIELDS = [
    {
      label: "cleanup level",
      key: "cleanup.level",
      type: "seg",
      values: { "raw": "Raw", "light": "Light",
                "standard": "Standard", "polished": "Polished" },
    },
    {
      label: "hotkey",
      key: "hotkey.mode",
      type: "seg",
      values: { "right_option": "Right Option", "left_option": "Left Option",
                "fn": "Fn" },
      restart: true,
    },
    {
      label: "whisper model",
      key: "whisper.model",
      type: "select",
      values: {
        "mlx-community/whisper-tiny-mlx":     "tiny",
        "mlx-community/whisper-small-mlx":    "small",
        "mlx-community/whisper-medium-mlx":   "medium",
        "mlx-community/whisper-large-v3-mlx": "large-v3",
      },
      restart: true,
    },
    {
      label: "language",
      key: "whisper.language",
      type: "select",
      values: {
        "auto": "Auto", "ru": "Русский", "en": "English",
        "tr": "Türkçe", "de": "Deutsch", "fr": "Français",
        "es": "Español",
      },
      restart: true,
    },
    {
      label: "hands-free pause",
      key: "dictation.hands_free_pause_ms",
      type: "seg",
      values: { 1200: "Snappy", 2000: "Standard", 3500: "Patient" },
    },
    // CR (2026-06-23) — user-facing dictation cloud/local toggle.
    // Restart required: app.py builds the dictation transcriber once at
    // boot via make_dictation_transcriber; flipping cloud_mode mid-session
    // wouldn't rebuild it. Hint shown after save.
    {
      label: "voice processing",
      key: "dictation.cloud_mode",
      type: "seg",
      values: { false: "Local (offline)", true: "Cloud (fast)" },
      desc: "Local keeps audio on your Mac (slower on first run). Cloud sends short audio chunks to the Lirelle server for transcription.",
      restart: true,
    },
    // CR #8 — Cloud summaries toggle. Reads/writes summary_consent.json
    // via dedicated bridge handlers, NOT update_setting/config.json,
    // because consent is its own file with its own perms (0600).
    {
      label: "cloud summaries",
      type: "consent",
    },
  ];

  function findFieldRow(label) {
    var rows = document.querySelectorAll(".field-row");
    for (var i = 0; i < rows.length; i++) {
      var lbl = rows[i].querySelector(".fr-label");
      if (lbl && lbl.textContent.trim().toLowerCase() === label) return rows[i];
    }
    return null;
  }

  // CR (2026-06-23) — auto-build a row for fields not present in the
  // static HTML. Used for the "voice processing" toggle we added to JS
  // without touching Settings.html.
  function ensureFieldRow(field) {
    var row = findFieldRow(field.label);
    if (row) return row;
    var sec = document.querySelector(".sec-wrap");
    if (!sec) return null;
    var label = field.label.charAt(0).toUpperCase() + field.label.slice(1);
    var desc = field.desc || "";
    row = document.createElement("div");
    row.className = "field-row";
    var rightHtml = (field.type === "seg")
      ? '<div class="seg"></div>'
      : '<div></div>';
    row.innerHTML =
      '<div><div class="fr-label">' + esc(label) + '</div>'
      + (desc ? '<div class="fr-desc">' + esc(desc) + '</div>' : '')
      + '</div>' + rightHtml;
    sec.appendChild(row);
    return row;
  }

  function paintSeg(row, field, currentValue) {
    // Find or build the .seg container in this row.
    var seg = row.querySelector(".seg");
    if (!seg) return;
    seg.innerHTML = "";
    Object.keys(field.values).forEach(function (val) {
      var btn = document.createElement("button");
      btn.textContent = field.values[val];
      btn.dataset.value = val;
      // String() comparison — Object.keys() always returns strings,
      // but currentValue may be a number (e.g. hands_free_pause_ms = 2000).
      if (String(val) === String(currentValue)) btn.className = "on";
      btn.addEventListener("click", function () {
        applyChange(seg, field, val);
      });
      seg.appendChild(btn);
    });
  }

  function paintSelect(row, field, currentValue) {
    // Replace the right-hand static label with a styled <select>.
    // Default macOS <select> on Sequoia looks glassy/2008; force
    // appearance:none + provide our own chevron so it matches the
    // dark brand. CSS-only solution, no JS dropdown widget needed.
    var container = row.querySelector(":scope > div:last-child");
    if (!container) return;
    container.innerHTML = "";
    var wrap = document.createElement("div");
    wrap.style.cssText = "position:relative;display:inline-block;";
    var sel = document.createElement("select");
    sel.style.cssText = [
      "-webkit-appearance: none",
      "appearance: none",
      "background: rgba(255,255,255,.04)",
      "color: var(--text, #efeae4)",
      "border: 1px solid rgba(255,255,255,.08)",
      "border-radius: 8px",
      "padding: 7px 30px 7px 12px",   // right padding for chevron
      "font-family: var(--mono)",
      "font-size: 12.5px",
      "line-height: 1.3",
      "cursor: pointer",
      "outline: none",
      "min-width: 140px",
    ].join(";") + ";";
    Object.keys(field.values).forEach(function (val) {
      var opt = document.createElement("option");
      opt.value = val;
      opt.textContent = field.values[val];
      if (String(val) === String(currentValue)) opt.selected = true;
      // Style options too (macOS sometimes shows them dark by default).
      opt.style.background = "#141414";
      opt.style.color = "#efeae4";
      sel.appendChild(opt);
    });
    sel.addEventListener("change", function () {
      applyChange(container, field, sel.value);
    });
    // Custom chevron (svg, gold accent) layered to the right.
    var chev = document.createElement("span");
    chev.innerHTML =
      '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" '
      + 'aria-hidden="true"><path d="M6 9l6 6 6-6" '
      + 'stroke="currentColor" stroke-width="2" stroke-linecap="round" '
      + 'stroke-linejoin="round"/></svg>';
    chev.style.cssText =
      "position:absolute;right:10px;top:50%;transform:translateY(-50%);"
      + "color:var(--ink-dim,#888f8a);pointer-events:none;display:flex;";
    wrap.appendChild(sel);
    wrap.appendChild(chev);
    container.appendChild(wrap);
  }

  function applyChange(scope, field, newValue) {
    window.lumo.call("updateSetting", { key: field.key, value: newValue })
      .then(function (res) {
        if (!res || !res.ok) {
          alert("Couldn't save: " + ((res && res.error) || "unknown"));
          return;
        }
        // Update visible .on for seg, value for select.
        if (field.type === "seg") {
          scope.querySelectorAll("button").forEach(function (b) {
            b.classList.toggle("on", b.dataset.value === newValue);
          });
        }
        // CR (2026-06-23) — when the user switches voice processing to
        // Local on a Mac where the Whisper model has never been
        // downloaded, show an explicit "downloading Whisper" overlay
        // instead of silently leaving them with a backend that can't
        // transcribe and no UI cue. Surfaces the failure mode early
        // (size, progress, completion) so meetings actually work.
        if (field.key === "dictation.cloud_mode"
            && String(newValue) === "false") {
          ensureLocalModelOrDownload();
        }
        if (res.needs_restart) {
          showRestartHint(field);
        }
        // Re-render the backend status row so the OFFLINE/ONLINE pill
        // and the model row stay in sync with the new setting.
        paintTranscriberStatusRow();
      })
      .catch(function (err) { alert("Save threw: " + err); });
  }

  async function ensureLocalModelOrDownload() {
    var st;
    try {
      st = await window.lumo.call("requestTranscriberStatus");
    } catch (err) {
      console.error("ensureLocalModelOrDownload status failed:", err);
      return;
    }
    if (st && st.local_ready) return;  // already ready, nothing to do
    showModelDownloadOverlay();
  }

  function showModelDownloadOverlay() {
    if (document.querySelector(".lumo-download-overlay")) return;  // singleton
    var overlay = document.createElement("div");
    overlay.className = "lumo-download-overlay";
    overlay.style.cssText =
      "position:fixed;inset:0;background:rgba(5,5,5,.86);"
      + "z-index:99999;display:flex;align-items:center;justify-content:center;"
      + "backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);";
    overlay.innerHTML =
      '<div style="background:var(--card,#141414);'
      +   'border:1px solid rgba(255,255,255,.08);border-radius:16px;'
      +   'padding:28px 30px;max-width:460px;width:calc(100% - 40px);'
      +   'box-shadow:0 30px 80px rgba(0,0,0,.6);">'
      +   '<div style="font-family:var(--display);font-size:22px;'
      +     'color:var(--text,#efeae4);margin-bottom:10px;">'
      +     'Downloading Whisper'
      +   '</div>'
      +   '<div style="font-family:var(--mono);font-size:12.5px;'
      +     'color:var(--ink,#888f8a);line-height:1.55;margin-bottom:18px;">'
      +     'You switched to Local voice processing. The Whisper model '
      +     '(~1.5 GB) needs to be on disk so Lirelle can transcribe '
      +     'without going to the cloud. This runs once and is shared '
      +     'between dictation and meeting recording.'
      +   '</div>'
      +   '<div data-dl-status style="font-family:var(--mono);font-size:12px;'
      +     'color:var(--text,#efeae4);margin-bottom:8px;">Starting…</div>'
      +   '<div style="height:8px;background:rgba(255,255,255,.06);'
      +     'border-radius:4px;overflow:hidden;margin-bottom:8px;">'
      +     '<div data-dl-bar style="height:100%;width:0%;'
      +       'background:var(--accent,#ebb638);transition:width .3s ease;">'
      +     '</div>'
      +   '</div>'
      +   '<div data-dl-bytes style="font-family:var(--mono);font-size:11px;'
      +     'color:var(--ink,#888f8a);text-align:right;margin-bottom:18px;">'
      +     '— / —'
      +   '</div>'
      +   '<div style="display:flex;justify-content:flex-end;gap:10px;">'
      +     '<button data-dl-close style="appearance:none;-webkit-appearance:none;'
      +       'cursor:pointer;background:transparent;color:var(--ink,#888f8a);'
      +       'border:1px solid rgba(255,255,255,.12);padding:8px 14px;'
      +       'border-radius:8px;font-family:var(--mono);font-size:12px;">'
      +       'Hide'
      +     '</button>'
      +   '</div>'
      + '</div>';
    document.body.appendChild(overlay);
    overlay.querySelector("[data-dl-close]").addEventListener("click", function () {
      overlay.parentNode && overlay.parentNode.removeChild(overlay);
    });

    // Kick the actual download.
    window.lumo.call("downloadOfflineModel").then(function (res) {
      if (!res || !res.ok) {
        overlay.querySelector("[data-dl-status]").textContent =
          "Couldn't start: " + ((res && res.error) || "unknown");
      }
    }).catch(function (err) {
      overlay.querySelector("[data-dl-status]").textContent =
        "Download threw: " + err;
    });

    pollDownloadProgress(overlay);
  }

  async function pollDownloadProgress(overlay) {
    while (overlay.parentNode) {
      var st;
      try {
        st = await window.lumo.call("requestTranscriberStatus");
      } catch (err) { st = null; }
      if (st) {
        var prog = st.download_progress || {};
        var statusEl = overlay.querySelector("[data-dl-status]");
        var barEl    = overlay.querySelector("[data-dl-bar]");
        var bytesEl  = overlay.querySelector("[data-dl-bytes]");
        if (prog.bytes_total > 0) {
          var pct = Math.min(100,
            Math.round(100 * prog.bytes_done / prog.bytes_total));
          barEl.style.width = pct + "%";
          bytesEl.textContent =
            fmtBytes(prog.bytes_done) + " / " + fmtBytes(prog.bytes_total)
            + "  (" + pct + "%)";
        } else if (prog.bytes_done > 0) {
          bytesEl.textContent = fmtBytes(prog.bytes_done);
        }
        if (prog.msg) statusEl.textContent = prog.msg;
        if (st.local_ready || prog.done) {
          barEl.style.width = "100%";
          statusEl.textContent = "Model ready — Local transcription enabled.";
          overlay.querySelector("[data-dl-close]").textContent = "Done";
          paintTranscriberStatusRow();
          break;
        }
        if (prog.error) {
          statusEl.textContent = "Download failed: " + prog.error;
          break;
        }
      }
      await new Promise(function (r) { setTimeout(r, 1200); });
    }
  }

  function showRestartHint(field) {
    var existing = document.querySelector(".lumo-restart-hint");
    if (existing) return;  // one is enough
    var hint = document.createElement("div");
    hint.className = "lumo-restart-hint";
    hint.style.cssText =
      "position:fixed;bottom:24px;left:50%;transform:translateX(-50%);"
      + "background:var(--card,#141414);border:1px solid var(--accent,#ebb638);"
      + "color:var(--text,#efeae4);padding:10px 18px;border-radius:10px;"
      + "font-family:var(--mono);font-size:12px;z-index:9999;"
      + "box-shadow:0 12px 32px rgba(0,0,0,.6);";
    hint.textContent = "Saved. Restart Lirelle for the new "
      + field.label + " to take effect.";
    document.body.appendChild(hint);
    setTimeout(function () { hint.style.opacity = "0";
                             hint.style.transition = "opacity 400ms"; }, 3500);
    setTimeout(function () { hint.parentNode && hint.parentNode.removeChild(hint); }, 4200);
  }

  async function loadSettings() {
    var s;
    try {
      s = await window.lumo.call("requestSettings");
    } catch (err) {
      console.error("requestSettings failed:", err);
      return;
    }
    if (!s) return;
    var current = {
      "cleanup.level": s.cleanup_level,
      "hotkey.mode": s.hotkey_mode,
      "whisper.model": s.whisper_model,
      "whisper.language": s.whisper_language,
      "dictation.hands_free_pause_ms": s.hands_free_pause_ms,
      "dictation.cloud_mode": s.dictation_cloud_mode,
    };
    FIELDS.forEach(function (field) {
      // ensureFieldRow auto-builds a row for fields not present in the
      // static HTML (used by the new "voice processing" toggle).
      var row = ensureFieldRow(field);
      if (field.type === "consent") {
        paintConsentRow(row, field);
        return;
      }
      if (!row) return;
      var val = current[field.key];
      if (field.type === "seg") paintSeg(row, field, val);
      else if (field.type === "select") paintSelect(row, field, val);
    });
  }

  async function paintConsentRow(existingRow, field) {
    // CR #8 — the design HTML doesn't include a "cloud summaries" row.
    // Inject one at the end of the .sec-wrap so the user can flip the
    // consent flag without dictating a new meeting first. If a row
    // already exists (re-render), reuse it.
    var state;
    try {
      state = await window.lumo.call("requestSummaryConsent");
    } catch (err) {
      console.error("requestSummaryConsent failed:", err);
      state = { granted: null };
    }
    var sec = document.querySelector(".sec-wrap");
    if (!sec) return;
    var row = existingRow || sec.querySelector(".field-row[data-consent]");
    if (!row) {
      row = document.createElement("div");
      row.className = "field-row";
      row.dataset.consent = "1";
      // CR (2026-06-23) — rename was "Cloud summaries"; users read it
      // as "the cloud master switch" and got confused why OFFLINE
      // transcription co-existed with "Cloud … On". Explicit:
      // this is the LLM summary path, NOT the transcription path.
      row.innerHTML =
        '<div><div class="fr-label">Meeting AI summary</div>'
        + '<div class="fr-desc">After a meeting transcript is ready, '
        + 'send the <em>text</em> (never the audio) to the LLM so it '
        + 'writes a short summary. Independent from "Voice processing" '
        + 'above — that controls speech→text; this controls text→summary.'
        + '</div></div>'
        + '<div class="seg">'
        +   '<button data-consent-value="off">Off</button>'
        +   '<button data-consent-value="on">On</button>'
        + '</div>';
      sec.appendChild(row);
    }
    var onBtn  = row.querySelector('[data-consent-value="on"]');
    var offBtn = row.querySelector('[data-consent-value="off"]');
    onBtn.classList.toggle("on", state.granted === true);
    offBtn.classList.toggle("on", state.granted === false || state.granted === null);
    function setConsent(granted) {
      window.lumo.call("setSummaryConsent", { granted: granted })
        .then(function (res) {
          if (!res || !res.ok) {
            alert("Couldn't save: " + ((res && res.error) || "unknown"));
            return;
          }
          onBtn.classList.toggle("on", granted === true);
          offBtn.classList.toggle("on", granted === false);
          if (granted) {
            // CR #8 — when the user enables cloud summaries, retro-fire
            // summary for any meetings that were waiting for consent.
            window.lumo.call("retrySummariesForAwaiting")
              .then(function (r) {
                if (r && r.ok && r.kicked) {
                  showHint("Kicked summary for " + r.kicked
                          + " pending meeting" + (r.kicked === 1 ? "" : "s"));
                }
              })
              .catch(function () {});
          }
        });
    }
    onBtn.onclick  = function () { setConsent(true); };
    offBtn.onclick = function () { setConsent(false); };
  }

  function showHint(text) {
    var hint = document.createElement("div");
    hint.style.cssText =
      "position:fixed;bottom:24px;left:50%;transform:translateX(-50%);"
      + "background:var(--card,#141414);border:1px solid var(--accent,#ebb638);"
      + "color:var(--text,#efeae4);padding:10px 18px;border-radius:10px;"
      + "font-family:var(--mono);font-size:12px;z-index:9999;"
      + "box-shadow:0 12px 32px rgba(0,0,0,.6);";
    hint.textContent = text;
    document.body.appendChild(hint);
    setTimeout(function () { hint.style.opacity = "0";
                             hint.style.transition = "opacity 400ms"; }, 2200);
    setTimeout(function () { hint.parentNode && hint.parentNode.removeChild(hint); }, 2700);
  }

  // CR (2026-06-23) — Transcription backend status row + offline-model
  // download button. Tells the user whether dictation is going through
  // Cloud (Groq) or a local backend, and lets them pre-cache the local
  // Whisper model so the offline path is ready when network drops.
  function fmtBytes(n) {
    n = Number(n || 0);
    if (n <= 0) return "—";
    var mb = n / 1048576;
    if (mb < 1024) return mb.toFixed(0) + " MB";
    return (mb / 1024).toFixed(2) + " GB";
  }

  async function paintTranscriberStatusRow() {
    var sec = document.querySelector(".sec-wrap");
    if (!sec) return;
    var row = sec.querySelector('.field-row[data-tr-status="1"]');
    if (!row) {
      row = document.createElement("div");
      row.className = "field-row";
      row.dataset.trStatus = "1";
      row.innerHTML =
        '<div style="flex:1 1 0%;">'
        +   '<div class="fr-label" style="display:flex;align-items:center;'
        +     'gap:10px;">Transcription backend'
        +     '<span data-tr-mode-pill class="plan-pill" style="font-size:11px;'
        +     'padding:2px 8px;">…</span>'
        +   '</div>'
        +   '<div class="fr-desc" data-tr-active>Checking…</div>'
        +   '<div class="fr-desc" data-tr-model style="margin-top:4px;"></div>'
        + '</div>'
        + '<div data-tr-action style="display:flex;flex-direction:column;'
        +    'align-items:flex-end;gap:6px;min-width:160px;"></div>';
      sec.appendChild(row);
    }

    var st;
    try {
      st = await window.lumo.call("requestTranscriberStatus");
    } catch (err) {
      console.error("requestTranscriberStatus failed:", err);
      return;
    }
    if (!st) return;

    var activeEl = row.querySelector("[data-tr-active]");
    var modelEl  = row.querySelector("[data-tr-model]");
    var actEl    = row.querySelector("[data-tr-action]");
    var pillEl   = row.querySelector("[data-tr-mode-pill]");

    // Big visible mode pill — "Cloud" or "Local" — so the user can answer
    // "is this online or offline right now?" without parsing the label.
    var isCloud = (st.backend === "cloud");
    var isLocal = (st.backend === "mlx" || st.backend === "whisper_cpp");
    if (pillEl) {
      if (isCloud) {
        pillEl.textContent = "ONLINE";
        pillEl.style.background = "rgba(255,255,255,.06)";
        pillEl.style.color = "var(--text, #efeae4)";
      } else if (isLocal) {
        pillEl.textContent = "OFFLINE";
        pillEl.style.background = "rgba(235,182,56,.18)";
        pillEl.style.color = "var(--accent, #ebb638)";
      } else {
        pillEl.textContent = "—";
        pillEl.style.background = "rgba(255,255,255,.04)";
        pillEl.style.color = "var(--ink-dim, #888f8a)";
      }
    }

    activeEl.textContent = "Active: " + (st.active_label || "Unknown")
      + (isCloud ? " · audio leaves your Mac" : "")
      + (isLocal ? " · audio stays on your Mac" : "");

    var modelText;
    if (st.local_ready) {
      modelText = "Offline Whisper model: ready on disk"
                + (isCloud ? " (used as fallback when network drops)." : ".");
    } else {
      modelText = "Offline Whisper model: not downloaded "
                + "(~1.5 GB). "
                + (isCloud
                    ? "Cloud is doing the work right now; download to also work offline."
                    : "Current backend can't transcribe until the model is on disk.");
    }
    modelEl.textContent = modelText;

    actEl.innerHTML = "";
    var prog = st.download_progress || {};
    if (st.downloading) {
      var pct = (prog.bytes_total > 0)
        ? Math.round(100 * prog.bytes_done / prog.bytes_total)
        : null;
      var p = document.createElement("div");
      p.style.cssText =
        "font-family:var(--mono);font-size:11px;color:var(--ink);"
        + "text-align:right;line-height:1.5;";
      p.innerHTML =
        '<div>' + esc(prog.msg || "Downloading…") + '</div>'
        + '<div>' + (pct == null
                      ? esc(fmtBytes(prog.bytes_done))
                      : pct + "% · " + esc(fmtBytes(prog.bytes_done))
                          + " / " + esc(fmtBytes(prog.bytes_total)))
        + '</div>';
      actEl.appendChild(p);
      // Poll for progress.
      setTimeout(paintTranscriberStatusRow, 1500);
    } else if (st.local_ready) {
      var ok = document.createElement("span");
      ok.className = "badge transcript";
      ok.innerHTML = '<span class="bdot"></span>Ready';
      actEl.appendChild(ok);
    } else {
      var btn = document.createElement("button");
      btn.textContent = "Download offline model";
      btn.style.cssText =
        "appearance:none;-webkit-appearance:none;cursor:pointer;"
        + "background:var(--accent,#ebb638);color:#050505;border:none;"
        + "padding:8px 14px;border-radius:8px;font-family:var(--mono);"
        + "font-size:12px;font-weight:600;";
      btn.addEventListener("click", function () {
        btn.disabled = true;
        btn.textContent = "Starting…";
        window.lumo.call("downloadOfflineModel").then(function (res) {
          if (!res || !res.ok) {
            alert("Couldn't start download: "
                  + ((res && res.error) || "unknown"));
            btn.disabled = false;
            btn.textContent = "Download offline model";
            return;
          }
          paintTranscriberStatusRow();
        }).catch(function (err) {
          alert("Download threw: " + err);
          btn.disabled = false;
          btn.textContent = "Download offline model";
        });
      });
      actEl.appendChild(btn);
      if (prog.error) {
        var errN = document.createElement("div");
        errN.style.cssText =
          "font-family:var(--mono);font-size:11px;color:#e58a8a;"
          + "max-width:200px;text-align:right;line-height:1.4;";
        errN.textContent = String(prog.error);
        actEl.appendChild(errN);
      }
    }
  }

  function init() {
    loadSettings();
    paintTranscriberStatusRow();
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();
