/**
 * Notes — Surface Entry Point (v0.5.0)
 *
 * Markdown notes surface using the SDK:
 *   sw.api.ext('notes')  — scoped API client
 *   sw.ui.*              — primitive components
 *   sw.shell.Topbar      — navigation bar
 *   window.CM            — CodeMirror 6 bundle (optional)
 */
(async function () {
  'use strict';

  var mount = document.getElementById('extension-mount');
  if (!mount) return;

  var base = window.__BASE__ || '';
  var ver  = window.__VERSION__ || '0';

  // ── Dynamic Script Loader ────────────────────
  function _loadScript(src) {
    return new Promise(function(resolve, reject) {
      if (document.querySelector('script[src*="' + src.split('?')[0] + '"]')) {
        resolve();
        return;
      }
      var s = document.createElement('script');
      s.src = base + src;
      s.onload = resolve;
      s.onerror = function() { resolve(); }; // Non-fatal
      document.head.appendChild(s);
    });
  }

  // ── Boot SDK ───────────────────────────────
  try {
    if (!window.preact) {
      var { h, render } = await import(base + '/js/sw/vendor/preact.module.js');
      var hooksModule   = await import(base + '/js/sw/vendor/hooks.module.js');
      var htmModule     = await import(base + '/js/sw/vendor/htm.module.js');
      window.preact = { h, render };
      window.hooks  = hooksModule;
      window.html   = htmModule.default.bind(h);
    }
    var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver);
    await sdk.boot();
  } catch (e) {
    mount.innerHTML = '<p style="color:var(--danger);padding:24px;">SDK boot failed: ' + e.message + '</p>';
    return;
  }

  // ── Load CM6 bundle (optional — falls back to textarea) ──
  await _loadScript('/vendor/codemirror/codemirror.bundle.js?v=' + ver);
  var hasCM = !!(window.CM && window.CM.noteEditor);

  var { html } = window;
  var { useState, useEffect, useCallback, useRef, useMemo } = hooks;
  var { render } = preact;

  // ── SDK modules ────────────────────────────
  var api = sw.api.ext('notes');
  var { Button, Spinner } = sw.ui;
  var Topbar = sw.shell.Topbar;

  // ── Debounce helper ────────────────────────
  var _timers = {};
  function debounce(key, fn, ms) {
    clearTimeout(_timers[key]);
    _timers[key] = setTimeout(fn, ms);
  }

  // ═══════════════════════════════════════════
  //  Markdown renderer (inline, no deps)
  // ═══════════════════════════════════════════

  function renderMarkdown(src) {
    if (!src) return '';
    var lines = src.split('\n');
    var out = [];
    var inCode = false;
    var inList = false;
    var listTag = '';

    for (var i = 0; i < lines.length; i++) {
      var line = lines[i];

      // fenced code blocks
      if (line.trim().startsWith('```')) {
        if (inCode) {
          out.push('</code></pre>');
          inCode = false;
        } else {
          if (inList) { out.push('</' + listTag + '>'); inList = false; }
          out.push('<pre><code>');
          inCode = true;
        }
        continue;
      }
      if (inCode) {
        out.push(escHtml(line) + '\n');
        continue;
      }

      // blank line
      if (!line.trim()) {
        if (inList) { out.push('</' + listTag + '>'); inList = false; }
        continue;
      }

      // headings
      if (line.startsWith('### ')) { closeList(); out.push('<h3>' + inline(line.slice(4)) + '</h3>'); continue; }
      if (line.startsWith('## '))  { closeList(); out.push('<h2>' + inline(line.slice(3)) + '</h2>'); continue; }
      if (line.startsWith('# '))   { closeList(); out.push('<h1>' + inline(line.slice(2)) + '</h1>'); continue; }

      // hr
      if (/^[-*_]{3,}\s*$/.test(line)) { closeList(); out.push('<hr>'); continue; }

      // blockquote
      if (line.startsWith('> ')) { closeList(); out.push('<blockquote><p>' + inline(line.slice(2)) + '</p></blockquote>'); continue; }

      // unordered list
      if (/^[\-*+]\s/.test(line)) {
        if (!inList || listTag !== 'ul') { closeList(); out.push('<ul>'); inList = true; listTag = 'ul'; }
        out.push('<li>' + inline(line.replace(/^[\-*+]\s/, '')) + '</li>');
        continue;
      }

      // ordered list
      if (/^\d+\.\s/.test(line)) {
        if (!inList || listTag !== 'ol') { closeList(); out.push('<ol>'); inList = true; listTag = 'ol'; }
        out.push('<li>' + inline(line.replace(/^\d+\.\s/, '')) + '</li>');
        continue;
      }

      // paragraph
      closeList();
      out.push('<p>' + inline(line) + '</p>');
    }
    if (inCode) out.push('</code></pre>');
    if (inList) out.push('</' + listTag + '>');
    return out.join('\n');

    function closeList() { if (inList) { out.push('</' + listTag + '>'); inList = false; } }
  }

  function escHtml(s) {
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }

  function inline(s) {
    s = escHtml(s);
    // inline code
    s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
    // bold
    s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
    s = s.replace(/__(.+?)__/g, '<strong>$1</strong>');
    // italic
    s = s.replace(/\*(.+?)\*/g, '<em>$1</em>');
    s = s.replace(/_(.+?)_/g, '<em>$1</em>');
    // links
    s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
    // wikilinks: [[Note Title]] → clickable internal link
    s = s.replace(/\[\[([^\]]+)\]\]/g, function(match, linkText) {
      return '<a class="wikilink" data-wikilink="' + linkText.trim() + '" href="#">' + linkText.trim() + '</a>';
    });
    return s;
  }


  // ═══════════════════════════════════════════
  //  TagInput — editor tag management
  // ═══════════════════════════════════════════

  function TagInput({ tags, allTags, onChange }) {
    var [input, setInput] = useState('');
    var [showAC, setShowAC] = useState(false);
    var inputRef = useRef(null);

    var suggestions = useMemo(function() {
      if (!input.trim()) return [];
      var q = input.trim().toLowerCase();
      return (allTags || []).filter(function(t) {
        return t.toLowerCase().indexOf(q) !== -1 && (tags || []).indexOf(t) === -1;
      }).slice(0, 8);
    }, [input, allTags, tags]);

    function addTag(tag) {
      var t = tag.trim().toLowerCase();
      if (!t) return;
      var current = tags || [];
      if (current.indexOf(t) === -1) {
        onChange(current.concat([t]));
      }
      setInput('');
      setShowAC(false);
      if (inputRef.current) inputRef.current.focus();
    }

    function removeTag(tag) {
      onChange((tags || []).filter(function(t) { return t !== tag; }));
    }

    function handleKeyDown(e) {
      if (e.key === 'Enter' || e.key === ',') {
        e.preventDefault();
        if (input.trim()) addTag(input);
      }
      if (e.key === 'Backspace' && !input && tags && tags.length > 0) {
        removeTag(tags[tags.length - 1]);
      }
      if (e.key === 'Escape') {
        setShowAC(false);
      }
    }

    function handleInput(e) {
      var val = e.target.value.replace(',', '');
      setInput(val);
      setShowAC(val.trim().length > 0);
    }

    return html`
      <div class="tag-input">
        ${(tags || []).map(function(tag) {
          return html`
            <span key=${tag} class="tag-pill">
              ${tag}
              <span class="tag-pill__remove" onClick=${function() { removeTag(tag); }}>×</span>
            </span>
          `;
        })}
        <input ref=${inputRef} class="tag-input__field" type="text"
               placeholder=${(tags && tags.length) ? 'Add tag…' : 'Add tags (comma to separate)…'}
               value=${input}
               onInput=${handleInput}
               onKeyDown=${handleKeyDown}
               onFocus=${function() { if (input.trim()) setShowAC(true); }}
               onBlur=${function() { setTimeout(function() { setShowAC(false); }, 150); }} />
        ${showAC && suggestions.length > 0 && html`
          <div class="tag-autocomplete">
            ${suggestions.map(function(s) {
              return html`<div key=${s} class="tag-autocomplete__item"
                onClick=${function() { addTag(s); }}>${s}</div>`;
            })}
          </div>
        `}
      </div>
    `;
  }


  // ═══════════════════════════════════════════
  //  TagFilter — sidebar tag filter
  // ═══════════════════════════════════════════

  function TagFilter({ tags, activeTag, onSelect }) {
    if (!tags || tags.length === 0) return null;

    return html`
      <div class="tag-filter">
        <div class="tag-filter__label">
          <span>Tags</span>
          ${activeTag && html`
            <button class="tag-filter__clear" onClick=${function() { onSelect(null); }}>Clear</button>
          `}
        </div>
        <div class="tag-filter__pills">
          ${tags.map(function(tag) {
            return html`
              <span key=${tag} class="tag-pill ${activeTag === tag ? 'tag-pill--active' : ''}"
                    onClick=${function() { onSelect(activeTag === tag ? null : tag); }}>
                ${tag}
              </span>
            `;
          })}
        </div>
      </div>
    `;
  }


  // ═══════════════════════════════════════════
  //  NoteCard — sidebar list item (draggable)
  // ═══════════════════════════════════════════

  function NoteCard({ note, active, onClick }) {
    var [dragging, setDragging] = useState(false);
    var dateStr = note.updated_at || note.created_at || '';
    if (dateStr) {
      try { dateStr = new Date(dateStr).toLocaleDateString(); } catch(e) { /* keep raw */ }
    }
    var tags = note.tags || [];
    var showTags = tags.slice(0, 3);
    var overflow = tags.length - 3;

    function handleDragStart(e) {
      e.dataTransfer.setData('text/plain', note.id);
      e.dataTransfer.effectAllowed = 'move';
      setDragging(true);
    }

    function handleDragEnd() {
      setDragging(false);
    }

    return html`
      <div class="note-card ${active ? 'note-card--active' : ''} ${dragging ? 'note-card--dragging' : ''}"
           onClick=${onClick}
           draggable="true"
           onDragStart=${handleDragStart}
           onDragEnd=${handleDragEnd}>
        <div class="note-card__title">
          ${note.pinned ? html`<span class="note-card__pin">📌</span>` : null}
          ${note.title || 'Untitled'}
        </div>
        ${note.snippet && html`<div class="note-card__snippet">${note.snippet}</div>`}
        ${showTags.length > 0 && html`
          <div class="note-card__tags">
            ${showTags.map(function(t) { return html`<span key=${t} class="tag-pill">${t}</span>`; })}
            ${overflow > 0 && html`<span class="tag-pill tag-pill--overflow">+${overflow}</span>`}
          </div>
        `}
        ${dateStr && html`<div class="note-card__date">${dateStr}</div>`}
      </div>
    `;
  }


  // ═══════════════════════════════════════════
  //  FolderContextMenu — right-click menu
  // ═══════════════════════════════════════════

  function FolderContextMenu({ x, y, onRename, onDelete, onAddSub, onClose }) {
    var menuRef = useRef(null);

    useEffect(function() {
      function handleClick(e) {
        if (menuRef.current && !menuRef.current.contains(e.target)) onClose();
      }
      document.addEventListener('mousedown', handleClick);
      return function() { document.removeEventListener('mousedown', handleClick); };
    }, [onClose]);

    return html`
      <div ref=${menuRef} class="folder-context-menu" style="top: ${y}px; left: ${x}px">
        <div class="folder-context-menu__item" onClick=${onAddSub}>Add subfolder</div>
        <div class="folder-context-menu__item" onClick=${onRename}>Rename</div>
        <div class="folder-context-menu__item folder-context-menu__item--danger" onClick=${onDelete}>Delete</div>
      </div>
    `;
  }


  // ═══════════════════════════════════════════
  //  FolderNode — recursive tree node (drop target)
  // ═══════════════════════════════════════════

  function FolderNode({ node, depth, activeFolderId, onSelect, onRename, onDelete, onCreateSub, onDropNote }) {
    var [expanded, setExpanded] = useState(true);
    var [editing, setEditing]   = useState(false);
    var [editName, setEditName] = useState(node.folder.name);
    var [dropOver, setDropOver] = useState(false);
    var [menu, setMenu]         = useState(null);
    var hasChildren = node.children.length > 0;
    var isActive = activeFolderId === node.folder.id;
    var indent = 20 + depth * 16;

    function handleToggle(e) {
      e.stopPropagation();
      setExpanded(!expanded);
    }

    function handleRename() {
      if (editName.trim() && editName !== node.folder.name) {
        onRename(node.folder.id, editName.trim());
      }
      setEditing(false);
    }

    function handleContext(e) {
      e.preventDefault();
      e.stopPropagation();
      setMenu({ x: e.clientX, y: e.clientY });
    }

    function handleDragOver(e) {
      e.preventDefault();
      e.dataTransfer.dropEffect = 'move';
      setDropOver(true);
    }
    function handleDragLeave() { setDropOver(false); }
    function handleDrop(e) {
      e.preventDefault();
      setDropOver(false);
      var noteId = e.dataTransfer.getData('text/plain');
      if (noteId && onDropNote) onDropNote(noteId, node.folder.id);
    }

    return html`
      <div>
        <div class="folder-tree__item ${isActive ? 'folder-tree__item--active' : ''} ${dropOver ? 'folder-tree__item--drop-target' : ''}"
             style="padding-left: ${indent}px"
             onClick=${function() { onSelect(node.folder.id); }}
             onContextMenu=${handleContext}
             onDragOver=${handleDragOver}
             onDragLeave=${handleDragLeave}
             onDrop=${handleDrop}>
          <span class="folder-tree__toggle" onClick=${handleToggle}>
            ${hasChildren ? (expanded ? '▼' : '▶') : '·'}
          </span>
          <span class="folder-tree__icon">${isActive ? '📂' : '📁'}</span>
          ${editing
            ? html`<input class="folder-tree__edit" value=${editName}
                     onInput=${function(e) { setEditName(e.target.value); }}
                     onBlur=${handleRename}
                     onKeyDown=${function(e) { if (e.key === 'Enter') handleRename(); if (e.key === 'Escape') setEditing(false); }}
                     onClick=${function(e) { e.stopPropagation(); }}
                     ref=${function(el) { if (el) el.focus(); }} />`
            : html`<span class="folder-tree__name">${node.folder.name}</span>`
          }
        </div>
        ${menu && html`<${FolderContextMenu} x=${menu.x} y=${menu.y}
          onRename=${function() { setMenu(null); setEditing(true); setEditName(node.folder.name); }}
          onDelete=${function() { setMenu(null); onDelete(node.folder.id); }}
          onAddSub=${function() { setMenu(null); onCreateSub(node.folder.id); }}
          onClose=${function() { setMenu(null); }} />`}
        ${expanded && node.children.map(function(child) {
          return html`<${FolderNode} key=${child.folder.id} node=${child} depth=${depth + 1}
            activeFolderId=${activeFolderId}
            onSelect=${onSelect} onRename=${onRename}
            onDelete=${onDelete} onCreateSub=${onCreateSub}
            onDropNote=${onDropNote} />`;
        })}
      </div>
    `;
  }


  // ═══════════════════════════════════════════
  //  FolderTree — navigation panel (drop targets)
  // ═══════════════════════════════════════════

  function FolderTree({ folders, activeFolderId, showUnfiled, onSelectFolder, onSelectAll, onSelectUnfiled, onCreateFolder, onRenameFolder, onDeleteFolder, onDropNote }) {
    var [dropAll, setDropAll] = useState(false);
    var [dropUnfiled, setDropUnfiled] = useState(false);

    // build tree from flat list
    var tree = useMemo(function() {
      var byId = {};
      var roots = [];
      for (var i = 0; i < folders.length; i++) {
        byId[folders[i].id] = { folder: folders[i], children: [] };
      }
      for (var i = 0; i < folders.length; i++) {
        var f = folders[i];
        var node = byId[f.id];
        if (f.parent_id && byId[f.parent_id]) {
          byId[f.parent_id].children.push(node);
        } else {
          roots.push(node);
        }
      }
      function sortNodes(arr) {
        arr.sort(function(a, b) {
          if (a.folder.sort_order !== b.folder.sort_order) return a.folder.sort_order - b.folder.sort_order;
          return (a.folder.name || '').localeCompare(b.folder.name || '');
        });
        for (var j = 0; j < arr.length; j++) sortNodes(arr[j].children);
      }
      sortNodes(roots);
      return roots;
    }, [folders]);

    // drop handlers for "All Notes" / "Unfiled" (move to unfiled = folder_id "")
    function makeDropHandlers(setOver) {
      return {
        onDragOver: function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOver(true); },
        onDragLeave: function() { setOver(false); },
        onDrop: function(e) {
          e.preventDefault(); setOver(false);
          var noteId = e.dataTransfer.getData('text/plain');
          if (noteId && onDropNote) onDropNote(noteId, '');
        }
      };
    }
    var allDrop = makeDropHandlers(setDropAll);
    var unfiledDrop = makeDropHandlers(setDropUnfiled);

    return html`
      <div class="folder-tree">
        <div class="folder-tree__item ${!activeFolderId && !showUnfiled ? 'folder-tree__item--active' : ''} ${dropAll ? 'folder-tree__item--drop-target' : ''}"
             onClick=${onSelectAll}
             onDragOver=${allDrop.onDragOver} onDragLeave=${allDrop.onDragLeave} onDrop=${allDrop.onDrop}>
          <span class="folder-tree__icon">📋</span>
          <span class="folder-tree__name">All Notes</span>
        </div>
        <div class="folder-tree__item ${showUnfiled ? 'folder-tree__item--active' : ''} ${dropUnfiled ? 'folder-tree__item--drop-target' : ''}"
             onClick=${onSelectUnfiled}
             onDragOver=${unfiledDrop.onDragOver} onDragLeave=${unfiledDrop.onDragLeave} onDrop=${unfiledDrop.onDrop}>
          <span class="folder-tree__icon">📄</span>
          <span class="folder-tree__name">Unfiled</span>
        </div>
        ${tree.map(function(node) {
          return html`<${FolderNode} key=${node.folder.id} node=${node} depth=${0}
            activeFolderId=${activeFolderId}
            onSelect=${onSelectFolder} onRename=${onRenameFolder}
            onDelete=${onDeleteFolder} onCreateSub=${onCreateFolder}
            onDropNote=${onDropNote} />`;
        })}
        <div class="folder-tree__add" onClick=${function() { onCreateFolder(''); }}>
          + New Folder
        </div>
      </div>
    `;
  }


  // ═══════════════════════════════════════════
  //  BacklinksPanel — incoming wikilinks
  // ═══════════════════════════════════════════

  function BacklinksPanel({ noteId, onNavigate }) {
    var [backlinks, setBacklinks] = useState([]);
    var [collapsed, setCollapsed] = useState(false);

    useEffect(function() {
      if (!noteId) { setBacklinks([]); return; }
      api.get('/backlinks/' + noteId).then(function(res) {
        var items = (res && res.data) || res || [];
        setBacklinks(Array.isArray(items) ? items : []);
      }).catch(function() { setBacklinks([]); });
    }, [noteId]);

    if (!backlinks.length) return null;

    return html`
      <div class="backlinks-panel">
        <div class="backlinks-panel__header" onClick=${function() { setCollapsed(!collapsed); }}>
          <span class="backlinks-panel__toggle">${collapsed ? '▶' : '▼'}</span>
          <span>Backlinks</span>
          <span class="backlinks-panel__count">${backlinks.length}</span>
        </div>
        ${!collapsed && backlinks.map(function(bl) {
          return html`
            <div key=${bl.source_id} class="backlinks-panel__item"
                 onClick=${function() { if (onNavigate) onNavigate(bl.source_id); }}>
              <span class="backlinks-panel__title">${bl.source_title}</span>
              <span class="backlinks-panel__context">via [[${bl.link_text}]]</span>
            </div>
          `;
        })}
      </div>
    `;
  }


  // ═══════════════════════════════════════════
  //  EditorPane — note editor + preview + tags
  // ═══════════════════════════════════════════

  // ═══════════════════════════════════════════
  //  Export / Import helpers
  // ═══════════════════════════════════════════

  function exportNoteAsMarkdown(note, tags) {
    var parts = ['---'];
    parts.push('title: ' + (note.title || 'Untitled'));
    if (tags && tags.length) parts.push('tags: [' + tags.join(', ') + ']');
    if (note.created_at) parts.push('created: ' + note.created_at);
    parts.push('---');
    parts.push('');
    parts.push(note.body || '');
    var text = parts.join('\n');
    var blob = new Blob([text], { type: 'text/markdown;charset=utf-8' });
    var url = URL.createObjectURL(blob);
    var a = document.createElement('a');
    a.href = url;
    a.download = (note.title || 'Untitled').replace(/[^a-zA-Z0-9_\- ]/g, '').trim() + '.md';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  function parseFrontmatter(text) {
    var result = { title: '', tags: [], body: text };
    if (!text.startsWith('---')) return result;
    var end = text.indexOf('\n---', 3);
    if (end === -1) return result;
    var fm = text.substring(3, end).trim();
    var bodyStart = end + 4;
    // skip leading newline after frontmatter
    if (text[bodyStart] === '\n') bodyStart++;
    result.body = text.substring(bodyStart);
    var lines = fm.split('\n');
    for (var i = 0; i < lines.length; i++) {
      var line = lines[i].trim();
      if (line.startsWith('title:')) {
        result.title = line.substring(6).trim().replace(/^["']|["']$/g, '');
      } else if (line.startsWith('tags:')) {
        var tagStr = line.substring(5).trim();
        // parse [tag1, tag2] or tag1, tag2
        tagStr = tagStr.replace(/^\[|\]$/g, '');
        result.tags = tagStr.split(',').map(function(t) { return t.trim().toLowerCase(); }).filter(Boolean);
      }
    }
    return result;
  }


  function EditorPane({ note, folders, allTags, onSave, onDelete, onRefresh, onTagsChanged, onNavigate, onNavigateToTitle }) {
    var [title, setTitle]     = useState(note ? note.title : '');
    var [body, setBody]       = useState(note ? note.body : '');
    var [noteTags, setNoteTags] = useState([]);
    var [preview, setPreview] = useState(false);
    var [dirty, setDirty]     = useState(false);
    var [saving, setSaving]   = useState(false);
    var [saveCount, setSaveCount] = useState(0);
    var textareaRef = useRef(null);
    var cmContainerRef = useRef(null);
    var cmEditorRef = useRef(null);

    // keep mutable refs for the latest title/body so CM6 callbacks see current values
    var titleRef = useRef(title);
    var bodyRef = useRef(body);
    titleRef.current = title;
    bodyRef.current = body;

    // sync when note changes
    useEffect(function() {
      if (note) {
        setTitle(note.title || '');
        setBody(note.body || '');
        titleRef.current = note.title || '';
        bodyRef.current = note.body || '';
        setNoteTags(note.tags || []);
        setDirty(false);
        setPreview(false);
        // update CM6 content if editor exists
        if (cmEditorRef.current) {
          cmEditorRef.current.setValue(note.body || '');
        }
      }
    }, [note ? note.id : null]);

    // ── CM6 lifecycle ─────────────────────────
    useEffect(function() {
      if (!hasCM || preview || !note) return;
      var el = cmContainerRef.current;
      if (!el) return;

      // destroy previous
      if (cmEditorRef.current) {
        cmEditorRef.current.destroy();
        cmEditorRef.current = null;
      }

      var editor = CM.noteEditor(el, {
        value: bodyRef.current,
        darkMode: document.documentElement.getAttribute('data-theme') !== 'light',
        onChange: function(text) {
          bodyRef.current = text;
          setBody(text);
          setDirty(true);
          debounce('save', function() { doSave(titleRef.current, text); }, 1000);
        },
        onLink: function(linkTitle) {
          if (onNavigateToTitle) onNavigateToTitle(linkTitle);
        },
        linkCompleter: async function(query) {
          try {
            var res = await api.get('/search?q=' + encodeURIComponent(query));
            var items = (res && res.data) || res || [];
            if (!Array.isArray(items)) items = [];
            return items.map(function(n) { return { label: n.title, id: n.id }; });
          } catch (e) { return []; }
        },
      });

      cmEditorRef.current = editor;

      // Ctrl/Cmd+S via CM6 keymap injection
      var view = editor.getView();
      if (view) {
        var handleSaveKey = function(e) {
          if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
            e.preventDefault();
            doSave(titleRef.current, bodyRef.current);
          }
        };
        view.dom.addEventListener('keydown', handleSaveKey);
      }

      editor.focus();

      return function() {
        if (cmEditorRef.current) {
          cmEditorRef.current.destroy();
          cmEditorRef.current = null;
        }
      };
    }, [note ? note.id : null, preview]);

    function handleTitleChange(e) {
      setTitle(e.target.value);
      titleRef.current = e.target.value;
      setDirty(true);
      debounce('save', function() { doSave(e.target.value, bodyRef.current); }, 1000);
    }

    function handleBodyChange(e) {
      setBody(e.target.value);
      bodyRef.current = e.target.value;
      setDirty(true);
      debounce('save', function() { doSave(titleRef.current, e.target.value); }, 1000);
    }

    async function doSave(t, b) {
      if (!note) return;
      setSaving(true);
      try {
        await api.put('/notes/' + note.id, { title: t || 'Untitled', body: b });
        setDirty(false);
        setSaveCount(function(c) { return c + 1; });
        onRefresh();
      } catch (e) {
        console.error('Save failed:', e);
      } finally {
        setSaving(false);
      }
    }

    async function handleTagsChange(newTags) {
      if (!note) return;
      setNoteTags(newTags);
      try {
        await api.put('/tags/' + note.id, { tags: newTags });
        if (onTagsChanged) onTagsChanged();
      } catch (e) {
        console.error('Save tags failed:', e);
      }
    }

    async function handlePin() {
      if (!note) return;
      await api.put('/notes/' + note.id, { pinned: note.pinned ? 0 : 1 });
      onRefresh();
    }

    async function handleArchive() {
      if (!note || !confirm('Archive this note?')) return;
      await api.del('/notes/' + note.id);
      onDelete();
    }

    function handleExport() {
      if (!note) return;
      exportNoteAsMarkdown(note, noteTags);
    }

    function handleKeyDown(e) {
      // Tab inserts two spaces
      if (e.key === 'Tab') {
        e.preventDefault();
        var ta = e.target;
        var start = ta.selectionStart;
        var end = ta.selectionEnd;
        var val = ta.value;
        ta.value = val.substring(0, start) + '  ' + val.substring(end);
        ta.selectionStart = ta.selectionEnd = start + 2;
        setBody(ta.value);
        bodyRef.current = ta.value;
        setDirty(true);
        debounce('save', function() { doSave(title, ta.value); }, 1000);
      }
      // Ctrl/Cmd+S force save
      if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
        e.preventDefault();
        doSave(title, body);
      }
    }

    // ── Outgoing links for resolved/unresolved styling ──
    var [outgoingLinks, setOutgoingLinks] = useState([]);
    useEffect(function() {
      if (!note) { setOutgoingLinks([]); return; }
      api.get('/links/' + note.id).then(function(res) {
        var items = (res && res.data) || res || [];
        setOutgoingLinks(Array.isArray(items) ? items : []);
      }).catch(function() { setOutgoingLinks([]); });
    }, [note ? note.id : null, saveCount]);

    var previewHtml = useMemo(function() {
      var h = renderMarkdown(body);
      // mark unresolved wikilinks
      for (var i = 0; i < outgoingLinks.length; i++) {
        var link = outgoingLinks[i];
        if (!link.target_id) {
          var search = 'class="wikilink" data-wikilink="' + escHtml(link.link_text) + '"';
          var replace = 'class="wikilink wikilink--unresolved" data-wikilink="' + escHtml(link.link_text) + '"';
          h = h.split(search).join(replace);
        }
      }
      return h;
    }, [body, outgoingLinks]);

    // ── Wikilink click handling in preview ──
    var previewRef = useRef(null);
    useEffect(function() {
      var el = previewRef.current;
      if (!el || !preview) return;
      function handleClick(e) {
        var target = e.target;
        if (target.classList && target.classList.contains('wikilink')) {
          e.preventDefault();
          var linkText = target.getAttribute('data-wikilink');
          if (linkText && onNavigateToTitle) onNavigateToTitle(linkText);
        }
      }
      el.addEventListener('click', handleClick);
      return function() { el.removeEventListener('click', handleClick); };
    }, [preview, onNavigateToTitle]);

    // build indented folder list for the select dropdown
    var folderOptions = useMemo(function() {
      var list = folders || [];
      var byId = {};
      var roots = [];
      for (var i = 0; i < list.length; i++) {
        byId[list[i].id] = { folder: list[i], children: [] };
      }
      for (var i = 0; i < list.length; i++) {
        var f = list[i];
        if (f.parent_id && byId[f.parent_id]) {
          byId[f.parent_id].children.push(byId[f.id]);
        } else {
          roots.push(byId[f.id]);
        }
      }
      var flat = [];
      function walk(nodes, depth) {
        nodes.sort(function(a, b) {
          if (a.folder.sort_order !== b.folder.sort_order) return a.folder.sort_order - b.folder.sort_order;
          return (a.folder.name || '').localeCompare(b.folder.name || '');
        });
        for (var j = 0; j < nodes.length; j++) {
          var prefix = '';
          for (var k = 0; k < depth; k++) prefix += '\u00A0\u00A0';
          if (depth > 0) prefix += '\u2514 ';
          flat.push({ id: nodes[j].folder.id, label: prefix + nodes[j].folder.name });
          walk(nodes[j].children, depth + 1);
        }
      }
      walk(roots, 0);
      return flat;
    }, [folders]);

    if (!note) {
      return html`<div class="notes-editor__empty">Select a note or create a new one</div>`;
    }

    // ── Render editor body ─────────────────────
    function renderEditorBody() {
      if (preview) {
        return html`<div class="notes-preview" ref=${previewRef} dangerouslySetInnerHTML=${{ __html: previewHtml }} />`;
      }
      if (hasCM) {
        return html`<div class="notes-editor__cm" ref=${cmContainerRef} />`;
      }
      // fallback textarea
      return html`<textarea class="notes-editor__textarea" ref=${textareaRef}
                    value=${body} onInput=${handleBodyChange}
                    onKeyDown=${handleKeyDown}
                    placeholder="Start writing in Markdown…" />`;
    }

    return html`
      <div class="notes-editor">
        <div class="notes-editor__header">
          <input class="notes-editor__title-input" type="text"
                 value=${title} onInput=${handleTitleChange}
                 placeholder="Note title" />
          <div class="notes-editor__actions">
            <select class="notes-editor__folder-select"
              value=${note.folder_id || ''}
              onChange=${async function(e) {
                await api.post('/notes/move', { note_id: note.id, folder_id: e.target.value });
                onRefresh();
              }}>
              <option value="">Unfiled</option>
              ${folderOptions.map(function(f) {
                return html`<option key=${f.id} value=${f.id}>${f.label}</option>`;
              })}
            </select>
            <span class=${dirty ? 'notes-saved notes-saved--dirty' : 'notes-saved'}>
              ${saving ? 'Saving…' : (dirty ? 'Unsaved' : 'Saved')}
            </span>
            <button class="notes-toggle ${preview ? 'notes-toggle--active' : ''}"
                    onClick=${() => setPreview(!preview)}>
              ${preview ? 'Edit' : 'Preview'}
            </button>
            <button class="notes-btn" onClick=${handleExport} title="Export as .md">Export</button>
            <button class="notes-btn" onClick=${handlePin}
                    title=${note.pinned ? 'Unpin' : 'Pin'}>
              ${note.pinned ? '📌' : '📍'}
            </button>
            <button class="notes-btn notes-btn--danger" onClick=${handleArchive}
                    title="Archive">🗑</button>
          </div>
        </div>
        <${TagInput} tags=${noteTags} allTags=${allTags} onChange=${handleTagsChange} />
        <div class="notes-editor__body">
          ${renderEditorBody()}
        </div>
        <${BacklinksPanel} noteId=${note.id} onNavigate=${onNavigate} />
      </div>
    `;
  }


  // ═══════════════════════════════════════════
  //  NotesApp — root component
  // ═══════════════════════════════════════════

  function NotesApp() {
    var [notes, setNotes]               = useState([]);
    var [loading, setLoading]           = useState(true);
    var [activeId, setActiveId]         = useState(null);
    var [activeNote, setActiveNote]     = useState(null);
    var [searchTerm, setSearchTerm]     = useState('');
    var [stats, setStats]               = useState(null);
    var [folders, setFolders]           = useState([]);
    var [activeFolderId, setActiveFolderId] = useState(null);
    var [showUnfiled, setShowUnfiled]   = useState(false);
    var [allTags, setAllTags]           = useState([]);
    var [activeTag, setActiveTag]       = useState(null);

    // ── Load note list ────────────────────────
    var loadNotes = useCallback(async function() {
      try {
        var res;
        if (searchTerm.trim()) {
          res = await api.get('/search?q=' + encodeURIComponent(searchTerm.trim()));
        } else if (activeFolderId) {
          res = await api.get('/notes?folder_id=' + encodeURIComponent(activeFolderId));
        } else {
          res = await api.get('/notes');
        }
        var items = (res && res.data) || res || [];
        if (!Array.isArray(items)) items = [];
        // filter to unfiled when that view is active
        if (showUnfiled && !searchTerm.trim() && !activeFolderId) {
          items = items.filter(function(n) { return !n.folder_id; });
        }
        // filter by active tag (client-side)
        if (activeTag) {
          items = items.filter(function(n) {
            return n.tags && n.tags.indexOf(activeTag) !== -1;
          });
        }
        // sort: pinned first, then by updated_at desc
        items.sort(function(a, b) {
          if (a.pinned !== b.pinned) return b.pinned - a.pinned;
          return (b.updated_at || b.created_at || '').localeCompare(a.updated_at || a.created_at || '');
        });
        setNotes(items);
      } catch (e) {
        console.error('Load notes failed:', e);
      } finally {
        setLoading(false);
      }
    }, [searchTerm, activeFolderId, showUnfiled, activeTag]);

    // ── Load single note (full body) ──────────
    var loadNote = useCallback(async function(id) {
      if (!id) { setActiveNote(null); return; }
      try {
        var note = await api.get('/notes/' + id);
        setActiveNote(note);
      } catch (e) {
        console.error('Load note failed:', e);
        setActiveNote(null);
      }
    }, []);

    // ── Load folders ──────────────────────────
    var loadFolders = useCallback(async function() {
      try {
        var res = await api.get('/folders');
        var items = (res && res.data) || res || [];
        setFolders(Array.isArray(items) ? items : []);
      } catch (e) { console.error('Load folders failed:', e); }
    }, []);

    // ── Load stats ────────────────────────────
    var loadStats = useCallback(async function() {
      try {
        var s = await api.get('/stats');
        setStats(s);
      } catch (e) { /* ignore */ }
    }, []);

    // ── Load all tags ─────────────────────────
    var loadTags = useCallback(async function() {
      try {
        var res = await api.get('/tags');
        var items = (res && res.data) || res || [];
        setAllTags(Array.isArray(items) ? items : []);
      } catch (e) { console.error('Load tags failed:', e); }
    }, []);

    useEffect(function() { loadNotes(); loadFolders(); loadStats(); loadTags(); }, [loadNotes]);

    // select note
    function handleSelect(id) {
      setActiveId(id);
      loadNote(id);
    }

    // ── Create note ───────────────────────────
    async function handleNew() {
      try {
        var payload = { title: 'Untitled', body: '' };
        if (activeFolderId) payload.folder_id = activeFolderId;
        var note = await api.post('/notes', payload);
        if (note && note.id) {
          setActiveId(note.id);
          loadNote(note.id);
          loadNotes();
          loadStats();
        }
      } catch (e) {
        console.error('Create failed:', e);
      }
    }

    // ── Refresh after save ────────────────────
    function handleRefresh() {
      loadNotes();
      if (activeId) loadNote(activeId);
      loadStats();
    }

    // ── After delete ──────────────────────────
    function handleDelete() {
      setActiveId(null);
      setActiveNote(null);
      loadNotes();
      loadStats();
      loadTags();
    }

    // ── Tags changed in editor ────────────────
    function handleTagsChanged() {
      loadTags();
      loadNotes();
      loadStats();
    }

    // ── Tag filter ────────────────────────────
    function handleTagFilter(tag) {
      setActiveTag(tag);
    }

    // ── Drag-and-drop move note to folder ─────
    async function handleDropNote(noteId, folderId) {
      try {
        await api.post('/notes/move', { note_id: noteId, folder_id: folderId });
        loadNotes();
        loadStats();
        if (activeId === noteId) loadNote(activeId);
      } catch (e) { console.error('Move note failed:', e); }
    }

    // ── Folder actions ───────────────────────
    function handleSelectFolder(folderId) {
      setActiveFolderId(folderId);
      setShowUnfiled(false);
      setSearchTerm('');
    }
    function handleSelectAll() {
      setActiveFolderId(null);
      setShowUnfiled(false);
    }
    function handleSelectUnfiled() {
      setActiveFolderId(null);
      setShowUnfiled(true);
    }
    async function handleCreateFolder(parentId) {
      var name = prompt('Folder name:');
      if (!name || !name.trim()) return;
      try {
        await api.post('/folders', { name: name.trim(), parent_id: parentId || '' });
        loadFolders();
        loadStats();
      } catch (e) { console.error('Create folder failed:', e); }
    }
    async function handleRenameFolder(folderId, newName) {
      try {
        await api.put('/folders/' + folderId, { name: newName });
        loadFolders();
      } catch (e) { console.error('Rename folder failed:', e); }
    }
    async function handleDeleteFolder(folderId) {
      if (!confirm('Delete this folder? Notes will be moved to Unfiled.')) return;
      try {
        await api.del('/folders/' + folderId);
        if (activeFolderId === folderId) setActiveFolderId(null);
        loadFolders();
        loadNotes();
        loadStats();
      } catch (e) { console.error('Delete folder failed:', e); }
    }

    // ── Wikilink navigation by title ─────────
    async function handleNavigateToTitle(title) {
      try {
        var res = await api.get('/search?q=' + encodeURIComponent(title.trim()));
        var items = (res && res.data) || res || [];
        if (!Array.isArray(items)) items = [];
        var match = null;
        for (var i = 0; i < items.length; i++) {
          if ((items[i].title || '').toLowerCase().trim() === title.toLowerCase().trim()) {
            match = items[i];
            break;
          }
        }
        if (match) {
          handleSelect(match.id);
        } else {
          // create the note and navigate to it
          var sourceId = activeId;
          var newNote = await api.post('/notes', { title: title.trim(), body: '' });
          if (newNote && newNote.id) {
            // re-save source note so _sync_links resolves the new target
            if (sourceId && activeNote) {
              await api.put('/notes/' + sourceId, { body: activeNote.body || '' });
            }
            setActiveId(newNote.id);
            loadNote(newNote.id);
            loadNotes();
            loadStats();
          }
        }
      } catch (e) {
        console.error('Navigate to title failed:', e);
      }
    }

    // ── Import .md file ─────────────────────
    function handleImport() {
      var input = document.createElement('input');
      input.type = 'file';
      input.accept = '.md,.markdown,.txt';
      input.style.display = 'none';
      input.onchange = async function() {
        var file = input.files && input.files[0];
        if (!file) return;
        try {
          var text = await new Promise(function(resolve, reject) {
            var reader = new FileReader();
            reader.onload = function() { resolve(reader.result); };
            reader.onerror = reject;
            reader.readAsText(file);
          });
          var parsed = parseFrontmatter(text);
          var noteTitle = parsed.title || file.name.replace(/\.(md|markdown|txt)$/i, '') || 'Imported';
          var payload = { title: noteTitle, body: parsed.body };
          if (activeFolderId) payload.folder_id = activeFolderId;
          var newNote = await api.post('/notes', payload);
          if (newNote && newNote.id) {
            if (parsed.tags.length) {
              await api.put('/tags/' + newNote.id, { tags: parsed.tags });
            }
            setActiveId(newNote.id);
            loadNote(newNote.id);
            loadNotes();
            loadStats();
            loadTags();
          }
        } catch (e) {
          console.error('Import failed:', e);
        }
        document.body.removeChild(input);
      };
      document.body.appendChild(input);
      input.click();
    }

    // ── Search ────────────────────────────────
    function handleSearch(e) {
      var val = e.target.value;
      setSearchTerm(val);
      debounce('search', function() { loadNotes(); }, 300);
    }

    return html`
      <${Topbar} title="Notes">
        <${Button} variant="primary" size="sm" onClick=${handleNew}>+ New Note<//>
        <${Button} variant="secondary" size="sm" onClick=${handleImport}>Import .md<//>
      <//>
      <div class="notes-app">
        <div class="notes-sidebar">
          <div class="notes-sidebar__header">
            <span class="notes-sidebar__title">Notes</span>
            ${stats && html`<span class="notes-sidebar__count">${stats.total}</span>`}
          </div>
          <${FolderTree} folders=${folders}
            activeFolderId=${activeFolderId} showUnfiled=${showUnfiled}
            onSelectFolder=${handleSelectFolder} onSelectAll=${handleSelectAll}
            onSelectUnfiled=${handleSelectUnfiled}
            onCreateFolder=${handleCreateFolder}
            onRenameFolder=${handleRenameFolder}
            onDeleteFolder=${handleDeleteFolder}
            onDropNote=${handleDropNote} />
          <${TagFilter} tags=${allTags} activeTag=${activeTag} onSelect=${handleTagFilter} />
          <div class="notes-search">
            <input type="text" placeholder="Search notes…"
                   value=${searchTerm} onInput=${handleSearch} />
          </div>
          <div class="notes-list">
            ${loading && html`<div class="notes-list__loading"><${Spinner} size="md" /></div>`}
            ${!loading && notes.length === 0 && html`
              <div class="notes-list__empty">
                ${searchTerm ? 'No matching notes' : (activeTag ? 'No notes with tag "' + activeTag + '"' : 'No notes yet. Click + New Note to start.')}
              </div>
            `}
            ${!loading && notes.map(function(n) {
              return html`
                <${NoteCard} key=${n.id} note=${n}
                  active=${n.id === activeId}
                  onClick=${() => handleSelect(n.id)} />
              `;
            })}
          </div>
        </div>
        <${EditorPane} note=${activeNote} folders=${folders} allTags=${allTags}
          onSave=${handleRefresh}
          onDelete=${handleDelete}
          onRefresh=${handleRefresh}
          onTagsChanged=${handleTagsChanged}
          onNavigate=${handleSelect}
          onNavigateToTitle=${handleNavigateToTitle} />
      </div>
    `;
  }

  // ── Mount ──────────────────────────────────
  render(html`<${NotesApp} />`, mount);

})();
