// PHYSIO SAMOS — admin console: the formatted-text editor (PSA.RichText).
//
// Writers type straight into formatted text and never see Markdown; the post is still stored as the
// blog's Markdown subset. The engine is public/admin/editor-kit-core.js (ProseMirror), published as
// window.PSEditorKit by editor-kit.js; this file is the console's face for it: the formatting bar,
// the link and image dialogs, the bubbles over a link or an image, and the notices under the text.
// Controlled like a text field: `value` in (outside changes are loaded, echoes ignored), Markdown out
// through onChange. There is no fallback to a Markdown text box, ever.
(() => {
  const { useState, useEffect, useLayoutEffect, useRef, useMemo } = React;
  const PSA = (window.PSA = window.PSA || {});

  const SITE_PAGES = [
    { value: "/", label: "Αρχική" }, { value: "/services", label: "Υπηρεσίες" }, { value: "/clinic", label: "Ο χώρος" },
    { value: "/about", label: "Σχετικά" }, { value: "/blog", label: "Άρθρα" },
  ];
  const LINK_KINDS = [
    { value: "page", label: "Σελίδα του ιστότοπου" },
    { value: "post", label: "Άρθρο του blog" },
    { value: "email", label: "Email" },
    { value: "phone", label: "Τηλέφωνο" },
    { value: "web", label: "Άλλος ιστότοπος" },
  ];
  const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  const PHONE_RE = /^\+?\d{6,15}$/;
  const SITE_HOST = /^(www\.)?physiosamos\.gr$/i;
  const postTitles = new Map();          // "/blog/<slug>" -> title, filled whenever the post list loads
  const decode = (s) => { try { return decodeURIComponent(s); } catch (e) { return s; } };

  // What a link points at, in the dialog's terms: a kind and the value its field holds.
  const kindOf = (href) => {
    const h = String(href || "");
    if (SITE_PAGES.some((p) => p.value === h)) return { kind: "page", value: h };
    if (/^\/blog\/[^/?#]+/.test(h)) return { kind: "post", value: h };
    if (/^mailto:/i.test(h)) return { kind: "email", value: decode(h.slice(7)) };
    if (/^tel:/i.test(h)) return { kind: "phone", value: h.slice(4) };
    if (/^https?:\/\//i.test(h)) {
      try {
        const u = new URL(h);
        if (SITE_HOST.test(u.hostname)) return { kind: "page", value: `${u.pathname}${u.search}${u.hash}` || "/" };
      } catch (e) {}
      return { kind: "web", value: decode(h) };
    }
    return { kind: "page", value: h };
  };
  const describeHref = (href) => {
    const h = String(href || "");
    const page = SITE_PAGES.find((p) => p.value === h);
    if (page) return `Σελίδα: ${page.label}`;
    const post = /^\/blog\/([^/?#]+)/.exec(h);
    if (post) return `Άρθρο: ${postTitles.get(`/blog/${post[1]}`) || `/blog/${post[1]}`}`;
    if (/^mailto:/i.test(h)) return `Email: ${decode(h.slice(7))}`;
    if (/^tel:/i.test(h)) return `Τηλέφωνο: ${h.slice(4)}`;
    if (/^https?:\/\//i.test(h)) {
      try {
        const u = new URL(h);
        return SITE_HOST.test(u.hostname) ? `Σελίδα: ${u.pathname}` : `Ιστότοπος: ${u.hostname.replace(/^www\./i, "")}`;
      } catch (e) {}
    }
    return `Σελίδα: ${h}`;
  };
  const absoluteHref = (href, origin) => (/^[/#]/.test(href) ? `${origin || "https://physiosamos.gr"}${href}` : href);

  PSA.useEditorKit = () => {
    const [kit, setKit] = useState(() => window.PSEditorKit || null);
    const [failed, setFailed] = useState(false);
    useEffect(() => {
      if (kit) return undefined;
      const on = () => { if (window.PSEditorKit) setKit(window.PSEditorKit); };
      window.addEventListener("ps:editorkit", on);
      on();
      const timer = setTimeout(() => { if (!window.PSEditorKit) setFailed(true); }, 10000);
      return () => { window.removeEventListener("ps:editorkit", on); clearTimeout(timer); };
    }, [kit]);
    return { kit, failed };
  };

  const Tool = ({ icon, label, pressed, disabled, onClick }) => (
    <button type="button" className="tool" data-rt-control="" aria-label={label} title={label}
            aria-pressed={pressed === undefined ? undefined : pressed ? "true" : "false"} disabled={disabled}
            onMouseDown={(e) => e.preventDefault()} onClick={onClick}>
      <PSA.Icon name={icon} />
    </button>
  );

  // ---------- link dialog: no Markdown, no brackets ----------
  const LinkDialog = ({ kit, init, onClose, onApply, onRemove }) => {
    const pre = init.editing ? kindOf(init.href) : { kind: "page", value: "" };
    const [kind, setKind] = useState(pre.kind);
    const [vals, setVals] = useState(() => ({ page: "", post: "", email: "", phone: "", web: "", [pre.kind]: pre.value }));
    const [text, setText] = useState("");
    const [attempted, setAttempted] = useState(false);
    const [touched, setTouched] = useState({});
    const [refused, setRefused] = useState(false);
    const [posts, setPosts] = useState(null);
    const [postsError, setPostsError] = useState(null);
    const first = useRef(null);
    const Select = PSA.Select;

    // The dialog opens with showModal() in its own effect, which runs before this one.
    useEffect(() => {
      const el = init.needText ? document.getElementById("rt-link-text") : document.getElementById("rt-link-kind");
      if (el) el.focus();
    }, []);
    useEffect(() => {
      if (kind !== "post" || posts) return undefined;
      let alive = true;
      PSA.api("/api/posts?state=published").then((d) => {
        if (!alive) return;
        const list = ((d && d.posts) || []).filter((p) => p && p.slug).map((p) => ({ value: `/blog/${p.slug}`, label: p.title || "Χωρίς τίτλο", description: `/blog/${p.slug}` }));
        list.forEach((o) => postTitles.set(o.value, o.label));
        setPosts(list);
      }, (err) => { if (alive) { setPosts([]); setPostsError(err); } });
      return () => { alive = false; };
    }, [kind]);

    const set = (k, v) => { setVals((x) => ({ ...x, [k]: v })); setRefused(false); };
    const touch = (k) => setTouched((t) => (t[k] ? t : { ...t, [k]: true }));
    const target = (() => {
      switch (kind) {
        case "page": return vals.page ? { href: vals.page } : { error: "Διαλέξτε σελίδα." };
        case "post": return vals.post ? { href: vals.post } : { error: "Διαλέξτε άρθρο." };
        case "email": {
          const v = vals.email.trim();
          return EMAIL_RE.test(v) ? { href: `mailto:${v}` } : { error: "Γράψτε μια έγκυρη διεύθυνση email." };
        }
        case "phone": {
          const v = vals.phone.replace(/[^\d+]/g, "");
          return PHONE_RE.test(v) ? { href: `tel:${v}` } : { error: "Γράψτε έναν έγκυρο αριθμό τηλεφώνου." };
        }
        default: {
          let v = vals.web.trim();
          const scheme = /^[a-z][a-z0-9+.-]*:/i.test(v) && !/^[^:/]+:\d/.test(v);
          if (v && !scheme) v = `https://${v.replace(/^\/+/, "")}`;
          const clean = v ? kit.cleanHref(v) : null;
          return clean && /^https?:\/\//i.test(clean) ? { href: clean } : { error: "Γράψτε μια έγκυρη διεύθυνση ιστότοπου." };
        }
      }
    })();
    const textError = init.needText && !text.trim() ? "Γράψτε το κείμενο του συνδέσμου." : null;
    const submit = () => {
      setAttempted(true);
      if (target.error || textError) return;
      if (!onApply({ href: target.href, text: init.needText ? text.trim() : undefined })) setRefused(true);
    };
    const onEnter = (e) => { if (e.key === "Enter" && !e.nativeEvent.isComposing) { e.preventDefault(); submit(); } };
    const valueError = refused ? "Η διεύθυνση δεν επιτρέπεται." : attempted || touched[kind] ? target.error || null : null;

    const withCurrent = (list, v) => (v && !list.some((o) => o.value === v) ? [{ value: v, label: v }, ...list] : list);
    const pageOptions = withCurrent(SITE_PAGES.map((p) => ({ ...p, description: p.value })), vals.page);
    const postOptions = withCurrent(posts || [], vals.post);

    return (
      <PSA.Dialog open title={init.editing ? "Αλλαγή συνδέσμου" : "Σύνδεσμος"} onClose={onClose} footer={<>
        {init.editing && (
          <button type="button" className="btn btn--quiet" style={{ marginRight: "auto" }} onClick={onRemove}>
            <PSA.Icon name="unlink" size="sm" />Αφαίρεση συνδέσμου
          </button>
        )}
        <button type="button" className="btn" onClick={onClose}>Άκυρο</button>
        <button type="button" className="btn btn--primary" onClick={submit}>{init.editing ? "Αποθήκευση" : "Εισαγωγή"}</button>
      </>}>
        {init.needText ? (
          <PSA.Field label="Κείμενο συνδέσμου" htmlFor="rt-link-text" error={attempted || touched.text ? textError : null}>
            <input ref={first} id="rt-link-text" className="input" value={text} maxLength={200}
                   onChange={(e) => setText(e.target.value)} onBlur={() => touch("text")} onKeyDown={onEnter} />
          </PSA.Field>
        ) : (
          <p className="hint">Κείμενο: «{String(init.shownText || "").slice(0, 80)}»</p>
        )}
        <PSA.Field label="Πού οδηγεί" htmlFor="rt-link-kind">
          <Select id="rt-link-kind" ariaLabel="Είδος συνδέσμου" sheetTitle="Πού οδηγεί" value={kind} options={LINK_KINDS}
                  onChange={(v) => { setKind(v); setRefused(false); }} />
        </PSA.Field>
        {kind === "page" && (
          <PSA.Field htmlFor="rt-link-page" error={valueError}>
            <Select id="rt-link-page" ariaLabel="Σελίδα του ιστότοπου" sheetTitle="Σελίδα του ιστότοπου" value={vals.page || null}
                    options={pageOptions} invalid={!!valueError} onChange={(v) => set("page", v)} />
          </PSA.Field>
        )}
        {kind === "post" && (
          <PSA.Field htmlFor="rt-link-post" error={valueError}>
            {posts === null ? <PSA.Spinner /> : (
              <Select id="rt-link-post" ariaLabel="Άρθρο του blog" sheetTitle="Άρθρο του blog" value={vals.post || null}
                      options={postOptions} invalid={!!valueError} emptyText="Δεν υπάρχει ακόμη δημοσιευμένο άρθρο."
                      onChange={(v) => set("post", v)} />
            )}
            {postsError && <PSA.ErrorNotice error={postsError} />}
          </PSA.Field>
        )}
        {kind === "email" && (
          <PSA.Field htmlFor="rt-link-email" error={valueError}>
            <input id="rt-link-email" className="input" type="email" inputMode="email" aria-label="Email" placeholder="π.χ. info@physiosamos.gr"
                   value={vals.email} onChange={(e) => set("email", e.target.value)} onBlur={() => touch("email")} onKeyDown={onEnter} />
          </PSA.Field>
        )}
        {kind === "phone" && (
          <PSA.Field htmlFor="rt-link-phone" error={valueError}>
            <input id="rt-link-phone" className="input" type="tel" inputMode="tel" aria-label="Τηλέφωνο" placeholder="π.χ. 22730 78420"
                   value={vals.phone} onChange={(e) => set("phone", e.target.value)} onBlur={() => touch("phone")} onKeyDown={onEnter} />
          </PSA.Field>
        )}
        {kind === "web" && (
          <PSA.Field htmlFor="rt-link-web" error={valueError}>
            <input id="rt-link-web" className="input" type="url" inputMode="url" aria-label="Άλλος ιστότοπος" placeholder="π.χ. www.efet.gr"
                   value={vals.web} onChange={(e) => set("web", e.target.value)} onBlur={() => touch("web")} onKeyDown={onEnter} />
          </PSA.Field>
        )}
      </PSA.Dialog>
    );
  };

  // ---------- image dialog ----------
  const ImageDialog = ({ kit, img, onClose, onSave, onReplace, onRemove }) => {
    const [alt, setAlt] = useState(img.alt || "");
    const [caption, setCaption] = useState(img.caption || "");
    const altRef = useRef(null);
    useEffect(() => { if (altRef.current) altRef.current.focus(); }, []);
    const info = kit.blogText.mediaInfo(img.src);
    return (
      <PSA.Dialog open wide title="Εικόνα" onClose={onClose} footer={<>
        <button type="button" className="btn btn--quiet" onClick={onReplace}>Αντικατάσταση εικόνας</button>
        <button type="button" className="btn btn--quiet btn--danger-text" onClick={onRemove}>Αφαίρεση από το κείμενο</button>
        <span style={{ flex: 1 }} />
        <button type="button" className="btn" onClick={onClose}>Άκυρο</button>
        <button type="button" className="btn btn--primary" disabled={!alt.trim()} onClick={() => onSave({ alt, caption })}>Αποθήκευση</button>
      </>}>
        <img className="rt-img-preview" src={(info && info.small) || img.src} alt="" draggable="false" />
        <PSA.Field label="Περιγραφή εικόνας" htmlFor="rt-img-alt" count={alt.length} max={250} hint="Τι δείχνει η εικόνα. Απαραίτητη για να δημοσιευτεί το άρθρο.">
          <textarea ref={altRef} id="rt-img-alt" className="textarea" rows={3} value={alt} onChange={(e) => setAlt(e.target.value.replace(/[[\]]/g, ""))} />
        </PSA.Field>
        <PSA.Field label="Λεζάντα" optional htmlFor="rt-img-caption" count={caption.length} max={200} hint="Εμφανίζεται κάτω από την εικόνα.">
          <input id="rt-img-caption" className="input" value={caption} onChange={(e) => setCaption(e.target.value.replace(/"/g, "”"))} />
        </PSA.Field>
      </PSA.Dialog>
    );
  };

  // ---------- the editor ----------
  PSA.RichText = (props) => {
    const { id, value, readOnly = false, handleRef } = props;
    const { kit, failed } = PSA.useEditorKit();
    const session = PSA.useSession ? PSA.useSession() : null;
    const rtRef = useRef(null);
    const mount = useRef(null);
    const toolsRef = useRef(null);
    const bubbleRef = useRef(null);
    const editorRef = useRef(null);
    const latest = useRef(props);
    latest.current = props;
    const lastEmitted = useRef(value);
    const [ready, setReady] = useState(false);
    const [tb, setTb] = useState(null);
    const [focusedOnce, setFocusedOnce] = useState(false);
    const [problems, setProblems] = useState([]);
    const [missingAlt, setMissingAlt] = useState([]);
    const [externalAtLoad, setExternalAtLoad] = useState(false);
    const [countMd, setCountMd] = useState(value || "");
    const [linkDlg, setLinkDlg] = useState(null);
    const [imgDlg, setImgDlg] = useState(null);
    const [bubbleFocus, setBubbleFocus] = useState(false);
    const [, setScrollTick] = useState(0);
    const grammarBad = !!kit && !kit.grammarOk;
    const ro = !!readOnly || grammarBad;
    const roRef = useRef(ro);
    roRef.current = ro;

    const sameList = (a, b) => a.length === b.length && a.every((x, i) => x.pos === b[i].pos && x.src === b[i].src);
    const refreshAlt = () => { const ed = editorRef.current; if (ed) { const next = ed.imagesMissingAlt(); setMissingAlt((cur) => (sameList(cur, next) ? cur : next)); } };
    const afterLoad = (warnings) => {
      const ed = editorRef.current;
      if (!ed) return;
      setExternalAtLoad((warnings || []).some((w) => w.code === "image_external"));
      setProblems(ed.problems());
      refreshAlt();
    };
    const focusEditor = () => { const ed = editorRef.current; if (ed && !ed.view.isDestroyed) ed.view.focus(); };
    const refocusSoon = () => setTimeout(focusEditor, 30);

    const controls = () => (toolsRef.current ? [...toolsRef.current.querySelectorAll(".rt-block, [data-rt-control]")] : []);
    const actions = useRef({});
    actions.current = {
      openLink() {
        const ed = editorRef.current;
        if (!ed || roRef.current) return;
        const st = ed.view.state, sel = st.selection, link = ed.state().link;
        setLinkDlg({
          editing: !!link, href: link ? link.href : "", needText: sel.empty && !link,
          shownText: link && sel.empty ? link.text : st.doc.textBetween(sel.from, sel.to, " "),
        });
      },
      openImage(pos) {
        const ed = editorRef.current;
        if (!ed || roRef.current) return;
        const n = pos >= 0 && pos < ed.view.state.doc.content.size ? ed.view.state.doc.nodeAt(pos) : null;
        if (!n || n.type.name !== "image") return;
        setImgDlg({ pos, src: n.attrs.src, alt: n.attrs.alt, caption: n.attrs.caption });
      },
      focusToolbar() { const first = controls().find((el) => !el.disabled); if (first) first.focus(); },
      pasteReport(r) {
        const n = (r.imagesDropped || 0) + (r.dataImages || 0);
        if (n > 0) PSA.toast(`${PSA.plural(n, "εικόνα", "εικόνες")} από άλλο ιστότοπο δεν επικολλήθηκαν. Ανεβάστε τις από το κουμπί Εικόνα.`, { kind: "error" });
        if (r.filesIgnored > 0) PSA.toast("Οι εικόνες δεν επικολλήθηκαν μαζί με το κείμενο. Προσθέστε τις από το κουμπί Εικόνα.", { kind: "error" });
      },
      focusProblem(p) {
        const ed = editorRef.current;
        if (!ed || !p) return;
        if (p.code === "image_alt" && p.src) {
          if (ed.selectImage(p.src)) { const img = ed.state().image; if (img) actions.current.openImage(img.pos); }
          return;
        }
        if (p.index != null) { ed.selectBlock(p.index); return; }
        ed.focus();
      },
    };

    // Created once per mount; callbacks read the latest props through refs, so a new closure never
    // recreates the editor (and never loses its undo history).
    useEffect(() => {
      if (!kit || !mount.current) return undefined;
      const p = latest.current;
      const ed = kit.createEditor(mount.current, {
        markdown: p.value, id: p.id, lang: p.lang || "el", label: p.label || "Κείμενο άρθρου", placeholder: p.placeholder || "",
        readOnly: !!p.readOnly || !kit.grammarOk,
        onChange: (md, probs) => {
          lastEmitted.current = md;
          setProblems(probs);
          setCountMd(md);
          setExternalAtLoad(false);
          refreshAlt();
          if (latest.current.onChange) latest.current.onChange(md, { problems: probs });
        },
        onState: (s) => { setTb(s); if (s.focused) setFocusedOnce(true); },
        onLinkRequest: () => actions.current.openLink(),
        onFiles: p.onFiles ? (files, insert) => { if (latest.current.onFiles) latest.current.onFiles(files, insert); } : undefined,
        onPasteReport: (r) => actions.current.pasteReport(r),
        onImageActivate: (attrs) => actions.current.openImage(attrs.pos),
        onToolbarFocus: () => actions.current.focusToolbar(),
      });
      editorRef.current = ed;
      lastEmitted.current = p.value;
      setCountMd(p.value || "");
      setTb(ed.state());
      afterLoad(ed.loadWarnings());
      setReady(true);
      return () => { ed.destroy(); editorRef.current = null; };
    }, [kit]);

    // An outside change: a restored revision, a machine translation, a conflict reload.
    useEffect(() => {
      const ed = editorRef.current;
      if (!ed || value === lastEmitted.current) return;
      const r = ed.setMarkdown(value);
      lastEmitted.current = value;
      setCountMd(value || "");
      afterLoad(r.warnings);
    }, [value]);

    useEffect(() => { const ed = editorRef.current; if (ed) ed.setEditable(!ro); }, [ro, ready]);

    useEffect(() => {
      if (!handleRef) return undefined;
      const handle = {
        focus: focusEditor,
        focusProblem: (p) => actions.current.focusProblem(p),
        selectImage: (src) => { const ed = editorRef.current; return ed ? ed.selectImage(src) : false; },
        getMarkdown: () => { const ed = editorRef.current; return ed ? ed.getMarkdown() : String(latest.current.value || ""); },
        getProblems: () => { const ed = editorRef.current; return ed ? ed.problems() : []; },
        get editor() { return editorRef.current; },      // the engine's API, for tests and diagnostics
      };
      handleRef.current = handle;
      return () => { if (handleRef.current === handle) handleRef.current = null; };
    }, [handleRef]);

    // The toolbar is one Tab stop: the first enabled control takes it, the arrow keys move inside.
    useLayoutEffect(() => {
      const all = controls();
      const first = all.find((el) => !el.disabled);
      all.forEach((el) => { el.tabIndex = el === first ? 0 : -1; });
    });
    const onToolsKeyDown = (e) => {
      if (e.defaultPrevented) return;
      if (e.key === "Escape") { e.preventDefault(); focusEditor(); return; }
      const all = controls().filter((el) => !el.disabled);
      if (!all.length) return;
      const i = all.findIndex((el) => el === document.activeElement || el.contains(document.activeElement));
      let next = null;
      if (e.key === "ArrowRight") next = all[(i + 1) % all.length];
      else if (e.key === "ArrowLeft") next = all[(i - 1 + all.length) % all.length];
      else if (e.key === "Home") next = all[0];
      else if (e.key === "End") next = all[all.length - 1];
      if (next) { e.preventDefault(); next.focus(); next.scrollIntoView({ block: "nearest", inline: "nearest" }); }
    };

    // Bubbles follow the page as it scrolls, and hide once their anchor leaves the editor's box.
    useEffect(() => {
      let frame = 0;
      const on = () => { if (!frame) frame = requestAnimationFrame(() => { frame = 0; setScrollTick((n) => n + 1); }); };
      window.addEventListener("scroll", on, true);
      window.addEventListener("resize", on);
      return () => { window.removeEventListener("scroll", on, true); window.removeEventListener("resize", on); if (frame) cancelAnimationFrame(frame); };
    }, []);
    useLayoutEffect(() => {
      const b = bubbleRef.current, rt = rtRef.current;
      if (!b || !rt) return;
      const max = rt.clientWidth - b.offsetWidth - 8;
      if (b.offsetLeft > max) b.style.left = `${Math.max(8, max)}px`;
    });

    const words = useMemo(() => (kit ? kit.blogText.plainText(countMd).split(/\s+/).filter(Boolean).length : 0), [kit, countMd]);

    if (!kit && failed) {
      return (
        <div className="notice notice--danger" role="alert">
          <PSA.Icon name="warning" size="sm" />
          <div>Ο επεξεργαστής κειμένου δεν φόρτωσε. Ανανεώστε τη σελίδα.</div>
        </div>
      );
    }

    const ed = editorRef.current;
    const t = tb || { can: {} };
    const off = !ready || ro;
    const cmd = (name) => () => { const e = editorRef.current; if (e) e.commands[name](); };
    const pickImage = () => {
      const e = editorRef.current, pick = latest.current.onPickImage;
      if (e && pick && !roRef.current) pick((attrs) => e.commands.insertImage(attrs));
    };

    // ---- bubble ----
    let bubble = null;
    const showLink = !!(ed && t.link && (t.focused || bubbleFocus) && !linkDlg && !imgDlg);
    const showImage = !!(ed && !showLink && t.image && (t.focused || bubbleFocus) && !linkDlg && !imgDlg);
    if ((showLink || showImage) && rtRef.current) {
      let place = null;
      try {
        const box = rtRef.current.getBoundingClientRect();
        let anchor = null;
        if (showLink) { const c = ed.view.coordsAtPos(t.link.from); anchor = { left: c.left, top: c.top, bottom: c.bottom }; }
        else { const dom = ed.view.nodeDOM(t.image.pos); if (dom && dom.getBoundingClientRect) { const r = dom.getBoundingClientRect(); anchor = { left: r.left, top: r.top, bottom: r.bottom }; } }
        const toolsBottom = toolsRef.current ? toolsRef.current.getBoundingClientRect().bottom : box.top;
        if (anchor && anchor.bottom >= box.top && anchor.top <= box.bottom && anchor.bottom + 6 >= toolsBottom) {
          place = { left: Math.max(8, Math.round(anchor.left - box.left)), top: Math.round(anchor.bottom - box.top + 6) };
        }
      } catch (e) { place = null; }
      if (place) {
        const keep = (e) => e.preventDefault();
        bubble = (
          <div ref={bubbleRef} className="rt-bubble" style={{ left: place.left, top: place.top }}
               onFocus={() => setBubbleFocus(true)}
               onBlur={(e) => { if (!e.currentTarget.contains(e.relatedTarget)) setBubbleFocus(false); }}
               onKeyDown={(e) => { if (e.key === "Escape") { e.preventDefault(); focusEditor(); } }}>
            {showLink ? <>
              <span className="rt-bubble__text">{describeHref(t.link.href)}</span>
              {!ro && <button type="button" className="btn btn--sm btn--quiet" onMouseDown={keep} onClick={() => actions.current.openLink()}><PSA.Icon name="edit" size="sm" />Αλλαγή</button>}
              {!ro && <button type="button" className="btn btn--sm btn--quiet" onMouseDown={keep} onClick={() => { const e = editorRef.current; if (e) e.commands.removeLink(); }}><PSA.Icon name="unlink" size="sm" />Αφαίρεση</button>}
              <a className="btn btn--sm btn--quiet" href={absoluteHref(t.link.href, session && session.publicOrigin)} target="_blank" rel="noopener" onMouseDown={keep}><PSA.Icon name="external" size="sm" />Άνοιγμα</a>
            </> : <>
              <span className="rt-bubble__text">Εικόνα</span>
              {!ro && <button type="button" className="btn btn--sm btn--quiet" onMouseDown={keep} onClick={() => actions.current.openImage(t.image.pos)}><PSA.Icon name="edit" size="sm" />Επεξεργασία</button>}
              {!ro && <button type="button" className="btn btn--sm btn--quiet" onMouseDown={keep} onClick={() => { const e = editorRef.current; if (e) e.commands.removeImageAt(t.image.pos); }}><PSA.Icon name="trash" size="sm" />Αφαίρεση</button>}
            </>}
          </div>
        );
      }
    }

    // ---- notices ----
    const notices = [];
    if (grammarBad) {
      notices.push(
        <div key="grammar" className="notice notice--danger" role="alert"><PSA.Icon name="warning" size="sm" />
          <div>Ο επεξεργαστής δεν ταιριάζει με την έκδοση του ιστότοπου. Ανανεώστε τη σελίδα.</div></div>,
      );
    }
    missingAlt.forEach((m) => notices.push(
      <div key={`alt-${m.pos}`} className="notice notice--warn"><PSA.Icon name="warning" size="sm" />
        <div>Μια εικόνα δεν έχει περιγραφή. Χωρίς αυτή το άρθρο δεν δημοσιεύεται.</div>
        {!ro && <button type="button" className="btn btn--sm" onClick={() => { const e = editorRef.current; if (e && e.selectImage(m.src)) actions.current.openImage(m.pos); }}>Προσθήκη περιγραφής</button>}
      </div>,
    ));
    if (externalAtLoad) {
      notices.push(
        <div key="external" className="notice notice--warn"><PSA.Icon name="warning" size="sm" />
          <div>Μια εικόνα από άλλο ιστότοπο δεν εμφανίζεται στο άρθρο και θα αφαιρεθεί με την επόμενη αλλαγή. Ανεβάστε την στις Εικόνες.</div></div>,
      );
    }
    problems.forEach((p, i) => notices.push(
      <div key={`problem-${i}`} className="notice notice--danger" role="alert"><PSA.Icon name="warning" size="sm" />
        <div>{p.code === "nesting"
          ? `Τα έντονα και τα πλάγια μέσα στην ίδια λέξη δεν αποθηκεύονται σωστά: «${p.text}…». Κρατήστε μόνο το ένα από τα δύο.`
          : `Αυτό το κομμάτι δεν αποθηκεύεται ακριβώς όπως φαίνεται: «${p.text}…». Αλλάξτε λίγο τη μορφοποίησή του.`}</div>
        <button type="button" className="btn btn--sm" onClick={() => { const e = editorRef.current; if (e) e.selectBlock(p.index); }}>Εμφάνιση</button>
      </div>,
    ));

    return (
      <>
        <div ref={rtRef} className="compose rt">
          <div ref={toolsRef} className="compose__tools rt-tools" role="toolbar" aria-label="Μορφοποίηση" aria-controls={id} onKeyDown={onToolsKeyDown}>
            <Tool icon="paragraph" label="Κείμενο" pressed={t.block === "p"} disabled={off || !focusedOnce} onClick={() => editorRef.current?.commands.setBlock("p")} />
            <Tool icon="h2" label="Επικεφαλίδα" pressed={t.block === "h2"} disabled={off || !focusedOnce || !t.can.heading} onClick={() => editorRef.current?.commands.setBlock("h2")} />
            <Tool icon="h3" label="Υποεπικεφαλίδα" pressed={t.block === "h3"} disabled={off || !focusedOnce || !t.can.heading} onClick={() => editorRef.current?.commands.setBlock("h3")} />
            <span className="sep" aria-hidden="true" />
            <Tool icon="bold" label="Έντονα (Ctrl/⌘ B)" pressed={!!t.bold} disabled={off} onClick={cmd("bold")} />
            <Tool icon="italic" label="Πλάγια (Ctrl/⌘ I)" pressed={!!t.italic} disabled={off} onClick={cmd("italic")} />
            <span className="sep" aria-hidden="true" />
            <Tool icon="link" label="Σύνδεσμος (Ctrl/⌘ K)" pressed={!!t.link} disabled={off} onClick={() => actions.current.openLink()} />
            <span className="sep" aria-hidden="true" />
            <Tool icon="ul" label="Λίστα με κουκκίδες" pressed={t.list === "bullet"} disabled={off} onClick={cmd("bulletList")} />
            <Tool icon="ol" label="Αριθμημένη λίστα" pressed={t.list === "ordered"} disabled={off} onClick={cmd("orderedList")} />
            <Tool icon="quote" label="Παράθεση" pressed={!!t.quote} disabled={off} onClick={cmd("quote")} />
            <span className="sep" aria-hidden="true" />
            <Tool icon="imagePlus" label="Εικόνα" disabled={off || !props.onPickImage} onClick={pickImage} />
            <Tool icon="divider" label="Διαχωριστική γραμμή" disabled={off} onClick={cmd("rule")} />
            <span className="spacer" />
            <Tool icon="undo" label="Αναίρεση (Ctrl/⌘ Z)" disabled={off || !t.can.undo} onClick={cmd("undo")} />
            <Tool icon="redo" label="Επανάληψη (Ctrl/⌘ Shift Z)" disabled={off || !t.can.redo} onClick={cmd("redo")} />
          </div>
          {!kit && <div className="rt-loading"><PSA.Spinner label="Φόρτωση επεξεργαστή…" /></div>}
          <div ref={mount} className="rt-mount" />
          {bubble}
          <div className="compose__foot">
            <span className="count">{PSA.plural(words, "λέξη", "λέξεις")} · ~{Math.max(1, Math.round(words / 200))} λεπτ. ανάγνωσης</span>
            <span className="rt-hint">Enter: νέα παράγραφος · Shift+Enter: αλλαγή γραμμής</span>
          </div>
        </div>
        {notices.length > 0 && <div className="rt-notices">{notices}</div>}
        {linkDlg && kit && (
          <LinkDialog kit={kit} init={linkDlg}
                      onClose={() => { setLinkDlg(null); refocusSoon(); }}
                      onApply={({ href, text }) => {
                        const e = editorRef.current;
                        if (!e || !e.commands.setLink({ href, text })) return false;
                        setLinkDlg(null);
                        refocusSoon();
                        return true;
                      }}
                      onRemove={() => { const e = editorRef.current; if (e) e.commands.removeLink(); setLinkDlg(null); refocusSoon(); }} />
        )}
        {imgDlg && kit && (
          <ImageDialog kit={kit} img={imgDlg}
                       onClose={() => { setImgDlg(null); refocusSoon(); }}
                       onSave={({ alt, caption }) => { const e = editorRef.current; if (e) e.commands.updateImageAt(imgDlg.pos, { alt, caption }); setImgDlg(null); refocusSoon(); }}
                       onRemove={() => { const e = editorRef.current; if (e) e.commands.removeImageAt(imgDlg.pos); setImgDlg(null); refocusSoon(); }}
                       onReplace={() => {
                         const pos = imgDlg.pos, pick = latest.current.onPickImage;
                         setImgDlg(null);
                         if (pick) setTimeout(() => pick((attrs) => { const e = editorRef.current; return e ? e.commands.replaceImageAt(pos, attrs) : false; }), 0);
                       }} />
        )}
      </>
    );
  };
})();
