// Notequick — shared components.
// Sidebar, NotesList, Editor, ContextMenu, FormatToolbar, AttachmentViewer.

// ─── time helpers ────────────────────────────────────────────────────────────
function fmtRelative(iso) {
  const t = new Date(iso).getTime();
  const now = Date.now();
  const diff = Math.max(0, now - t);
  const m = Math.round(diff / 60000);
  if (m < 1)   return 'just now';
  if (m < 60)  return `${m}m ago`;
  const h = Math.round(m / 60);
  if (h < 24)  return `${h}h ago`;
  const d = Math.round(h / 24);
  if (d < 7)   return `${d}d ago`;
  return new Date(iso).toLocaleDateString('en-US',{month:'short',day:'numeric'});
}
function fmtTimestamp(iso) {
  const d = new Date(iso);
  const now = new Date();
  const sameDay = d.toDateString() === now.toDateString();
  const yest = new Date(now); yest.setDate(yest.getDate()-1);
  const isYest = d.toDateString() === yest.toDateString();
  const time = d.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit'});
  if (sameDay) return `Today · ${time}`;
  if (isYest)  return `Yesterday · ${time}`;
  return d.toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric'}) + ' · ' + time;
}
function dayKey(iso) { return new Date(iso).toDateString(); }
function fmtDayHeader(iso) {
  const d = new Date(iso);
  const now = new Date();
  const sameDay = d.toDateString() === now.toDateString();
  const yest = new Date(now); yest.setDate(yest.getDate()-1);
  if (sameDay)                                  return 'Today';
  if (d.toDateString() === yest.toDateString()) return 'Yesterday';
  return d.toLocaleDateString('en-US',{weekday:'long',month:'long',day:'numeric'});
}

// ─── ContextMenu ─────────────────────────────────────────────────────────────
function ContextMenu({ menu, onClose }) {
  const ref = React.useRef(null);
  const [pos, setPos] = React.useState({ x: 0, y: 0 });

  React.useLayoutEffect(() => {
    if (!menu) return;
    const w = ref.current?.offsetWidth || 200;
    const h = ref.current?.offsetHeight || 240;
    const x = Math.min(menu.x, window.innerWidth - w - 8);
    const y = Math.min(menu.y, window.innerHeight - h - 8);
    setPos({ x, y });
  }, [menu]);

  React.useEffect(() => {
    if (!menu) return;
    const onDown = (e) => {
      if (ref.current && !ref.current.contains(e.target)) onClose();
    };
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    document.addEventListener('mousedown', onDown, true);
    document.addEventListener('contextmenu', onDown, true);
    window.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown, true);
      document.removeEventListener('contextmenu', onDown, true);
      window.removeEventListener('keydown', onKey);
    };
  }, [menu, onClose]);

  if (!menu) return null;
  return (
    <div ref={ref} className="nq-ctx" style={{ left: pos.x, top: pos.y }}
         onContextMenu={(e)=>e.preventDefault()}>
      {menu.items.map((it, i) => {
        if (it.divider) return <div key={i} className="nq-ctx-div"/>;
        if (it.header)  return <div key={i} className="nq-ctx-h">{it.header}</div>;
        return (
          <button key={i}
                  className={'nq-ctx-item' + (it.danger ? ' danger' : '')}
                  disabled={it.disabled}
                  onClick={(e)=>{ e.stopPropagation(); onClose(); it.onClick?.(); }}>
            <span className="nq-ctx-icon">{it.icon}</span>
            <span className="nq-ctx-lbl">{it.label}</span>
            {it.kbd && <span className="nq-ctx-kbd mono">{it.kbd}</span>}
          </button>
        );
      })}
    </div>
  );
}

function useContextMenu() {
  const [menu, setMenu] = React.useState(null);
  const open = React.useCallback((e, items) => {
    e.preventDefault();
    e.stopPropagation();
    setMenu({ x: e.clientX, y: e.clientY, items });
  }, []);
  const close = React.useCallback(() => setMenu(null), []);
  return { menu, open, close };
}

// ─── FormatToolbar ───────────────────────────────────────────────────────────
function FormatToolbar({ taRef, value, onChange, showTimestamp = false }) {
  const apply = (before, after = '', placeholder = '') => {
    const ta = taRef.current;
    if (!ta) return;
    ta.focus();
    const start = ta.selectionStart;
    const end = ta.selectionEnd;
    const sel = value.slice(start, end) || placeholder;
    const insertion = before + sel + after;
    const newValue = value.slice(0, start) + insertion + value.slice(end);
    onChange(newValue);
    requestAnimationFrame(() => {
      const cs = start + before.length;
      const ce = cs + sel.length;
      ta.setSelectionRange(cs, ce);
      ta.focus();
    });
  };

  const applyLinePrefix = (prefix) => {
    const ta = taRef.current;
    if (!ta) return;
    ta.focus();
    const start = ta.selectionStart;
    const end = ta.selectionEnd;
    const before = value.slice(0, start);
    const after = value.slice(end);
    const lineStart = before.lastIndexOf('\n') + 1;
    const head = value.slice(0, lineStart);
    const middle = value.slice(lineStart, end);
    const lines = middle.split('\n');
    const allHave = lines.every(l => l.startsWith(prefix));
    const newLines = lines.map(l => allHave ? l.slice(prefix.length) : prefix + l);
    const newMiddle = newLines.join('\n');
    const newValue = head + newMiddle + after;
    onChange(newValue);
    requestAnimationFrame(() => {
      const delta = newMiddle.length - middle.length;
      ta.setSelectionRange(start + (allHave ? -prefix.length : prefix.length), end + delta);
      ta.focus();
    });
  };

  const insertAtCursor = (text) => {
    const ta = taRef.current;
    if (!ta) return;
    ta.focus();
    const start = ta.selectionStart;
    const end = ta.selectionEnd;
    const before = value.slice(0, start);
    const needsLeadingNewline = text.startsWith('[') && before.length > 0 && !before.endsWith('\n');
    const insertion = (needsLeadingNewline ? '\n' : '') + text;
    const newValue = before + insertion + value.slice(end);
    onChange(newValue);
    requestAnimationFrame(() => {
      const cursor = start + insertion.length;
      ta.setSelectionRange(cursor, cursor);
      ta.focus();
    });
  };

  const link = () => {
    const url = prompt('Link URL:', 'https://');
    if (!url) return;
    apply('[', `](${url})`, 'text');
  };

  const stamp = () => {
    insertAtCursor(`[${fmtTimestamp(new Date().toISOString())}] - `);
  };

  const Btn = ({ onClick, title, children }) => (
    <button type="button" className="nq-fmt-btn" onClick={onClick} title={title}
            onMouseDown={(e)=>e.preventDefault()}>
      {children}
    </button>
  );

  return (
    <div className="nq-fmt">
      <Btn title="Bold"          onClick={()=>apply('**', '**', 'bold')}><b>B</b></Btn>
      <Btn title="Italic"        onClick={()=>apply('*', '*', 'italic')}><i>I</i></Btn>
      <Btn title="Strikethrough" onClick={()=>apply('~~', '~~', 'strike')}><s>S</s></Btn>
      <Btn title="Inline code"   onClick={()=>apply('`', '`', 'code')}>
        <span className="mono" style={{fontSize:11}}>{'<>'}</span>
      </Btn>
      <div className="nq-fmt-div"/>
      <Btn title="Heading 1" onClick={()=>applyLinePrefix('# ')}>H1</Btn>
      <Btn title="Heading 2" onClick={()=>applyLinePrefix('## ')}>H2</Btn>
      <div className="nq-fmt-div"/>
      <Btn title="Bullet list"   onClick={()=>applyLinePrefix('- ')}>•</Btn>
      <Btn title="Numbered list" onClick={()=>applyLinePrefix('1. ')}>1.</Btn>
      <Btn title="Quote"         onClick={()=>applyLinePrefix('> ')}>&ldquo;</Btn>
      <Btn title="Code block"    onClick={()=>apply('\n```\n', '\n```\n', 'code')}>{'{}'}</Btn>
      <Btn title="Link"          onClick={link}>↗</Btn>
      {showTimestamp && (
        <>
          <div className="nq-fmt-div"/>
          <Btn title="Insert timestamp" onClick={stamp}>
            <IconClock size={12}/>
          </Btn>
        </>
      )}
    </div>
  );
}

// ─── ImportProgress modal ────────────────────────────────────────────────────
function ImportProgress({ state, onClose }) {
  if (!state) return null;
  const isDone = !!state.done;
  const pct = state.total ? Math.round((state.current / state.total) * 100) : 0;
  return (
    <div className="nq-import-modal" onClick={(e)=>{ if (e.target===e.currentTarget && isDone) onClose(); }}>
      <div className="nq-import-card">
        <div className="nq-import-h">
          <IconImport size={16}/>
          <span>{isDone ? 'Import complete' : 'Importing from Evernote'}</span>
        </div>
        {!isDone && (
          <>
            <div className="nq-import-bar"><div style={{ width: pct + '%' }}/></div>
            <div className="nq-import-meta mono">
              {state.current} / {state.total} · {pct}%
            </div>
            <div className="nq-import-now" title={state.title}>
              {state.phase === 'attachment'
                ? `${state.title} — uploading ${state.attachmentName} (${state.attachmentIndex}/${state.attachmentCount})`
                : state.title}
            </div>
          </>
        )}
        {isDone && (
          <>
            <div className="nq-import-summary">
              {state.message ? (
                <>
                  <div>Import failed.</div>
                  <pre className="nq-import-err nq-import-detail">{state.message}</pre>
                </>
              ) : (
                <>
                  <div><b>{state.imported}</b> note{state.imported===1?'':'s'} imported into <b>{state.notebookName}</b>.</div>
                  {state.errors > 0 && (
                    <div className="nq-import-err">{state.errors} item{state.errors===1?'':'s'} failed (see browser console).</div>
                  )}
                </>
              )}
            </div>
            <div style={{display:'flex',justifyContent:'flex-end'}}>
              <button className="nq-comp-send" onClick={onClose}>Done</button>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// ─── Sidebar ─────────────────────────────────────────────────────────────────
function Sidebar({ notebooks, tags, notes, filter, setFilter, onNewNote, onImport, importing, sideW, density,
                   onCtxNotebook, onCtxNotebookHeader, onCtxTag, onCtxTagHeader }) {
  const counts = React.useMemo(() => {
    const byBook = {}, byTag = {};
    notes.forEach(n => {
      byBook[n.notebook] = (byBook[n.notebook]||0) + 1;
      (n.tags||[]).forEach(t => byTag[t] = (byTag[t]||0)+1);
    });
    return { byBook, byTag, all: notes.length,
      starred: notes.filter(n => n.starred).length };
  }, [notes]);

  const Row = ({ id, label, count, swatch, icon, active, onContextMenu }) => (
    <button className="nq-sb-row" data-active={active ? '1' : '0'}
            onClick={() => setFilter(id)}
            onContextMenu={onContextMenu}>
      <span className="nq-sb-icon">
        {swatch && <span className="nq-sb-dot" style={{ background: swatch }} />}
        {!swatch && icon}
      </span>
      <span className="nq-sb-lbl">{label}</span>
      {count != null && <span className="nq-sb-cnt mono">{count}</span>}
    </button>
  );

  return (
    <aside className="nq-sb scroll" style={{ width: sideW }} data-density={density}>
      <div className="nq-brand">
        <div className="nq-brand-mark"><span /></div>
        <div className="nq-brand-name">Notequick</div>
      </div>

      <button className="nq-new" onClick={onNewNote}>
        <IconPencil size={14} />
        <span>New note</span>
        <span className="nq-kbd mono">⌘N</span>
      </button>

      <button className="nq-import-sb" onClick={onImport} disabled={importing}
              title="Import an Evernote .enex export">
        <IconImport size={13}/>
        <span>{importing ? 'Importing…' : 'Import from Evernote'}</span>
      </button>

      <div className="nq-sb-group">
        <Row id={{type:'all'}}     label="All notes" count={counts.all}
             icon={<IconInbox size={15}/>}  active={filter.type==='all'} />
        <Row id={{type:'starred'}} label="Starred"   count={counts.starred}
             icon={<IconStar size={15}/>}   active={filter.type==='starred'} />
      </div>

      <div className="nq-sb-h" onContextMenu={onCtxNotebookHeader}>
        Notebooks
        <button className="nq-sb-h-add" title="New notebook" onClick={onCtxNotebookHeader}>
          <IconPlus size={11}/>
        </button>
      </div>
      <div className="nq-sb-group">
        {notebooks.map(nb => (
          <Row key={nb.id} id={{type:'notebook', id:nb.id}}
               label={nb.name} count={counts.byBook[nb.id]||0}
               swatch={nb.color}
               active={filter.type==='notebook' && filter.id===nb.id}
               onContextMenu={(e)=>onCtxNotebook(e, nb)} />
        ))}
      </div>

      <div className="nq-sb-h" onContextMenu={onCtxTagHeader}>
        Tags
        <button className="nq-sb-h-add" title="New tag" onClick={onCtxTagHeader}>
          <IconPlus size={11}/>
        </button>
      </div>
      <div className="nq-sb-group">
        {tags.map(t => (
          <Row key={t.id} id={{type:'tag', id:t.id}}
               label={`#${t.name}`} count={counts.byTag[t.id]||0}
               icon={<IconTag size={14}/>}
               active={filter.type==='tag' && filter.id===t.id}
               onContextMenu={(e)=>onCtxTag(e, t)} />
        ))}
      </div>

      <div className="nq-sb-spacer" />
    </aside>
  );
}

// ─── NotesList ───────────────────────────────────────────────────────────────
function NotesList({ notes, notebooks, query, setQuery, selectedId, onSelect,
                     title, sort, setSort, listW, density, onCtxCard }) {
  return (
    <section className="nq-list scroll" style={{ width: listW }} data-density={density}>
      <div className="nq-list-hd">
        <div className="nq-list-title">
          <span>{title}</span>
          <span className="mono nq-list-count">{notes.length}</span>
        </div>
        <div className="nq-search">
          <IconSearch size={14} />
          <input value={query} onChange={(e)=>setQuery(e.target.value)}
                 placeholder="Search notes & entries…" />
          {query && (
            <button className="nq-search-x" onClick={()=>setQuery('')} aria-label="Clear">
              <IconX size={12}/>
            </button>
          )}
        </div>
        <div className="nq-list-tools">
          <button className={'nq-sort '+(sort==='updated'?'is-on':'')}
                  onClick={()=>setSort('updated')}>Recent</button>
          <button className={'nq-sort '+(sort==='alpha'?'is-on':'')}
                  onClick={()=>setSort('alpha')}>A–Z</button>
          <button className={'nq-sort '+(sort==='created'?'is-on':'')}
                  onClick={()=>setSort('created')}>Created</button>
        </div>
      </div>

      <div className="nq-list-body">
        {notes.length === 0 && (
          <div className="nq-empty">
            <div className="nq-empty-mark"/>
            <div className="nq-empty-t">No notes here</div>
            <div className="nq-empty-s">Try a different filter, or create a new note.</div>
          </div>
        )}
        {notes.map(n => {
          const nb = notebooks.find(b => b.id===n.notebook);
          const snippet = stripMarkdown(n.summary) || 'Empty note';
          return (
            <button key={n.id} className="nq-card" data-active={selectedId===n.id?'1':'0'}
                    onClick={()=>onSelect(n.id)}
                    onContextMenu={(e)=>onCtxCard(e, n)}>
              <div className="nq-card-top">
                <span className="nq-card-nb">
                  <span className="nq-sb-dot" style={{background:nb?.color}}/>
                  {nb?.name || n.notebook}
                </span>
                <span className="mono nq-card-time">{fmtRelative(n.updated)}</span>
              </div>
              <div className="nq-card-title">
                {n.starred && <IconStar size={12} fill="currentColor" stroke={0}/>}
                <span>{n.title || 'Untitled note'}</span>
              </div>
              <div className="nq-card-snippet">{snippet}</div>
              <div className="nq-card-bot">
                <div className="nq-card-tags">
                  {(n.tags||[]).slice(0,3).map(t => (
                    <span key={t} className="nq-chip">#{t}</span>
                  ))}
                </div>
                <div className="nq-card-meta mono">
                  {n.attachments.length > 0 && (
                    <span title={`${n.attachments.length} attachments`}>
                      <IconPaperclip size={12}/> {n.attachments.length}
                    </span>
                  )}
                </div>
              </div>
            </button>
          );
        })}
      </div>
    </section>
  );
}

// ─── Attachment card ─────────────────────────────────────────────────────────
function Attachment({ a, onRemove, onOpen }) {
  const Mark = a.kind === 'pdf'   ? IconPDF
             : a.kind === 'image' ? IconImage
             : a.kind === 'video' ? IconVideo
             :                      IconFile;
  return (
    <div className={'nq-att nq-att-'+a.kind} onClick={()=>onOpen(a)} role="button" tabIndex={0}>
      <div className="nq-att-mark"><Mark size={16}/></div>
      <div className="nq-att-meta">
        <div className="nq-att-name">{a.name}</div>
        <div className="nq-att-sub mono">{a.kind.toUpperCase()} · {a.size}</div>
      </div>
      <button className="nq-att-x"
              onClick={(e)=>{e.stopPropagation(); onRemove();}}
              aria-label="Remove attachment">
        <IconX size={12}/>
      </button>
    </div>
  );
}

// ─── Full-screen viewer ──────────────────────────────────────────────────────
function AttachmentViewer({ att, onClose }) {
  const [url, setUrl] = React.useState('');
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);
  React.useEffect(() => {
    let cancelled = false;
    setUrl('');
    if (att?.storage_path) {
      getSignedFileUrl(att.storage_path).then(u => { if (!cancelled) setUrl(u); });
    }
    return () => { cancelled = true; };
  }, [att?.storage_path]);
  if (!att) return null;
  let inner;
  if (!url) inner = <div className="nq-viewer-fallback"><div>Loading…</div></div>;
  else if (att.kind === 'image')      inner = <img src={url} alt={att.name}/>;
  else if (att.kind === 'video') inner = <video src={url} controls autoPlay/>;
  else if (att.kind === 'pdf')   inner = <iframe src={url} title={att.name}/>;
  else inner = (
    <div className="nq-viewer-fallback">
      <IconFile size={40}/>
      <div>{att.name}</div>
      <a href={url} target="_blank" rel="noopener noreferrer">Open in new tab</a>
    </div>
  );
  return (
    <div className="nq-viewer" onClick={(e)=>{ if (e.target===e.currentTarget) onClose(); }}>
      <div className="nq-viewer-bar">
        <span className="nq-viewer-name">{att.name}</span>
        {url && <a className="nq-viewer-link mono" href={url} target="_blank" rel="noopener noreferrer">Open ↗</a>}
        <button className="nq-viewer-x" onClick={onClose} aria-label="Close"><IconX size={16}/></button>
      </div>
      <div className="nq-viewer-stage">{inner}</div>
    </div>
  );
}

// ─── Editor ──────────────────────────────────────────────────────────────────
function Editor({ note, notebooks, tagsAll, onChange, onAddAttachment,
                  onRemoveAttachment, onStar, onDelete, accent, onCtxEditor }) {
  const bodyRef = React.useRef(null);
  const fileRef = React.useRef(null);
  const [body, setBody] = React.useState('');
  const [preview, setPreview] = React.useState(false);
  const [drag, setDrag] = React.useState(false);
  const [tagPickerOpen, setTagPickerOpen] = React.useState(false);
  const [notebookOpen, setNotebookOpen] = React.useState(false);
  const [viewing, setViewing] = React.useState(null);
  const [uploading, setUploading] = React.useState(0);
  const saveTimerRef = React.useRef(null);
  const savedBodyRef = React.useRef('');
  const migratedRef = React.useRef(new Set());

  // Sync local body state when switching notes.
  React.useEffect(() => {
    setBody(note?.summary || '');
    savedBodyRef.current = note?.summary || '';
    setPreview(false);
    clearTimeout(saveTimerRef.current);
  }, [note?.id]);

  // One-shot migration: fold legacy timestamped log entries into the body.
  React.useEffect(() => {
    if (!note) return;
    if (!note.entries || note.entries.length === 0) return;
    if (migratedRef.current.has(note.id)) return;
    migratedRef.current.add(note.id);

    (async () => {
      const sorted = note.entries.slice().sort((a, b) => new Date(a.at) - new Date(b.at));
      const block = sorted.map(e => {
        const ts = fmtTimestamp(e.at);
        const tag = e.tag ? ` #${e.tag}` : '';
        return `**${ts}**${tag}\n\n${e.body}`;
      }).join('\n\n---\n\n');
      const cur = (note.summary || '').trim();
      const newBody = cur ? cur + '\n\n---\n\n' + block : block;
      setBody(newBody);
      savedBodyRef.current = newBody;
      try {
        await updateNote(note.id, { summary: newBody });
        for (const e of sorted) {
          try { await removeEntry(e.id); } catch (err) { console.error(err); }
        }
      } catch (err) {
        console.error('Entry migration failed:', err);
      }
    })();
  }, [note?.id, note?.entries?.length]);

  const updateBody = (v) => {
    setBody(v);
    if (!note) return;
    const id = note.id;
    clearTimeout(saveTimerRef.current);
    saveTimerRef.current = setTimeout(() => {
      if (v === savedBodyRef.current) return;
      savedBodyRef.current = v;
      onChange(id, { summary: v });
    }, 400);
  };

  // Flush pending save on note switch / unmount so unsaved edits are not lost.
  React.useEffect(() => () => {
    if (!note) return;
    if (body === savedBodyRef.current) return;
    savedBodyRef.current = body;
    onChange(note.id, { summary: body });
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [note?.id]);

  const addComment = () => {
    const ta = bodyRef.current;
    if (!ta) return;
    ta.focus();
    const stamp = `[${fmtTimestamp(new Date().toISOString())}] - `;
    // Always append after the most recent content so comments stay chronological,
    // regardless of where the cursor was.
    const trimmed = body.replace(/\s+$/, '');
    const separator = trimmed.length > 0 ? '\n\n' : '';
    const newValue = trimmed + separator + stamp;
    updateBody(newValue);
    requestAnimationFrame(() => {
      const cursor = newValue.length;
      ta.setSelectionRange(cursor, cursor);
      // Scroll the textarea (and its scroll container) to the bottom so the
      // new comment is visible.
      ta.scrollTop = ta.scrollHeight;
      const edBody = ta.closest('.nq-ed-body');
      if (edBody) edBody.scrollTop = edBody.scrollHeight;
      ta.focus();
    });
  };

  if (!note) {
    return (
      <main className="nq-ed nq-ed-empty" onContextMenu={onCtxEditor}>
        <div className="nq-empty-mark big"/>
        <div className="nq-empty-t">Select a note</div>
        <div className="nq-empty-s">Or create a new one — ⌘N</div>
      </main>
    );
  }

  const nb = notebooks.find(b => b.id===note.notebook);

  const ingest = async (files) => {
    if (!files || !files.length) return;
    setUploading(u => u + files.length);
    try {
      for (const f of files) {
        try { await onAddAttachment(note.id, f); }
        catch (err) { console.error('Upload failed:', err); alert('Upload failed: ' + err.message); }
      }
    } finally {
      setUploading(u => Math.max(0, u - files.length));
    }
  };
  const onDropFiles = (e) => {
    e.preventDefault(); setDrag(false);
    ingest([...(e.dataTransfer?.files||[])]);
  };
  const onFilePick = (e) => {
    ingest([...(e.target.files||[])]);
    e.target.value = '';
  };

  return (
    <main className={'nq-ed'+(drag?' is-drag':'')}
          onContextMenu={onCtxEditor}
          onDragOver={(e)=>{e.preventDefault(); setDrag(true);}}
          onDragLeave={(e)=>{ if (e.currentTarget===e.target) setDrag(false); }}
          onDrop={onDropFiles}>
      <div className="nq-ed-hd">
        <div className="nq-ed-crumb">
          <div className="nq-tagadd">
            <button className="nq-ed-nb" onClick={()=>setNotebookOpen(o=>!o)}>
              <span className="nq-sb-dot" style={{background:nb?.color}}/>
              {nb?.name || note.notebook}
            </button>
            {notebookOpen && (
              <div className="nq-tagpop" onMouseLeave={()=>setNotebookOpen(false)}>
                {notebooks.map(b => (
                  <button key={b.id} className="nq-tagpop-row"
                          onClick={()=>{
                            onChange(note.id, { notebook: b.id });
                            setNotebookOpen(false);
                          }}>
                    <span className="nq-sb-dot" style={{background:b.color,marginRight:8}}/>
                    {b.name}
                  </button>
                ))}
              </div>
            )}
          </div>
          <span className="nq-crumb-sep">/</span>
          <span className="mono nq-ed-time">edited {fmtRelative(note.updated)}</span>
        </div>
        <div className="nq-ed-actions">
          <button className="nq-comp-btn" onClick={()=>fileRef.current?.click()}>
            <IconPaperclip size={13}/> Attach
            {uploading > 0 && <span className="mono"> · {uploading}…</span>}
          </button>
          <button className={'nq-ic'+(note.starred?' is-on':'')}
                  onClick={()=>onStar(note.id)} title="Star">
            <IconStar size={16} fill={note.starred?'currentColor':'none'}/>
          </button>
          <button className="nq-ic" title="Delete" onClick={()=>onDelete(note.id)}>
            <IconTrash size={16}/>
          </button>
        </div>
      </div>

      <input ref={fileRef} type="file" multiple style={{display:'none'}}
             accept="application/pdf,image/*,video/*"
             onChange={onFilePick}/>

      <div className="nq-ed-body scroll">
        <input className="nq-ed-title" value={note.title}
               placeholder="Untitled note"
               onChange={(e)=>onChange(note.id, { title: e.target.value })}/>

        <div className="nq-ed-tags">
          {(note.tags||[]).map(t => (
            <button key={t} className="nq-chip nq-chip-rm"
                    onClick={()=>onChange(note.id, { tags: note.tags.filter(x=>x!==t) })}>
              #{t} <IconX size={10}/>
            </button>
          ))}
          <div className="nq-tagadd">
            <button className="nq-chip nq-chip-add"
                    onClick={()=>setTagPickerOpen(o=>!o)}>
              <IconPlus size={11}/> tag
            </button>
            {tagPickerOpen && (
              <div className="nq-tagpop" onMouseLeave={()=>setTagPickerOpen(false)}>
                {tagsAll.filter(t=>!note.tags.includes(t.id)).map(t => (
                  <button key={t.id} className="nq-tagpop-row"
                          onClick={()=>{
                            onChange(note.id, { tags: [...note.tags, t.id] });
                            setTagPickerOpen(false);
                          }}>
                    #{t.name}
                  </button>
                ))}
                {tagsAll.filter(t=>!note.tags.includes(t.id)).length===0 && (
                  <div className="nq-tagpop-empty">All tags added</div>
                )}
              </div>
            )}
          </div>
        </div>

        {note.attachments.length > 0 && (
          <div className="nq-atts">
            <div className="nq-section-h">
              <IconPaperclip size={12}/> Attachments
              <span className="mono">{note.attachments.length}</span>
            </div>
            <div className="nq-atts-grid">
              {note.attachments.map(a => (
                <Attachment key={a.id} a={a}
                            onOpen={()=>setViewing(a)}
                            onRemove={()=>onRemoveAttachment(note.id, a)}/>
              ))}
            </div>
          </div>
        )}

        <div className="nq-body-toolbar">
          <FormatToolbar taRef={bodyRef} value={body} onChange={updateBody}/>
          <div className="nq-body-tools">
            <button className="nq-comment-btn"
                    onClick={addComment}
                    title="Insert a date & time stamp followed by a dash">
              <IconClock size={13}/> Comment
            </button>
            <button className={'nq-fmt-toggle '+(preview?'is-on':'')}
                    onClick={()=>setPreview(p=>!p)}
                    title="Toggle preview">
              {preview ? 'Edit' : 'Preview'}
            </button>
          </div>
        </div>

        {preview ? (
          <div className="nq-md nq-body-preview"
               dangerouslySetInnerHTML={{__html: renderMarkdown(body) || '<p style="color:var(--ink-3)">Nothing to preview yet.</p>'}}/>
        ) : (
          <textarea ref={bodyRef} className="nq-body-ta"
                    value={body}
                    placeholder="Write your note… markdown is supported."
                    onChange={(e)=>updateBody(e.target.value)}/>
        )}
      </div>

      {drag && (
        <div className="nq-drop">
          <div className="nq-drop-card">
            <IconPaperclip size={20}/>
            <div>Drop files to attach</div>
          </div>
        </div>
      )}

      {viewing && <AttachmentViewer att={viewing} onClose={()=>setViewing(null)}/>}
    </main>
  );
}

// ─── Auth gate ───────────────────────────────────────────────────────────────
// Tracks the Supabase session, MFA factor state, and AAL. Returns either a
// sign-in/enroll/challenge screen, or `children` once the session is AAL2.

function useAuthSession() {
  const [state, setState] = React.useState({
    loading: true,
    user: null,
    currentLevel: null,
    nextLevel: null,
    factors: [],
  });

  const refresh = React.useCallback(async () => {
    try {
      const user = await getCurrentUser();
      if (!user) {
        setState({ loading: false, user: null, currentLevel: null, nextLevel: null, factors: [] });
        return;
      }
      const [aal, facts] = await Promise.all([getAALStatus(), listMFAFactors()]);
      setState({
        loading: false,
        user,
        currentLevel: aal.currentLevel || 'aal1',
        nextLevel:    aal.nextLevel    || 'aal1',
        factors:      facts.totp || [],
      });
    } catch (err) {
      console.error('auth refresh failed', err);
      setState({ loading: false, user: null, currentLevel: null, nextLevel: null, factors: [] });
    }
  }, []);

  React.useEffect(() => {
    refresh();
    const unsub = onAuthStateChange(() => { refresh(); });
    return unsub;
  }, [refresh]);

  return { ...state, refresh };
}

function AuthGate({ children }) {
  const auth = useAuthSession();

  if (auth.loading) {
    return <div className="nq-auth"><div className="nq-auth-card"><div className="nq-auth-h">Loading…</div></div></div>;
  }
  if (!auth.user) {
    return <SignInScreen onDone={auth.refresh}/>;
  }
  const hasVerified = (auth.factors || []).some(f => f.status === 'verified');

  // No TOTP enrolled yet → require enrollment now. Any account without an
  // authenticator app must add one before they can see notes.
  if (!hasVerified) {
    return <EnrollScreen onDone={auth.refresh} userEmail={auth.user.email}/>;
  }
  // Verified factor exists but session is still AAL1 → challenge.
  if (auth.currentLevel !== 'aal2') {
    return <ChallengeScreen factor={auth.factors.find(f => f.status === 'verified')}
                            onDone={auth.refresh} userEmail={auth.user.email}/>;
  }
  return children;
}

function SignInScreen({ onDone }) {
  const [mode, setMode]   = React.useState('signin'); // 'signin' | 'signup'
  const [email, setEmail] = React.useState('');
  const [pw, setPw]       = React.useState('');
  const [err, setErr]     = React.useState('');
  const [busy, setBusy]   = React.useState(false);
  const [notice, setNotice] = React.useState('');

  const submit = async (e) => {
    e.preventDefault();
    setErr(''); setNotice(''); setBusy(true);
    try {
      if (mode === 'signin') {
        await signInWithPassword(email.trim(), pw);
        onDone();
      } else {
        const weak = validatePasswordStrength(pw);
        if (weak) { setErr(weak); setBusy(false); return; }
        const res = await signUpWithPassword(email.trim(), pw);
        if (!res.session) {
          // Email confirmation is required by the project's auth settings.
          setNotice('Check your email to confirm your address, then sign in.');
          setMode('signin');
        } else {
          onDone();
        }
      }
    } catch (e2) {
      setErr(e2.message || String(e2));
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="nq-auth">
      <div className="nq-auth-card">
        <div className="nq-auth-brand">
          <div className="nq-brand-mark"><span/></div>
          <div className="nq-brand-name">Notequick</div>
        </div>
        <div className="nq-auth-h">{mode === 'signin' ? 'Sign in' : 'Create an account'}</div>
        <div className="nq-auth-sub">
          {mode === 'signin'
            ? 'Welcome back. An authenticator code is required after sign-in.'
            : 'After signing up you’ll set up an authenticator app.'}
        </div>
        <form onSubmit={submit} className="nq-auth-form">
          <label className="nq-auth-lbl">
            <span>Email</span>
            <input type="email" required autoComplete="email"
                   value={email} onChange={e => setEmail(e.target.value)}
                   className="nq-auth-in" placeholder="you@example.com"/>
          </label>
          <label className="nq-auth-lbl">
            <span>Password</span>
            <input type="password" required minLength={mode === 'signin' ? 8 : 10}
                   autoComplete={mode === 'signin' ? 'current-password' : 'new-password'}
                   value={pw} onChange={e => setPw(e.target.value)}
                   className="nq-auth-in" placeholder="••••••••"/>
            {mode === 'signup' && (
              <span className="nq-auth-hint">
                At least 10 characters, with upper- and lower-case letters and a number.
              </span>
            )}
          </label>
          {err && <div className="nq-auth-err">{err}</div>}
          {notice && <div className="nq-auth-notice">{notice}</div>}
          <button type="submit" className="nq-auth-btn" disabled={busy}>
            {busy ? 'Working…' : (mode === 'signin' ? 'Sign in' : 'Create account')}
          </button>
        </form>
        <button type="button" className="nq-auth-link"
                onClick={() => { setErr(''); setNotice(''); setMode(mode === 'signin' ? 'signup' : 'signin'); }}>
          {mode === 'signin' ? 'No account? Create one' : 'Already have an account? Sign in'}
        </button>
      </div>
    </div>
  );
}

function EnrollScreen({ onDone, userEmail }) {
  const [factor, setFactor] = React.useState(null); // { factorId, qr, secret, uri }
  const [code, setCode]     = React.useState('');
  const [err, setErr]       = React.useState('');
  const [busy, setBusy]     = React.useState(false);
  const [enrolling, setEnrolling] = React.useState(true);
  const [showSecret, setShowSecret] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const f = await enrollTOTP();
        if (!cancelled) setFactor(f);
      } catch (e) {
        if (!cancelled) setErr(e.message || String(e));
      } finally {
        if (!cancelled) setEnrolling(false);
      }
    })();
    return () => { cancelled = true; };
  }, []);

  const submit = async (e) => {
    e.preventDefault();
    setErr(''); setBusy(true);
    try {
      await verifyTOTPEnrollment(factor.factorId, code.trim());
      onDone();
    } catch (e2) {
      setErr(e2.message || String(e2));
    } finally {
      setBusy(false);
    }
  };

  const onCancel = async () => {
    await signOut();
    onDone();
  };

  return (
    <div className="nq-auth">
      <div className="nq-auth-card">
        <div className="nq-auth-h">Set up your authenticator</div>
        <div className="nq-auth-sub">
          Signed in as <b>{userEmail}</b>. Notequick requires an authenticator app
          (1Password, Authy, Google Authenticator, etc.) for every account.
        </div>

        {enrolling && <div className="nq-auth-loading">Generating a secret…</div>}

        {!enrolling && err && !factor && (
          <div className="nq-auth-err">{err}</div>
        )}

        {factor && (
          <>
            <ol className="nq-auth-steps">
              <li>Open your authenticator app.</li>
              <li>Scan the QR code below — or enter the setup key manually.</li>
              <li>Enter the 6-digit code your app shows.</li>
            </ol>

            <div className="nq-auth-qr"
                 dangerouslySetInnerHTML={{ __html: sanitizeSVG(factor.qr) }}/>

            <button type="button" className="nq-auth-link" onClick={() => setShowSecret(s => !s)}>
              {showSecret ? 'Hide setup key' : 'Show setup key'}
            </button>
            {showSecret && (
              <div className="nq-auth-secret mono">{factor.secret}</div>
            )}

            <form onSubmit={submit} className="nq-auth-form">
              <label className="nq-auth-lbl">
                <span>6-digit code</span>
                <input type="text" inputMode="numeric" pattern="[0-9]{6}"
                       maxLength={6} required autoComplete="one-time-code"
                       value={code} onChange={e => setCode(e.target.value.replace(/\D/g, ''))}
                       className="nq-auth-in mono nq-auth-otp" placeholder="123456"/>
              </label>
              {err && <div className="nq-auth-err">{err}</div>}
              <button type="submit" className="nq-auth-btn" disabled={busy || code.length !== 6}>
                {busy ? 'Verifying…' : 'Verify & enable'}
              </button>
            </form>
          </>
        )}

        <button type="button" className="nq-auth-link nq-auth-link-muted" onClick={onCancel}>
          Cancel and sign out
        </button>
      </div>
    </div>
  );
}

function ChallengeScreen({ factor, onDone, userEmail }) {
  const [code, setCode] = React.useState('');
  const [err, setErr]   = React.useState('');
  const [busy, setBusy] = React.useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setErr(''); setBusy(true);
    try {
      await challengeAndVerifyTOTP(factor.id, code.trim());
      onDone();
    } catch (e2) {
      setErr(e2.message || String(e2));
    } finally {
      setBusy(false);
    }
  };

  const onCancel = async () => {
    await signOut();
    onDone();
  };

  return (
    <div className="nq-auth">
      <div className="nq-auth-card">
        <div className="nq-auth-brand">
          <div className="nq-brand-mark"><span/></div>
          <div className="nq-brand-name">Notequick</div>
        </div>
        <div className="nq-auth-h">Enter your code</div>
        <div className="nq-auth-sub">
          Signed in as <b>{userEmail}</b>. Open your authenticator app to get a
          6-digit code.
        </div>
        <form onSubmit={submit} className="nq-auth-form">
          <label className="nq-auth-lbl">
            <span>6-digit code</span>
            <input type="text" inputMode="numeric" pattern="[0-9]{6}"
                   maxLength={6} required autoFocus autoComplete="one-time-code"
                   value={code} onChange={e => setCode(e.target.value.replace(/\D/g, ''))}
                   className="nq-auth-in mono nq-auth-otp" placeholder="123456"/>
          </label>
          {err && <div className="nq-auth-err">{err}</div>}
          <button type="submit" className="nq-auth-btn" disabled={busy || code.length !== 6}>
            {busy ? 'Verifying…' : 'Verify'}
          </button>
        </form>
        <button type="button" className="nq-auth-link nq-auth-link-muted" onClick={onCancel}>
          Sign out
        </button>
      </div>
    </div>
  );
}

Object.assign(window, {
  Sidebar, NotesList, Editor, Attachment, AttachmentViewer,
  ContextMenu, useContextMenu, FormatToolbar, ImportProgress,
  AuthGate, SignInScreen, EnrollScreen, ChallengeScreen, useAuthSession,
  fmtRelative, fmtTimestamp, fmtDayHeader, dayKey,
});
