// ============================================================
// NOTES — per-user personal scratchpad under Clinical Tools.
// Rich-text via the app-wide Tiptap bundle (window.Tiptap).
// CRUD against /api/notes (auth + server-side encryption).
//
// Three panes — list / reader / editor. After opening or saving
// a note, the reader shows a clean, read-only rendering; tap
// Edit to switch to the Tiptap editor. Autosaves during edit
// (1.2s quiet period) so you never lose progress if you close
// the tab or the browser crashes.
//
// On mobile (<900px) only one pane is visible at a time — the
// layout's data-view attribute controls which one. Desktop
// always shows the sidebar + the right pane together.
// ============================================================

(function() {
  var _inited = false;
  var _notes = [];             // active notes (deleted_at IS NULL)
  var _trash = [];             // trashed notes (deleted_at IS NOT NULL)
  var _pane = 'active';        // 'active' | 'trash' — drives the list view
  var _activeId = null;        // currently open note id (reader OR editor)
  var _editor = null;          // Tiptap instance
  var _search = '';

  // Autosave state
  var _dirty = false;
  var _autosaveTimer = null;
  var _autosaveInFlight = false;
  var _pendingAfterInflight = false;
  var AUTOSAVE_DEBOUNCE = 1200;

  // Voice recording state
  var _recorder = null;
  var _recTimer = null;
  var _recPaused = false;
  var _recActive = false;

  var _statusTimer = null;

  // Wait for the Notes tab to be activated — the component HTML is
  // lazy-loaded, so elements don't exist until the user clicks the tab.
  document.addEventListener('tabChanged', function(e) {
    if (e.detail.tab === 'notes') {
      if (_inited) { refreshList(); return; }
      _inited = true;
      init();
      return;
    }
    // Leaving the Notes tab — cancel any in-flight recording so the
    // mic doesn't stay live + the recorder doesn't keep buffering.
    if (_recActive) stopRecording(true);
    flushAutosave();
  });

  function $(id) { return document.getElementById(id); }

  function init() {
    var newBtn      = $('btn-notes-new');
    var searchEl    = $('notes-search');
    var titleEl     = $('note-title');
    var listEl      = $('notes-list');
    if (!newBtn || !listEl) return;

    // List + sidebar
    newBtn.addEventListener('click', function() { startCreate(); });
    searchEl.addEventListener('input', function() {
      _search = searchEl.value.trim().toLowerCase();
      renderList();
    });
    listEl.addEventListener('click', function(e) {
      // Trash-view actions get handled by their own classes; route them
      // first so a click on Restore or × doesn't also open the note.
      var restoreBtn = e.target.closest('.notes-restore-btn');
      if (restoreBtn) {
        e.stopPropagation();
        restoreNote(parseInt(restoreBtn.dataset.id));
        return;
      }
      var hardBtn = e.target.closest('.notes-hard-delete-btn');
      if (hardBtn) {
        e.stopPropagation();
        hardDeleteNote(parseInt(hardBtn.dataset.id));
        return;
      }
      var item = e.target.closest('.notes-list-item');
      if (!item) return;
      var id = parseInt(item.dataset.id);
      if (!id) return;
      if (_pane === 'active') openReader(id);
      // In trash, clicking the row body is a no-op — explicit Restore /
      // delete-forever buttons are the only actions, so a stray click
      // can't accidentally open a stale draft.
    });

    // Active <-> Trash tab toggle
    var tabActive = $('btn-notes-tab-active');
    var tabTrash  = $('btn-notes-tab-trash');
    if (tabActive) tabActive.addEventListener('click', function() { switchPane('active'); });
    if (tabTrash)  tabTrash.addEventListener('click',  function() { switchPane('trash'); });

    var emptyTrashBtn = $('btn-notes-trash-empty');
    if (emptyTrashBtn) emptyTrashBtn.addEventListener('click', emptyTrash);

    // Empty-state CTA
    var newFromEmpty = $('btn-notes-new-empty');
    if (newFromEmpty) newFromEmpty.addEventListener('click', function() { startCreate(); });

    // Reader pane
    var editBtn = $('btn-note-edit');
    var delFromRead = $('btn-note-delete-read');
    var backFromRead = $('btn-notes-reader-back');
    if (editBtn)       editBtn.addEventListener('click', function() { if (_activeId != null) openEditor(_activeId); });
    if (delFromRead)   delFromRead.addEventListener('click', deleteActive);
    if (backFromRead)  backFromRead.addEventListener('click', closeToList);

    // Editor pane
    var saveBtn     = $('btn-note-save');
    var delBtn      = $('btn-note-delete');
    var backFromEdit = $('btn-notes-editor-back');
    if (saveBtn)       saveBtn.addEventListener('click', function() { saveNote({ source: 'manual', returnToReader: true }); });
    if (delBtn)        delBtn.addEventListener('click', deleteActive);
    if (backFromEdit)  backFromEdit.addEventListener('click', backFromEditor);

    titleEl.addEventListener('input', function() {
      _dirty = true; updateStatus('Saving…', 'saving'); scheduleAutosave();
    });

    // Voice recording controls. addEventListener passes the click Event
    // as the first argument — wrap so the truthy Event doesn't get passed
    // to stopRecording(silent) and trigger the silent-cancel branch
    // (which shuts the mic without transcribing → "Stop does nothing").
    var recStart = $('btn-note-rec-start');
    var recPause = $('btn-note-rec-pause');
    var recStop  = $('btn-note-rec-stop');
    if (recStart) recStart.addEventListener('click', function() { startRecording(); });
    if (recPause) recPause.addEventListener('click', function() { togglePauseRecording(); });
    if (recStop)  recStop.addEventListener('click',  function() { stopRecording(false); });

    // Ctrl/Cmd+S still saves manually from the editor
    document.addEventListener('keydown', function(e) {
      var tab = $('notes-tab');
      if (!tab || !tab.classList.contains('active')) return;
      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        if (!$('notes-editor').classList.contains('hidden')) {
          e.preventDefault();
          saveNote({ source: 'shortcut', returnToReader: true });
        }
      }
    });

    // Persist dirty state on tab close / page unload — best-effort final flush.
    // navigator.sendBeacon is POST-only so it can't drive a PUT update; fetch
    // with keepalive:true is the right primitive here (works for both POST
    // for new notes and PUT for existing).
    window.addEventListener('beforeunload', function() {
      if (!_dirty) return;
      var title = getTitle();
      if (!title) return;
      var body = getBody();
      var isNew = _activeId == null;
      try {
        window.NotesApi.saveNoteKeepalive(isNew ? null : _activeId, { title: title, body: body });
      } catch (e) {}
    });

    refreshList();
    setView('list');
  }

  // ── View management ──────────────────────────────────────
  // On mobile (<900px), only one pane visible at a time, driven
  // by the layout's data-view attribute.
  function setView(view) {
    var layout = $('notes-layout');
    if (layout) layout.setAttribute('data-view', view);

    var reader = $('notes-reader');
    var editor = $('notes-editor');
    var empty  = $('notes-empty-state');

    if (reader) reader.classList.toggle('hidden', view !== 'reader');
    if (editor) editor.classList.toggle('hidden', view !== 'editor');
    // Empty state is only the "no note picked" desktop placeholder
    if (empty)  empty.classList.toggle('hidden', view !== 'empty' && view !== 'list');
  }

  function closeToList() {
    // Any unsaved edits are already autosaved, so no discard prompt needed.
    flushAutosave();
    _activeId = null;
    _dirty = false;
    if (_editor) { _editor.destroy(); _editor = null; }
    stopRecording(true); // silent cancel if in flight
    setView('list');
    renderList();
  }

  function backFromEditor() {
    flushAutosave();
    if (_activeId != null) openReader(_activeId);
    else closeToList();
  }

  // ── Data fetching ─────────────────────────────────────────
  // Always pulls both lists in parallel so the trash-count badge stays
  // current regardless of which pane the user has open.
  function refreshList() {
    var listEl = $('notes-list');
    if (!listEl) return;
    listEl.innerHTML = '<div class="notes-empty">Loading…</div>';
    Promise.all([
      window.NotesApi.listActive(),
      window.NotesApi.listTrash(),
    ])
      .then(function(results) {
        var active = results[0], trash = results[1];
        if (active && active.success) _notes = active.notes || [];
        if (trash  && trash.success)  _trash = trash.notes  || [];
        renderList();
        updateTrashCount();
      })
      .catch(function(err) {
        listEl.innerHTML = '<div class="notes-empty">' + esc(err.message || 'Load failed') + '</div>';
      });
  }

  function switchPane(pane) {
    if (pane === _pane) return;
    _pane = pane;
    var tabA = $('btn-notes-tab-active');
    var tabT = $('btn-notes-tab-trash');
    if (tabA) tabA.classList.toggle('active', pane === 'active');
    if (tabT) tabT.classList.toggle('active', pane === 'trash');
    var foot = $('notes-trash-foot');
    if (foot) foot.classList.toggle('hidden', pane !== 'trash');
    // Switching to trash clears any open editor — trashed notes are
    // read-only previews, opening them is intentionally disabled.
    if (pane === 'trash') closeToList();
    renderList();
  }

  function updateTrashCount() {
    var el = $('notes-trash-count');
    if (!el) return;
    el.textContent = _trash.length ? '(' + _trash.length + ')' : '';
  }

  function renderList() {
    var listEl = $('notes-list');
    if (!listEl) return;
    var source = _pane === 'trash' ? _trash : _notes;
    var filtered = source;
    if (_search) {
      filtered = source.filter(function(n) {
        var hay = (n.title + ' ' + stripTags(n.body || '')).toLowerCase();
        return hay.indexOf(_search) !== -1;
      });
    }
    if (filtered.length === 0) {
      listEl.innerHTML = '<div class="notes-empty">'
        + (_search ? 'No matches' : (_pane === 'trash' ? 'Trash is empty' : ''))
        + '</div>';
      return;
    }
    listEl.innerHTML = filtered.map(function(n) {
      var snippet = stripTags(n.body || '').substring(0, 110);
      var when = formatWhen(_pane === 'trash' ? n.deleted_at : n.updated_at);
      var active = (n.id === _activeId && _pane === 'active') ? ' active' : '';
      var trashActions = _pane === 'trash'
        ? '<div class="notes-trash-actions">'
            + '<button type="button" class="notes-restore-btn" data-id="' + n.id + '" title="Restore">'
            +   '<i class="fas fa-rotate-left"></i> Restore'
            + '</button>'
            + '<button type="button" class="notes-hard-delete-btn" data-id="' + n.id + '" title="Delete forever">'
            +   '<i class="fas fa-times"></i>'
            + '</button>'
          + '</div>'
        : '';
      return '<div class="notes-list-row">'
        +   '<button type="button" class="notes-list-item' + active + '" data-id="' + n.id + '">'
        +     '<div class="notes-list-title">' + esc(n.title || 'Untitled') + '</div>'
        +     '<div class="notes-list-snippet">' + esc(snippet) + '</div>'
        +     '<div class="notes-list-when">'
        +       (_pane === 'trash' ? 'Deleted ' : '') + esc(when)
        +     '</div>'
        +   '</button>'
        +   trashActions
        + '</div>';
    }).join('');
  }

  function restoreNote(id) {
    window.NotesApi.restore(id)
      .then(function(data) {
        if (!data.success) { if (typeof showToast === 'function') showToast(data.error || 'Restore failed', 'error'); return; }
        if (typeof showToast === 'function') showToast('Note restored', 'success');
        refreshList();
      })
      .catch(function(err) {
        if (typeof showToast === 'function') showToast(err.message || 'Restore failed', 'error');
      });
  }

  function hardDeleteNote(id) {
    if (typeof showConfirm !== 'function') return;
    showConfirm('Delete forever? This cannot be undone.', function() {
      window.NotesApi.hardDelete(id)
        .then(function(data) {
          if (!data.success) { if (typeof showToast === 'function') showToast(data.error || 'Delete failed', 'error'); return; }
          refreshList();
        });
    }, { danger: true, confirmText: 'Delete forever' });
  }

  function emptyTrash() {
    if (typeof showConfirm !== 'function') return;
    if (_trash.length === 0) return;
    showConfirm('Empty the trash? This will permanently delete ' + _trash.length + ' note' + (_trash.length === 1 ? '' : 's') + '.',
      function() {
        window.NotesApi.emptyTrash()
          .then(function(data) {
            if (!data.success) { if (typeof showToast === 'function') showToast(data.error || 'Empty trash failed', 'error'); return; }
            if (typeof showToast === 'function') showToast('Trash emptied', 'success');
            refreshList();
          });
      },
      { danger: true, confirmText: 'Empty trash' });
  }

  // ── Reader mode (default after opening or saving) ─────────
  function openReader(id) {
    flushAutosave();
    if (_recActive) stopRecording(true);
    var note = _notes.find(function(n) { return n.id === id; });
    if (!note) return;
    _activeId = id;
    _dirty = false;

    // Tear down the editor if it was open
    if (_editor) { _editor.destroy(); _editor = null; }

    $('notes-reader-title').textContent = note.title || 'Untitled';
    $('notes-reader-meta').textContent  = 'Created ' + formatWhen(note.created_at) + ' · Updated ' + formatWhen(note.updated_at);

    var body = $('notes-reader-body');
    body.innerHTML = note.body ? sanitizeHtml(note.body) : '';

    setView('reader');
    renderList();
  }

  // ── Editor mode (create new / edit existing) ──────────────
  function startCreate() {
    flushAutosave();
    if (_recActive) stopRecording(true);
    _activeId = null;
    _dirty = false;
    $('note-title').value = '';
    $('note-meta').textContent = '';
    $('btn-note-delete').style.display = 'none';
    mountTiptap($('note-body-editor'), '');
    updateStatus('', '');
    setRecUI('idle');
    setView('editor');
    setTimeout(function() { $('note-title').focus(); }, 20);
    renderList();
  }

  function openEditor(id) {
    var note = _notes.find(function(n) { return n.id === id; });
    if (!note) return;
    _activeId = id;
    _dirty = false;

    $('note-title').value = note.title || '';
    $('note-meta').textContent = 'Created ' + formatWhen(note.created_at) + ' · Updated ' + formatWhen(note.updated_at);
    $('btn-note-delete').style.display = '';
    mountTiptap($('note-body-editor'), note.body || '');
    updateStatus('', '');
    setRecUI('idle');
    setView('editor');
    renderList();
  }

  function getTitle() { return ($('note-title').value || '').trim(); }
  function getBody()  {
    if (!_editor) {
      // Fallback textarea if Tiptap didn't load
      var fb = document.querySelector('.notes-body-fallback');
      return fb ? fb.value : '';
    }
    var html = _editor.getHTML();
    return (html === '<p></p>' || html === '') ? '' : html;
  }

  // ── Save + autosave ──────────────────────────────────────
  function scheduleAutosave() {
    if (_autosaveTimer) clearTimeout(_autosaveTimer);
    _autosaveTimer = setTimeout(function() {
      _autosaveTimer = null;
      saveNote({ source: 'auto' });
    }, AUTOSAVE_DEBOUNCE);
  }

  function flushAutosave() {
    if (_autosaveTimer) { clearTimeout(_autosaveTimer); _autosaveTimer = null; }
    // Flush dirty state regardless of whether the note is new or existing.
    // saveNote() handles both paths (POST for new, PUT for existing), so
    // gating on _activeId here silently dropped the very first flush for
    // a brand-new note — e.g. tap New note → type → tap Back.
    if (_dirty) saveNote({ source: 'flush' });
  }

  function saveNote(opts) {
    opts = opts || {};
    var title = getTitle();
    var body  = getBody();

    // Title is required — but for autosave, don't nag every keystroke.
    if (!title) {
      if (opts.source === 'manual' || opts.source === 'shortcut') {
        updateStatus('Title required', 'err');
        $('note-title').focus();
      }
      return;
    }

    if (_autosaveInFlight) {
      // Another save is already pending the network — mark so we
      // fire one more right after it completes to capture the newest
      // state. Prevents "the last keystroke didn't save" races.
      _pendingAfterInflight = true;
      return;
    }

    var isNew = _activeId == null;

    _autosaveInFlight = true;
    updateStatus('Saving…', 'saving');

    window.NotesApi.saveNote(isNew ? null : _activeId, { title: title, body: body })
      .then(function(data) {
        _autosaveInFlight = false;
        if (!data.success) { updateStatus(data.error || 'Save failed', 'err'); return; }
        if (isNew && data.id) _activeId = data.id;
        _dirty = false;
        updateStatus('Saved', 'ok');

        // Refresh the list so the new/updated note appears with correct
        // timestamps + ordering.
        return window.NotesApi.listActive()
          .then(function(d) {
            if (d.success) _notes = d.notes || [];
            var note = _notes.find(function(n) { return n.id === _activeId; });
            if (note) {
              $('note-meta').textContent = 'Created ' + formatWhen(note.created_at) + ' · Updated ' + formatWhen(note.updated_at);
            }
            renderList();
          })
          .then(function() {
            // If the user typed more while the save was in flight, fire once more.
            if (_pendingAfterInflight) {
              _pendingAfterInflight = false;
              _dirty = true;
              scheduleAutosave();
            } else if (opts.returnToReader) {
              // Manual save → show the reader view with the saved content.
              openReader(_activeId);
            }
          });
      })
      .catch(function(err) {
        _autosaveInFlight = false;
        updateStatus(err.message || 'Save failed', 'err');
      });
  }

  // ── Delete ──────────────────────────────────────────────
  function deleteActive() {
    if (_activeId == null) return;
    var note = _notes.find(function(n) { return n.id === _activeId; });
    var label = (note && note.title) ? note.title : 'this note';

    // Soft-delete: row gets deleted_at timestamp and moves to Trash.
    // The user can Restore from there or Empty trash to actually erase.
    var doMove = function() {
      flushAutosave();
      window.NotesApi.moveToTrash(_activeId)
        .then(function(data) {
          if (!data.success) { updateStatus(data.error || 'Move to trash failed', 'err'); return; }
          _notes = _notes.filter(function(n) { return n.id !== _activeId; });
          _activeId = null;
          _dirty = false;
          if (_editor) { _editor.destroy(); _editor = null; }
          closeToList();
          if (typeof showToast === 'function') showToast('Moved to trash', 'success');
          refreshList();
        })
        .catch(function(err) { updateStatus(err.message || 'Move to trash failed', 'err'); });
    };

    if (typeof showConfirm === 'function') {
      showConfirm('Move "' + label + '" to trash?', doMove, { confirmText: 'Move to trash' });
    }
  }

  // ── Voice recording → AI note ──────────────────────────────
  function startRecording() {
    if (_recActive) return;
    // Ensure the editor is open — voice always targets the editor
    if ($('notes-editor').classList.contains('hidden')) startCreate();
    if (typeof AudioRecorder === 'undefined') { updateStatus('Recorder unavailable', 'err'); return; }
    _recorder = new AudioRecorder();
    _recorder.start().then(function() {
      _recActive = true; _recPaused = false;
      setRecUI('recording');
      startRecTimer();
    }).catch(function(err) {
      updateStatus('Mic denied: ' + (err && err.message || ''), 'err');
    });
  }

  function togglePauseRecording() {
    if (!_recActive || !_recorder || !_recorder.mediaRecorder) return;
    if (!_recPaused) {
      try { _recorder.mediaRecorder.pause(); } catch (e) {}
      _recPaused = true; stopRecTimer(); setRecUI('paused');
    } else {
      try { _recorder.mediaRecorder.resume(); } catch (e) {
        // Browser doesn't support resume — start a fresh segment.
        try {
          if (_recorder.stream && _recorder.stream.active) {
            var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
            _recorder.mediaRecorder = new MediaRecorder(_recorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
            _recorder.mediaRecorder.ondataavailable = function(ev) { if (ev.data.size > 0) _recorder.chunks.push(ev.data); };
            _recorder.mediaRecorder.start(1000);
          }
        } catch (e2) {}
      }
      _recPaused = false; startRecTimer(); setRecUI('recording');
    }
  }

  function stopRecording(silent) {
    if (!_recActive || !_recorder) {
      if (!silent) setRecUI('idle');
      return;
    }
    _recActive = false; _recPaused = false;
    stopRecTimer(true);
    if (silent) {
      // Cancel cleanly — close the stream, don't transcribe.
      try { _recorder.mediaRecorder && _recorder.mediaRecorder.state !== 'inactive' && _recorder.mediaRecorder.stop(); } catch (e) {}
      try { _recorder.stream && _recorder.stream.getTracks().forEach(function(t) { t.stop(); }); } catch (e) {}
      setRecUI('idle');
      return;
    }
    setRecUI('processing');
    updateStatus('Transcribing…', 'saving');

    _recorder.stop().then(function(blob) {
      if (!blob || blob.size === 0) { updateStatus('Nothing recorded', 'err'); setRecUI('idle'); return; }
      if (typeof transcribeAudio !== 'function') { updateStatus('Transcription unavailable', 'err'); setRecUI('idle'); return; }
      return transcribeAudio(blob).then(function(resp) {
        if (!resp || !resp.success || !resp.text) {
          var msg = (resp && (resp.error || (resp.noProvider ? 'No STT provider configured' : null))) || 'Transcription failed';
          updateStatus(msg, 'err'); setRecUI('idle'); return;
        }
        updateStatus('Generating note…', 'saving');
        var modelEl = document.getElementById('notes-model-select');
        var selectedModel = modelEl ? modelEl.value : '';
        return window.NotesApi.noteFromVoice({ transcript: resp.text, model: selectedModel || undefined })
          .then(function(data) {
            setRecUI('idle');
            if (!data.success) { updateStatus(data.error || 'Generation failed', 'err'); return; }
            applyGeneratedNote(data.title || 'Voice note', data.body || '');
            updateStatus('Generated — autosaving…', 'ok');
            // Trigger the autosave so the AI-generated note is persisted
            // even if the user walks away.
            _dirty = true; scheduleAutosave();
          });
      });
    }).catch(function(err) {
      setRecUI('idle');
      updateStatus(err.message || 'Recording failed', 'err');
    });
  }

  function applyGeneratedNote(title, body) {
    var titleEl = $('note-title');
    var container = $('note-body-editor');
    if (!titleEl || !container) {
      console.error('[notes] applyGeneratedNote: editor not mounted; title/container missing');
      return;
    }
    titleEl.value = title || 'Voice note';

    // Prefer Tiptap's native commands.setContent over a remount when an
    // editor already exists — remounting on every voice take left the
    // toolbar reattaching, which has caused flicker + a brief window
    // where the body looked empty.
    if (_editor && typeof _editor.commands === 'object') {
      try {
        _editor.commands.setContent(body || '<p></p>', { emitUpdate: true });
        _dirty = true;
        return;
      } catch (err) {
        console.warn('[notes] Tiptap setContent failed, rebuilding:', err && err.message);
      }
    }
    if (_editor) { try { _editor.destroy(); } catch (e) {} _editor = null; }
    mountTiptap(container, body || '<p></p>');
    _dirty = true;
  }

  function startRecTimer() {
    var el = $('notes-rec-timer');
    if (_recTimer || !el) return;
    _recTimer = createTimerLite(el);
    _recTimer.start();
  }
  function stopRecTimer(reset) {
    if (_recTimer) _recTimer.stop();
    if (reset && _recTimer) { _recTimer.reset(); _recTimer = null; }
  }
  function createTimerLite(el) {
    var s = 0, iv = null;
    function paint() { el.textContent = String(Math.floor(s/60)).padStart(2,'0') + ':' + String(s%60).padStart(2,'0'); }
    return {
      start: function() { paint(); if (!iv) iv = setInterval(function() { s++; paint(); }, 1000); },
      stop:  function() { if (iv) { clearInterval(iv); iv = null; } },
      reset: function() { s = 0; paint(); },
    };
  }

  function setRecUI(state) {
    var start   = $('btn-note-rec-start');
    var pause   = $('btn-note-rec-pause');
    var stopBtn = $('btn-note-rec-stop');
    var ind     = $('notes-rec-indicator');
    var stateEl = $('notes-rec-state');
    var dot     = ind ? ind.querySelector('.pulse-dot') : null;
    if (!start) return;

    var idle       = (state === 'idle');
    var recording  = (state === 'recording');
    var paused     = (state === 'paused');
    var processing = (state === 'processing');

    start.classList.toggle('hidden', !idle);
    pause.classList.toggle('hidden', !(recording || paused));
    stopBtn.classList.toggle('hidden', !(recording || paused));
    ind.classList.toggle('hidden', idle);

    if (pause) pause.innerHTML = paused ? '<i class="fas fa-play"></i> Resume' : '<i class="fas fa-pause"></i> Pause';
    if (stateEl) stateEl.textContent = processing ? 'Processing…' : (paused ? 'Paused' : 'Recording');
    if (dot) dot.classList.toggle('paused', paused || processing);

    start.disabled   = !idle;
    pause.disabled   = processing;
    stopBtn.disabled = processing;
  }

  // ── Tiptap ─────────────────────────────────────────────
  function mountTiptap(container, initialHtml) {
    var T = window.Tiptap || {};
    if (!T.Editor) {
      // Fallback when the Tiptap bundle didn't load — plain textarea.
      container.innerHTML = '<textarea class="notes-body-fallback" placeholder="Write your note…">' + esc(stripTags(initialHtml)) + '</textarea>';
      _editor = null;
      var fb = container.querySelector('.notes-body-fallback');
      if (fb) fb.addEventListener('input', function() { _dirty = true; updateStatus('Saving…', 'saving'); scheduleAutosave(); });
      return;
    }
    container.innerHTML = toolbarHTML() + '<div class="tp-content"></div>';
    _editor = new T.Editor({
      element: container.querySelector('.tp-content'),
      extensions: [
        T.StarterKit,
        T.Link.configure({ openOnClick: false, autolink: true }),
        T.Underline
      ],
      content: initialHtml || '',
      autofocus: false,
      onUpdate: function() {
        _dirty = true;
        updateStatus('Saving…', 'saving');
        updateToolbarState(container);
        scheduleAutosave();
      },
      onSelectionUpdate: function() { updateToolbarState(container); }
    });
    wireToolbar(container, _editor);
  }

  function toolbarHTML() {
    var btns = ''
      + '<button type="button" class="tp-btn" data-cmd="bold" title="Bold"><i class="fas fa-bold"></i></button>'
      + '<button type="button" class="tp-btn" data-cmd="italic" title="Italic"><i class="fas fa-italic"></i></button>'
      + '<button type="button" class="tp-btn" data-cmd="underline" title="Underline"><i class="fas fa-underline"></i></button>'
      + '<button type="button" class="tp-btn" data-cmd="strike" title="Strike"><i class="fas fa-strikethrough"></i></button>'
      + '<span class="tp-sep"></span>'
      + '<button type="button" class="tp-btn" data-cmd="h2" title="Heading 2">H2</button>'
      + '<button type="button" class="tp-btn" data-cmd="h3" title="Heading 3">H3</button>'
      + '<span class="tp-sep"></span>'
      + '<button type="button" class="tp-btn" data-cmd="bulletList" title="Bullet list"><i class="fas fa-list-ul"></i></button>'
      + '<button type="button" class="tp-btn" data-cmd="orderedList" title="Numbered list"><i class="fas fa-list-ol"></i></button>'
      + '<button type="button" class="tp-btn" data-cmd="blockquote" title="Quote"><i class="fas fa-quote-left"></i></button>'
      + '<button type="button" class="tp-btn" data-cmd="codeBlock" title="Code"><i class="fas fa-code"></i></button>'
      + '<span class="tp-sep"></span>'
      + '<button type="button" class="tp-btn" data-cmd="link" title="Link"><i class="fas fa-link"></i></button>'
      + '<span class="tp-sep"></span>'
      + '<button type="button" class="tp-btn" data-cmd="clear" title="Clear formatting"><i class="fas fa-remove-format"></i></button>';
    return ''
      + '<div class="tp-toolbar">' + btns + '</div>'
      + '<div class="tp-link-bar" style="display:none;">'
      +   '<input type="url" class="tp-link-input" placeholder="https://">'
      +   '<button type="button" class="tp-link-apply">Apply</button>'
      +   '<button type="button" class="tp-link-remove">Remove</button>'
      +   '<button type="button" class="tp-link-cancel">✕</button>'
      + '</div>';
  }

  function wireToolbar(wrap, ed) {
    var toolbar = wrap.querySelector('.tp-toolbar');
    var linkBar = wrap.querySelector('.tp-link-bar');
    var linkInput = wrap.querySelector('.tp-link-input');

    toolbar.addEventListener('mousedown', function(e) {
      var btn = e.target.closest('.tp-btn[data-cmd]');
      if (!btn) return;
      e.preventDefault();
      var cmd = btn.dataset.cmd;
      switch (cmd) {
        case 'bold':         ed.chain().focus().toggleBold().run(); break;
        case 'italic':       ed.chain().focus().toggleItalic().run(); break;
        case 'underline':    ed.chain().focus().toggleUnderline().run(); break;
        case 'strike':       ed.chain().focus().toggleStrike().run(); break;
        case 'h2':           ed.chain().focus().toggleHeading({ level: 2 }).run(); break;
        case 'h3':           ed.chain().focus().toggleHeading({ level: 3 }).run(); break;
        case 'bulletList':   ed.chain().focus().toggleBulletList().run(); break;
        case 'orderedList':  ed.chain().focus().toggleOrderedList().run(); break;
        case 'blockquote':   ed.chain().focus().toggleBlockquote().run(); break;
        case 'codeBlock':    ed.chain().focus().toggleCodeBlock().run(); break;
        case 'clear':        ed.chain().focus().unsetAllMarks().clearNodes().run(); break;
        case 'link':
          if (linkBar.style.display === 'none') {
            linkInput.value = ed.getAttributes('link').href || '';
            linkBar.style.display = 'flex';
            setTimeout(function() { linkInput.focus(); }, 0);
          } else {
            linkBar.style.display = 'none';
          }
          break;
      }
      updateToolbarState(wrap);
    });

    wrap.querySelector('.tp-link-apply').addEventListener('mousedown', function(e) {
      e.preventDefault();
      var url = linkInput.value.trim();
      if (url) ed.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
      linkBar.style.display = 'none';
    });
    wrap.querySelector('.tp-link-remove').addEventListener('mousedown', function(e) {
      e.preventDefault();
      ed.chain().focus().unsetLink().run();
      linkBar.style.display = 'none';
    });
    wrap.querySelector('.tp-link-cancel').addEventListener('mousedown', function(e) {
      e.preventDefault();
      linkBar.style.display = 'none';
    });
    linkInput.addEventListener('keydown', function(e) {
      if (e.key === 'Enter')  { e.preventDefault(); wrap.querySelector('.tp-link-apply').dispatchEvent(new MouseEvent('mousedown')); }
      if (e.key === 'Escape') { linkBar.style.display = 'none'; }
    });
  }

  function updateToolbarState(wrap) {
    if (!_editor) return;
    wrap.querySelectorAll('.tp-btn[data-cmd]').forEach(function(btn) {
      var cmd = btn.dataset.cmd;
      var active = false;
      if      (cmd === 'bold')        active = _editor.isActive('bold');
      else if (cmd === 'italic')      active = _editor.isActive('italic');
      else if (cmd === 'underline')   active = _editor.isActive('underline');
      else if (cmd === 'strike')      active = _editor.isActive('strike');
      else if (cmd === 'h2')          active = _editor.isActive('heading', { level: 2 });
      else if (cmd === 'h3')          active = _editor.isActive('heading', { level: 3 });
      else if (cmd === 'bulletList')  active = _editor.isActive('bulletList');
      else if (cmd === 'orderedList') active = _editor.isActive('orderedList');
      else if (cmd === 'blockquote')  active = _editor.isActive('blockquote');
      else if (cmd === 'codeBlock')   active = _editor.isActive('codeBlock');
      else if (cmd === 'link')        active = _editor.isActive('link');
      btn.classList.toggle('active', active);
    });
  }

  // ── Helpers ──────────────────────────────────────────────
  function updateStatus(text, kind) {
    var el = $('notes-status');
    if (!el) return;
    el.textContent = text || '';
    el.className = 'notes-status' + (kind ? ' notes-status-' + kind : '');
    if (_statusTimer) { clearTimeout(_statusTimer); _statusTimer = null; }
    if (kind === 'ok') {
      _statusTimer = setTimeout(function() { el.textContent = ''; el.className = 'notes-status'; }, 2000);
    }
  }

  function esc(s) {
    return (s == null ? '' : String(s))
      .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
  }
  function stripTags(html) {
    if (!html) return '';
    var d = document.createElement('div'); d.innerHTML = html;
    return (d.textContent || d.innerText || '').replace(/\s+/g, ' ').trim();
  }
  // Sanitize via DOMPurify (loaded from cdnjs in index.html, also used by
  // learningHub.js). DOMPurify's allowlist + attribute filter is the right
  // primitive — homegrown regex/walker sanitizers historically have bypasses.
  // If DOMPurify somehow fails to load, refuse to render HTML rather than
  // falling back to a hand-rolled walker.
  function sanitizeHtml(html) {
    if (!window.DOMPurify || typeof window.DOMPurify.sanitize !== 'function') {
      console.warn('[notes] DOMPurify unavailable — rendering as plain text.');
      var d = document.createElement('div');
      d.textContent = String(html == null ? '' : html);
      return d.innerHTML;
    }
    return window.DOMPurify.sanitize(html, {
      ALLOWED_TAGS: ['p','br','strong','em','b','i','u','s','h2','h3','h4',
        'ul','ol','li','a','blockquote','code','pre','hr'],
      ALLOWED_ATTR: ['href','target','rel'],
      ADD_ATTR: ['target'],
      FORBID_ATTR: ['style','onerror','onload','onclick','onmouseover'],
      ALLOW_DATA_ATTR: false
    });
  }
  function formatWhen(iso) {
    if (!iso) return '';
    var d = new Date(iso);
    if (isNaN(d.getTime())) return '';
    var now = new Date();
    var sameDay = d.toDateString() === now.toDateString();
    if (sameDay) return d.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
    return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: d.getFullYear() === now.getFullYear() ? undefined : 'numeric' });
  }
})();
