// PHYSIO SAMOS — admin console: the post editor.
//
// One post, every language. The source language is written first; each other language is a tab
// holding its own translation, typed or machine-made, which goes public only once approved.
// Saving: a draft saves itself 1.5s after typing stops; a published or scheduled post never does —
// its changes are the live page, so they wait for "Ενημέρωση" and are kept on this device meanwhile.
(() => {
  const { useState, useEffect, useRef, useMemo, useCallback } = React;
  const PSA = window.PSA;
  const { Icon, api, toast, confirm, errorText, Field, StateBadge, fmtDate, fmtRelative, LANGS, LANG_NAME } = PSA;

  const SOURCE_KEYS = ["title", "excerpt", "body", "slug", "lang", "writerId", "coverMediaId", "coverAlt", "seoTitle", "seoDescription"];
  const TR_KEYS = ["title", "excerpt", "body", "coverAlt", "seoTitle", "seoDescription"];
  const EMPTY_TR = { title: "", excerpt: "", body: "", coverAlt: "", seoTitle: "", seoDescription: "" };
  const norm = (k, v) => ((k === "coverMediaId" || k === "writerId") ? (v ?? null) : (v ?? ""));
  const pick = (o, keys) => Object.fromEntries(keys.map((k) => [k, norm(k, o && o[k])]));
  const differs = (a, b, keys) => keys.some((k) => norm(k, a[k]) !== norm(k, b[k]));
  const FIELD_NAME = { title: "Τίτλος", excerpt: "Περίληψη", body: "Κείμενο", slug: "Διεύθυνση", coverAlt: "Περιγραφή εξωφύλλου", seoTitle: "Τίτλος κοινοποίησης", seoDescription: "Περιγραφή κοινοποίησης" };
  // A refused save names the field when the server says which one (e.g. a title over 200 characters).
  const saveErrorText = (err) => (err && err.code === "too_long" && err.data && FIELD_NAME[err.data.field]
    ? `Το πεδίο «${FIELD_NAME[err.data.field]}» ξεπερνά τους ${err.data.max} χαρακτήρες και δεν αποθηκεύτηκε.`
    : errorText(err));
  const smallCover = (url) => url && url.replace(/_(\d+)x(\d+)\./, (m, w, h) => (+w > 800 ? `_800x${Math.round((h * 800) / w)}.` : m));

  // Markdown is storage only. The writing surface always shows formatted content.
  const Compose = (props) => <>
    <PSA.RichText {...props} />
  </>;

  const Panel = ({ icon, title, open = true, children }) => (
    <details className="panel" open={open}>
      <summary><Icon name={icon} size="sm" />{title}<Icon name="chevronDown" size="sm" className="chev" /></summary>
      <div className="panel__body">{children}</div>
    </details>
  );

  const REVISION_KIND = { save: "Αποθήκευση", autosave: "Αυτόματη αποθήκευση", publish: "Δημοσίευση", unpublish: "Απόσυρση", restore: "Επαναφορά από τον κάδο" };
  const HistoryPanel = ({ postId, version, onRestore }) => {
    const [open, setOpen] = useState(false);
    const [viewing, setViewing] = useState(null);
    const list = PSA.useAsync(() => (open ? api(`/api/posts/${postId}/revisions`) : null), [open, version]);
    const show = async (r) => {
      try { setViewing((await api(`/api/revisions/${r.id}`)).revision); }
      catch (err) { toast(errorText(err), { kind: "error" }); }
    };
    const text = PSA.text();
    return (
      <details className="panel" onToggle={(e) => setOpen(e.currentTarget.open)}>
        <summary><Icon name="history" size="sm" />Ιστορικό εκδόσεων<Icon name="chevronDown" size="sm" className="chev" /></summary>
        <div className="panel__body">
          {list.loading && !list.data && <PSA.Spinner />}
          {list.data && list.data.revisions.length === 0 && <p className="hint">Κάθε αποθήκευση και δημοσίευση κρατά μια έκδοση εδώ.</p>}
          {list.data && list.data.revisions.length > 0 && (
            <ul className="revisions">
              {list.data.revisions.map((r) => (
                <li key={r.id}>
                  <span className="grow">
                    <strong>{REVISION_KIND[r.kind] || r.kind}</strong>
                    <span className="muted" style={{ display: "block" }} title={fmtDate(r.createdAt)}>{fmtRelative(r.createdAt)}</span>
                  </span>
                  <button type="button" className="btn btn--sm" onClick={() => show(r)}>Προβολή</button>
                </li>
              ))}
            </ul>
          )}
        </div>
        {viewing && (
          <PSA.Dialog open wide title={`${REVISION_KIND[viewing.kind] || viewing.kind} · ${fmtDate(viewing.createdAt)}`} onClose={() => setViewing(null)} footer={<>
            <span className="hint" style={{ marginRight: "auto" }}>Επαναφέρεται το κείμενο στο πρωτότυπο. Μετά αποθηκεύστε για να κρατηθεί.</span>
            <button type="button" className="btn" onClick={() => setViewing(null)}>Κλείσιμο</button>
            <button type="button" className="btn btn--primary" onClick={() => { onRestore(viewing.snapshot); setViewing(null); }}>Επαναφορά αυτής της έκδοσης</button>
          </>}>
            <div className="preview" style={{ padding: 0 }}>
              <h2 className="preview__title">{viewing.snapshot.title || "Χωρίς τίτλο"}</h2>
              {viewing.snapshot.excerpt && <p className="preview__lead">{viewing.snapshot.excerpt}</p>}
              <div className="prose" dangerouslySetInnerHTML={{ __html: text ? text.renderMarkdown(viewing.snapshot.body).html : "" }} />
            </div>
          </PSA.Dialog>
        )}
      </details>
    );
  };

  const Snippet = ({ title, slug, desc, image, live }) => (
    <div className="snippet" aria-label="Έτσι εμφανίζεται ο σύνδεσμος">
      <img src={image} alt="" style={{ width: "100%", maxHeight: 160, objectFit: "cover" }} />
      <div className="snippet__url">{live ? `physiosamos.gr › blog › ${slug}` : "Ιδιωτική προεπισκόπηση · χωρίς δημόσιο σύνδεσμο"}</div>
      <div className="snippet__title">{(title || "Χωρίς τίτλο")} | Physio Samos</div>
      <div className="snippet__desc">{desc || "Χωρίς περιγραφή."}</div>
    </div>
  );

  // ---------- the editor ----------
  PSA.Editor = ({ id }) => {
    const session = PSA.useSession();
    const text = PSA.text();
    const [status, setStatus] = useState({ loading: true, error: null });
    const [server, setServer] = useState(null);
    const [draft, setDraft] = useState(null);
    const [sourceHash, setSourceHash] = useState("");
    const [trs, setTrs] = useState({});
    const [tab, setTab] = useState(null);
    const [saving, setSaving] = useState(false);
    const [busy, setBusy] = useState(null);
    const [saveError, setSaveError] = useState(null);
    const [savedAt, setSavedAt] = useState(null);
    const [problems, setProblems] = useState([]);
    const [conflict, setConflict] = useState(null);
    const [picker, setPicker] = useState(null);
    const writerList = PSA.useWriters();
    const writers = writerList.data ? writerList.data.writers : [];
    const [addingWriter, setAddingWriter] = useState(false);
    const rich = useRef(null);
    const [slugTouched, setSlugTouched] = useState(false);
    const [slugState, setSlugState] = useState(null);
    const [translating, setTranslating] = useState(null);
    const [trWarnings, setTrWarnings] = useState({});
    const [backup, setBackup] = useState(null);
    const [when, setWhen] = useState("now");
    const [scheduleAt, setScheduleAt] = useState(() => PSA.athens.defaultScheduleMs());
    const [tick, setTick] = useState(0);
    const [scheduleError, setScheduleError] = useState(null);
    const serial = useRef(Promise.resolve());
    const pendingExtra = useRef({});
    const slugBlocked = useRef(null);
    const lastAutoError = useRef(null);
    const live = useRef({});
    const BACKUP_KEY = `ps_admin_backup_${id}`;


    const fetchPost = useCallback(async () => {
      const d = await api(`/api/posts/${id}`);
      setServer(d.post);
      if (d.post.state === "scheduled") setScheduleAt(d.post.publishedAt);
      setDraft({ ...pick(d.post, SOURCE_KEYS), coverUrl: d.post.coverUrl });
      setSourceHash(d.sourceHash);
      setTrs(Object.fromEntries(d.post.translations.map((t) => [t.lang, { saved: t, draft: pick(t, TR_KEYS) }])));
      setTab((t) => t || d.post.lang);
      const slugify = PSA.text() ? PSA.text().slugify : () => "";
      setSlugTouched(!d.post.slug.startsWith("draft-") && d.post.slug !== slugify(d.post.title));
      setStatus({ loading: false, error: null });
      try {
        const b = JSON.parse(localStorage.getItem(`ps_admin_backup_${id}`) || "null");
        if (b && b.draft && (differs(b.draft, d.post, SOURCE_KEYS) ||
            Object.entries(b.trs || {}).some(([l, f]) => { const t = d.post.translations.find((x) => x.lang === l); return differs(f, t || EMPTY_TR, TR_KEYS); }))) setBackup(b);
        else localStorage.removeItem(`ps_admin_backup_${id}`);
      } catch (e) {}
    }, [id]);

    useEffect(() => { fetchPost().catch((error) => setStatus({ loading: false, error })); }, [fetchPost]);

    const ready = !!(server && draft);
    const sourceDirty = ready && differs(draft, server, SOURCE_KEYS);
    const trDirtyOf = (map, lang) => { const t = map[lang]; return !!t && differs(t.draft, t.saved || EMPTY_TR, TR_KEYS); };
    const dirtyLangs = Object.keys(trs).filter((l) => trDirtyOf(trs, l));
    const anyDirty = sourceDirty || dirtyLangs.length > 0;
    const state = server ? server.state : "draft";
    const readOnly = state === "trash";
    live.current = { draft, server, trs, sourceHash, dirty: anyDirty, tab };

    // ---- saving ----
    const isLive = () => { const s = live.current.server; return !!s && (s.state === "published" || s.state === "scheduled"); };
    const showScheduleError = (message) => {
      setScheduleError(message);
      setTimeout(() => {
        const el = document.getElementById(live.current.server && live.current.server.state === "scheduled" ? "ed-resched" : "ed-schedule");
        if (el) { el.focus(); el.scrollIntoView({ block: "center" }); }
      }, 30);
    };
    const handleError = (err, { quiet = false } = {}) => {
      if (err.code === "version_conflict") { setConflict(err.data); return; }
      if (err.code === "not_ready") { setProblems(err.data.problems || []); if (!quiet) toast("Υπάρχουν εκκρεμότητες: δείτε τη λίστα στη Δημοσίευση.", { kind: "error" }); return; }
      if (err.code === "slug_taken" || err.code === "bad_slug") { setSlugState({ available: false, reason: err.code }); if (!quiet) toast(errorText(err), { kind: "error" }); return; }
      // The text was saved; only the chosen time was refused, so the message belongs on the date field.
      if (err.code === "date_past") { showScheduleError(errorText(err)); return; }
      if (err.formatting) { toast(err.message, { kind: "error" }); return; }
      setSaveError(err);
      if (!quiet) toast(saveErrorText(err), { kind: "error" });
    };

    // One request at a time. Autosave, manual saves and state changes queue behind each other, so two
    // requests never carry the same version (two at once produced a false "changed meanwhile" dialog).
    const exclusive = (fn) => { const run = serial.current.then(() => fn()); serial.current = run.catch(() => {}); return run; };

    // Formatting that cannot be stored exactly as shown (bold and italic on neighbouring letters of one
    // word). A draft still saves, so no typing is lost; nothing reaches the site until it is fixed, in
    // every language that is public or about to be.
    const kitProblems = (md) => {
      const K = window.PSEditorKit;
      if (!K || !String(md || "").trim()) return [];
      try { return K.toMarkdown(K.parseMarkdown(md).doc).problems || []; } catch (e) { return []; }
    };
    const assertFormatting = (extraLangs = []) => {
      const { draft: d, trs: all, tab: open } = live.current;
      const openProblems = rich.current ? rich.current.getProblems() : [];
      const issues = [];
      const check = (lang, md) => { const p = lang === open ? openProblems : kitProblems(md); if (p.length) issues.push({ lang, problem: p[0] }); };
      check(d.lang, d.body);
      for (const [lang, t] of Object.entries(all)) {
        if (lang !== d.lang && ((t.saved && t.saved.status === "ready") || extraLangs.includes(lang))) check(lang, t.draft.body);
      }
      if (!issues.length) return;
      const first = issues[0];
      if (first.lang !== open) { setTab(first.lang); setTimeout(() => rich.current && rich.current.focusProblem(first.problem), 150); }
      else if (rich.current) rich.current.focusProblem(first.problem);
      const err = new Error(`Διορθώστε πρώτα τη μορφοποίηση που επισημαίνεται κάτω από το κείμενο (${[...new Set(issues.map((i) => LANG_NAME[i.lang]))].join(", ")}).`);
      err.formatting = true;
      throw err;
    };

    const saveSource = async ({ autosave = false } = {}) => {
      const { draft: d, server: s } = live.current;
      const fields = {};
      for (const k of SOURCE_KEYS) if (norm(k, d[k]) !== norm(k, s[k])) fields[k] = d[k];
      // A taken or invalid address must not hold the text back: it is left out until it changes.
      if ("slug" in fields && slugBlocked.current === d.slug) delete fields.slug;
      if (!Object.keys(fields).length) return s;
      const autoSlug = !slugTouched && !s.firstPublishedAt;
      const put = (f) => api(`/api/posts/${id}`, { method: "PUT", body: { version: s.version, autosave, autoSlug, ...f } });
      let sent = fields, res;
      try {
        res = await put(sent);
      } catch (err) {
        if (!(err.code === "slug_taken" || err.code === "bad_slug") || !("slug" in fields)) throw err;
        slugBlocked.current = d.slug;
        setSlugState({ available: false, reason: err.code });
        const { slug, ...rest } = fields;
        if (!Object.keys(rest).length) throw err;
        sent = rest;
        res = await put(sent);
        if (!autosave) toast(errorText(err), { kind: "error" });
      }
      // The server may give an automatic address a suffix (-2); a typed address the server did not
      // receive stays as typed.
      if (res.post.slug !== d.slug && ("slug" in sent || autoSlug)) {
        setDraft((current) => current.slug === d.slug ? { ...current, slug: res.post.slug } : current);
        if (live.current.draft.slug === d.slug) live.current.draft = { ...live.current.draft, slug: res.post.slug };
      }
      live.current.server = res.post;
      live.current.sourceHash = res.sourceHash;
      setServer(res.post);
      setSourceHash(res.sourceHash);
      if ("slug" in sent) { setSlugState(null); slugBlocked.current = null; }
      return res.post;
    };

    const saveTr = async (lang, extra = {}) => {
      const t = live.current.trs[lang];
      if (!t) return null;
      // A machine translation keeps its draft/machine marking through any retry (e.g. "Κράτηση των
      // δικών μου" after a conflict), so unreviewed text can never be saved as approved.
      if (pendingExtra.current[lang]) extra = { ...pendingExtra.current[lang], ...extra };
      const base = t.saved ? pick(t.saved, TR_KEYS) : EMPTY_TR;
      const fields = {};
      for (const k of TR_KEYS) if (t.draft[k] !== base[k]) fields[k] = t.draft[k];
      const hasContent = TR_KEYS.some((k) => String(t.draft[k] || "").trim());
      if (!Object.keys(fields).length && !extra.status && !extra.origin) return t.saved;
      if (!t.saved && !hasContent) return null;
      const body = { version: t.saved ? t.saved.version : 0, ...fields, ...extra };
      if (t.saved && t.saved.origin === "machine" && Object.keys(fields).length && !extra.origin) body.origin = "edited";
      const res = await api(`/api/posts/${id}/translations/${lang}`, { method: "PUT", body });
      live.current.trs = { ...live.current.trs, [lang]: { saved: res.translation, draft: t.draft } };
      setTrs((x) => ({ ...x, [lang]: { saved: res.translation, draft: x[lang] ? x[lang].draft : t.draft } }));
      delete pendingExtra.current[lang];
      return res.translation;
    };

    // `quiet` only drops the "Αποθηκεύτηκε" toast; a failure is always explained.
    const saveAll = ({ quiet = false } = {}) => exclusive(async () => {
      if (readOnly) return true;
      setSaving(true);
      setSaveError(null);
      try {
        // A published or scheduled post saves straight to the site ("Ενημέρωση"), so it gets the same
        // formatting check as publishing.
        if (isLive()) assertFormatting();
        await saveSource();
        for (const lang of Object.keys(live.current.trs)) if (trDirtyOf(live.current.trs, lang) || pendingExtra.current[lang]) await saveTr(lang);
        setSavedAt(Date.now());
        lastAutoError.current = null;
        if (!quiet) toast("Αποθηκεύτηκε.");
        return true;
      } catch (err) {
        handleError(err);
        return false;
      } finally {
        setSaving(false);
      }
    });

    // Autosave: drafts and draft translations only, 1.5s after the last change. It never checks the
    // formatting and never moves the caret. A refusal (4xx) is said once and waits for the next edit;
    // only network and server errors retry on their own.
    useEffect(() => {
      if (!ready || readOnly) return;
      const autoSource = sourceDirty && state === "draft";
      const autoLangs = dirtyLangs.filter((l) => !trs[l].saved || trs[l].saved.status === "draft");
      if (!autoSource && !autoLangs.length) return;
      const timer = setTimeout(() => exclusive(async () => {
        setSaving(true);
        let retry = false;
        try {
          const { draft: d, server: s } = live.current;
          if (s.state === "draft" && differs(d, s, SOURCE_KEYS)) await saveSource({ autosave: true });
          for (const lang of autoLangs) {
            const t = live.current.trs[lang];
            if (t && (!t.saved || t.saved.status === "draft") && trDirtyOf(live.current.trs, lang)) await saveTr(lang, { autosave: true });
          }
          setSaveError(null);
          lastAutoError.current = null;
          setSavedAt(Date.now());
        } catch (err) {
          retry = err.code === "network" || err.status >= 500;
          if (err.code === "version_conflict" || err.code === "not_ready") { handleError(err, { quiet: true }); return; }
          setSaveError(err);
          const key = `${err.code}:${(err.data && err.data.field) || ""}`;
          if (!retry && lastAutoError.current !== key) { lastAutoError.current = key; toast(saveErrorText(err), { kind: "error" }); }
        } finally {
          setSaving(false);
          if (retry) setTimeout(() => setTick((n) => n + 1), 5000);
        }
      }), 1500);
      return () => clearTimeout(timer);
    }, [draft, trs, server && server.version, tick]);

    // Anything unsaved is also kept on this device, so a closed tab or an expired session loses nothing.
    useEffect(() => {
      if (!ready) return;
      if (!anyDirty) { try { localStorage.removeItem(BACKUP_KEY); } catch (e) {} return; }
      const timer = setTimeout(() => {
        try { localStorage.setItem(BACKUP_KEY, JSON.stringify({ at: Date.now(), draft, trs: Object.fromEntries(Object.entries(trs).map(([l, t]) => [l, t.draft])) })); } catch (e) {}
      }, 500);
      return () => clearTimeout(timer);
    }, [draft, trs, anyDirty]);

    useEffect(() => {
      PSA.leaveGuard = () => !live.current.dirty || window.confirm("Υπάρχουν αλλαγές που δεν έχουν αποθηκευτεί. Να φύγετε από τη σελίδα;");
      const onBeforeUnload = (e) => { if (live.current.dirty) { e.preventDefault(); e.returnValue = ""; } };
      const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && (e.key === "s" || e.key === "σ")) { e.preventDefault(); saveAllRef.current(); } };
      window.addEventListener("beforeunload", onBeforeUnload);
      window.addEventListener("keydown", onKey);
      return () => { PSA.leaveGuard = null; window.removeEventListener("beforeunload", onBeforeUnload); window.removeEventListener("keydown", onKey); };
    }, []);
    const saveAllRef = useRef(saveAll);
    saveAllRef.current = saveAll;

    // Address availability, checked while it is being typed.
    useEffect(() => {
      if (!ready || draft.slug === server.slug) { setSlugState(null); return; }
      const timer = setTimeout(async () => {
        try { setSlugState(await api(`/api/posts/slug?slug=${encodeURIComponent(draft.slug)}&id=${id}`)); } catch (e) {}
      }, 400);
      return () => clearTimeout(timer);
    }, [draft && draft.slug, server && server.slug]);

    // ---- field setters ----
    const setField = (k, v) => setDraft((d) => {
      const next = { ...d, [k]: v };
      if (k === "title" && !slugTouched && !server.firstPublishedAt && text) {
        const s = text.slugify(v);
        if (s) next.slug = s;
      }
      return next;
    });
    const setTrField = (lang, k, v) => setTrs((x) => {
      const cur = x[lang] || { saved: null, draft: { ...EMPTY_TR } };
      return { ...x, [lang]: { ...cur, draft: { ...cur.draft, [k]: v } } };
    });

    // ---- actions ----
    // `intent`: "schedule" (a future time from the picker) or "now" (the server's clock, not this device's).
    const publish = async (at, intent) => {
      setScheduleError(null);
      if (intent === "schedule" && PSA.athens.isPast(at)) { showScheduleError("Διαλέξτε ημερομηνία και ώρα στο μέλλον."); return; }
      try { assertFormatting(); } catch (err) { handleError(err); return; }
      if (!(await saveAll({ quiet: true }))) return;
      setBusy("publish");
      try {
        const res = await exclusive(() => api(`/api/posts/${id}/publish`, { method: "POST", body: { version: live.current.server.version, publishedAt: intent === "now" ? null : at ?? null, intent } }));
        live.current.server = res.post;
        setServer(res.post);
        setProblems([]);
        if (res.post.state === "scheduled") toast(`Προγραμματίστηκε για ${fmtDate(res.post.publishedAt)}.`);
        else if (session.settings.blogEnabled) toast("Το άρθρο δημοσιεύτηκε.", { action: { label: "Προβολή", onClick: () => window.open(res.liveUrl, "_blank", "noopener") } });
        else toast("Δημοσιεύτηκε. Το blog είναι ακόμη κρυφό, οπότε δεν φαίνεται στον ιστότοπο.");
      } catch (err) {
        handleError(err);
      } finally {
        setBusy(null);
      }
    };
    const transition = async (path, okText, confirmOpts, after) => {
      if (confirmOpts && !(await confirm(confirmOpts))) return;
      setBusy(path);
      try {
        const res = await exclusive(() => api(`/api/posts/${id}/${path}`, { method: "POST", body: { version: live.current.server.version } }));
        live.current.server = res.post;
        setServer(res.post);
        toast(okText);
        if (after) after(res);
      } catch (err) {
        handleError(err);
      } finally {
        setBusy(null);
      }
    };
    const unpublish = () => transition("unpublish", "Το άρθρο αποσύρθηκε και είναι ξανά πρόχειρο.", {
      title: state === "scheduled" ? "Ακύρωση προγραμματισμού;" : "Απόσυρση από τον ιστότοπο;",
      body: state === "scheduled" ? "Το άρθρο γίνεται πρόχειρο και δεν θα δημοσιευτεί." : "Το άρθρο φεύγει αμέσως από τον ιστότοπο και γίνεται πρόχειρο. Οι μεταφράσεις του μένουν.",
      confirmLabel: state === "scheduled" ? "Ακύρωση προγραμματισμού" : "Απόσυρση", danger: true,
    });
    const trashPost = () => transition("trash", "Το άρθρο μεταφέρθηκε στον κάδο.", {
      title: "Μεταφορά στον κάδο;", danger: true, confirmLabel: "Στον κάδο",
      body: state === "published" ? "Το άρθρο φεύγει αμέσως από τον ιστότοπο. Αν το επαναφέρετε, γυρίζει ως πρόχειρο." : "Μπορείτε να το επαναφέρετε από τον κάδο.",
    }, () => { live.current.dirty = false; PSA.navigate("/posts?state=trash", { force: true }); });
    const restorePost = () => transition("restore", "Το άρθρο επανήλθε ως πρόχειρο.");
    // Unsaved edits of a published or scheduled post are not the site's yet: actions other than
    // "Ενημέρωση" leave them unsaved instead of publishing them on the side.
    const duplicate = async () => {
      if (isLive() && anyDirty) {
        const ok = await confirm({ title: "Αντίγραφο της αποθηκευμένης έκδοσης;", confirmLabel: "Αντίγραφο",
          body: "Το αντίγραφο φτιάχνεται από την αποθηκευμένη έκδοση. Οι αλλαγές που δεν έχετε αποθηκεύσει με «Ενημέρωση» δεν θα μπουν σε αυτό." });
        if (!ok) return;
      } else if (!(await saveAll({ quiet: true }))) return;
      try {
        const { post } = await api(`/api/posts/${id}/duplicate`, { method: "POST", body: { titleSuffix: " (αντίγραφο)" } });
        toast("Δημιουργήθηκε αντίγραφο ως πρόχειρο.");
        PSA.navigate(`/posts/${post.id}`, { force: true });
      } catch (err) { toast(errorText(err), { kind: "error" }); }
    };
    const openPreview = async (lang) => {
      const w = window.open("about:blank", "_blank");
      if (anyDirty && isLive()) toast("Η προεπισκόπηση δείχνει την αποθηκευμένη έκδοση. Οι αλλαγές σας εμφανίζονται μετά την «Ενημέρωση».");
      else if (anyDirty && !(await saveAll({ quiet: true }))) { if (w) w.close(); return; }
      const url = `/preview/${id}${lang && lang !== draft.lang ? `?lang=${lang}` : ""}`;
      if (w) w.location.href = url; else window.location.href = url;
    };

    const machine = async (lang, { quiet = false } = {}) => {
      const t = live.current.trs[lang];
      if (isLive() && differs(live.current.draft, live.current.server, SOURCE_KEYS)) {
        toast("Αποθηκεύστε πρώτα τις αλλαγές του πρωτοτύπου με «Ενημέρωση» και μετά ζητήστε μετάφραση.", { kind: "error" });
        return false;
      }
      if (!quiet && t && TR_KEYS.some((k) => String(t.draft[k] || "").trim())) {
        const ok = await confirm({ title: "Αντικατάσταση της μετάφρασης;", danger: true, confirmLabel: "Αντικατάσταση",
          body: `Η αυτόματη μετάφραση θα αντικαταστήσει ό,τι έχει γραφτεί στα ${LANG_NAME[lang]}. Αν η μετάφραση ήταν εγκεκριμένη, γίνεται ξανά πρόχειρη.` });
        if (!ok) return false;
      }
      if (!isLive() && !(await saveAll({ quiet: true }))) return false;
      setTranslating(lang);
      try {
        const d = live.current.draft;
        const res = await api(`/api/posts/${id}/translate`, { method: "POST", body: { lang, source: {
          lang: d.lang, title: d.title, excerpt: d.excerpt, body: d.body, coverAlt: d.coverAlt, seoTitle: d.seoTitle, seoDescription: d.seoDescription,
        } } });
        const fresh = pick({ ...EMPTY_TR, ...res.fields }, TR_KEYS);
        // Read the translation again: the save above may have given it a new version.
        const cur = live.current.trs[lang];
        live.current.trs = { ...live.current.trs, [lang]: { saved: cur ? cur.saved : null, draft: fresh } };
        setTrs((x) => ({ ...x, [lang]: { saved: x[lang] ? x[lang].saved : null, draft: fresh } }));
        pendingExtra.current[lang] = { status: "draft", origin: "machine", sourceHash: live.current.sourceHash };
        await exclusive(() => saveTr(lang));
        setTrWarnings((w) => ({ ...w, [lang]: res.warnings || [] }));
        if (!quiet) toast(`Η μετάφραση στα ${LANG_NAME[lang]} είναι έτοιμη για έλεγχο.`);
        return true;
      } catch (err) {
        handleError(err);
        return false;
      } finally {
        setTranslating(null);
      }
    };
    const translateMissing = async () => {
      const missing = LANGS.filter((l) => l !== draft.lang && !(trs[l] && trs[l].saved));
      let done = 0;
      for (const lang of missing) { setTab(lang); if (await machine(lang, { quiet: true })) done++; else break; }
      if (done) toast(`${PSA.plural(done, "μετάφραση", "μεταφράσεις")} έτοιμες για έλεγχο.`);
    };
    const approve = async (lang) => {
      try { assertFormatting([lang]); } catch (err) { handleError(err); return; }
      if (!isLive() && !(await saveAll({ quiet: true }))) return;
      if (state === "published") {
        const ok = await confirm({ title: `Έγκριση μετάφρασης (${LANG_NAME[lang]});`, confirmLabel: "Έγκριση",
          body: "Το άρθρο είναι δημοσιευμένο: η μετάφραση θα εμφανίζεται αμέσως σε όσους έχουν διαλέξει αυτή τη γλώσσα στον ιστότοπο." });
        if (!ok) return;
      }
      try {
        await exclusive(() => saveTr(lang, { status: "ready", sourceHash: live.current.sourceHash }));
        setProblems((p) => p.filter((x) => x.lang !== lang));
        toast("Η μετάφραση εγκρίθηκε.");
      } catch (err) { handleError(err); }
    };
    const unapprove = async (lang) => {
      try { await exclusive(() => saveTr(lang, { status: "draft" })); toast("Η μετάφραση έγινε ξανά πρόχειρη και δεν εμφανίζεται."); }
      catch (err) { handleError(err); }
    };
    const removeTr = async (lang) => {
      const ok = await confirm({ title: `Διαγραφή μετάφρασης (${LANG_NAME[lang]});`, danger: true, confirmLabel: "Διαγραφή", body: "Η μετάφραση σβήνεται οριστικά. Το πρωτότυπο δεν αλλάζει." });
      if (!ok) return;
      try {
        if (trs[lang] && trs[lang].saved) await api(`/api/posts/${id}/translations/${lang}`, { method: "DELETE" });
        setTrs((x) => { const n = { ...x }; delete n[lang]; return n; });
        toast("Η μετάφραση διαγράφηκε.");
      } catch (err) { toast(errorText(err), { kind: "error" }); }
    };

    const onFiles = async (files, insert) => {
      try {
        toast(files.length > 1 ? `Ανεβαίνουν ${files.length} εικόνες…` : "Η εικόνα ανεβαίνει…");
        const uploaded = [];
        for (const f of files) uploaded.push(await PSA.uploadImage(f));
        if (uploaded.length === 1) setPicker({ mode: "insert", initialMedia: uploaded[0], onPick: ({ media, alt, caption }) => insert({ src: media.url, alt, caption: caption || "" }) });
        else { uploaded.forEach((m) => insert({ src: m.url, alt: m.alt || "", caption: "" })); toast("Προσθέστε περιγραφή σε κάθε εικόνα από τις ειδοποιήσεις κάτω από το κείμενο."); }
      } catch (err) { toast(errorText(err), { kind: "error" }); }
    };

    // ---- render ----
    if (status.loading) return <div className="page"><PSA.Spinner label="Άνοιγμα άρθρου…" /></div>;
    if (status.error) {
      return (
        <div className="page">
          <PSA.ErrorNotice error={status.error} onRetry={() => { setStatus({ loading: true, error: null }); fetchPost().catch((error) => setStatus({ loading: false, error })); }} />
          <p style={{ marginTop: 12 }}><PSA.Link to="/posts">Επιστροφή στα άρθρα</PSA.Link></p>
        </div>
      );
    }

    const isSource = tab === draft.lang;
    const tr = !isSource ? (trs[tab] || { saved: null, draft: { ...EMPTY_TR } }) : null;
    const stale = tr && tr.saved && tr.saved.sourceHash && tr.saved.sourceHash !== sourceHash;
    const langs = [draft.lang, ...LANGS.filter((l) => l !== draft.lang)];
    const problemField = { coverMediaId: "ed-cover", writerId: "ed-author", title: "ed-title", body: "ed-body", slug: "ed-slug", coverAlt: "ed-cover-alt" };
    const focusProblem = (p) => {
      if (p.lang) setTab(p.lang); else setTab(draft.lang);
      if (p.field === "body") setTimeout(() => rich.current && rich.current.focusProblem(p), 80);
      setTimeout(() => { const el = document.getElementById(problemField[p.field]); if (el) { el.focus(); el.scrollIntoView({ block: "center" }); } }, 60);
    };

    const label = (icon, text, spin) => <>{icon && <Icon name={icon} size="sm" className={spin ? "spin" : ""} />}<span className="save-state__label">{text}</span></>;
    const saveLabel = saving ? label("loader", "Αποθήκευση…", true)
      : saveError ? label("warning", "Δεν αποθηκεύτηκε")
      : anyDirty ? (state === "draft" && !dirtyLangs.some((l) => trs[l].saved && trs[l].saved.status === "ready") ? label(null, "Αλλαγές…") : label("warning", "Μη αποθηκευμένες αλλαγές"))
      : label("check", savedAt ? `Αποθηκεύτηκε ${PSA.fmtClock(savedAt)}` : "Αποθηκευμένο");

    let primary;
    if (state === "trash") primary = <button type="button" className="btn btn--primary" onClick={restorePost} disabled={!!busy}><Icon name="restore" size="sm" />Επαναφορά</button>;
    // One primary action, in the bar, for every state. A published or scheduled post with nothing to
    // save has none at all — the save state beside it already says everything is saved.
    else if (state === "published") primary = anyDirty || saving ? <button type="button" className="btn btn--live" onClick={() => saveAll()} disabled={saving}>Ενημέρωση</button> : null;
    else if (state === "scheduled") primary = anyDirty || saving ? <button type="button" className="btn btn--primary" onClick={() => saveAll()} disabled={saving}>Αποθήκευση</button> : null;
    else if (when === "later") primary = <button type="button" className="btn btn--primary" disabled={!!busy} onClick={() => publish(scheduleAt, "schedule")}><Icon name="calendar" size="sm" />Προγραμματισμός</button>;
    else primary = <button type="button" className="btn btn--live" disabled={!!busy} onClick={() => publish(null)}><Icon name="send" size="sm" />Δημοσίευση</button>;

    const metaFor = (d) => text ? text.postMetadata(d) : { title: d.title, description: "", summary: "", image: d.coverUrl };
    const sharingDraft = isSource ? draft : tr.draft;
    const setSharingField = (key, value) => isSource ? setField(key, value) : setTrField(tab, key, value);
    const sharing = metaFor({ ...sharingDraft, coverUrl: draft.coverUrl });

    return (
      <div className="editor">
        <div className="editor__bar">
          <PSA.Link to="/posts" className="btn btn--quiet btn--icon" aria-label="Πίσω στα άρθρα" title="Πίσω στα άρθρα"><Icon name="back" /></PSA.Link>
          <StateBadge state={state} />
          <span className={`save-state${saveError ? " is-error" : anyDirty && state !== "draft" ? " is-dirty" : ""}`} role="status" aria-live="polite" title={saveError ? saveErrorText(saveError) : undefined}>{saveLabel}</span>
          <span className="spacer" />
          {state === "published"
            ? <button type="button" className="btn btn--quiet" onClick={() => openPreview(tab)} title="Η αποθηκευμένη σελίδα του άρθρου, σε νέα καρτέλα" aria-label="Η σελίδα του άρθρου σε νέα καρτέλα"><Icon name="external" size="sm" /><span className="btn__label">Σελίδα</span></button>
            : <button type="button" className="btn btn--quiet" onClick={() => openPreview(tab)} title="Ιδιωτική προεπισκόπηση, μόνο για εσάς, σε νέα καρτέλα" aria-label="Προεπισκόπηση σε νέα καρτέλα"><Icon name="eye" size="sm" /><span className="btn__label">Προεπισκόπηση</span></button>}
          {primary}
        </div>

        <div className="editor__main">
          {backup && (
            <div className="notice notice--warn" style={{ marginBottom: 18 }}>
              <Icon name="history" size="sm" />
              <div style={{ flex: 1 }}>Βρέθηκαν αλλαγές σε αυτή τη συσκευή που δεν είχαν αποθηκευτεί ({fmtRelative(backup.at)}).</div>
              <button type="button" className="btn btn--sm" onClick={() => {
                setDraft((d) => ({ ...d, ...pick(backup.draft, SOURCE_KEYS), coverUrl: backup.draft.coverUrl ?? d.coverUrl }));
                setTrs((x) => { const n = { ...x }; for (const [l, f] of Object.entries(backup.trs || {})) n[l] = { saved: x[l] ? x[l].saved : null, draft: pick(f, TR_KEYS) }; return n; });
                setBackup(null);
              }}>Επαναφορά</button>
              <button type="button" className="btn btn--quiet btn--sm" onClick={() => { try { localStorage.removeItem(BACKUP_KEY); } catch (e) {} setBackup(null); }}>Απόρριψη</button>
            </div>
          )}
          {readOnly && <div className="notice notice--danger" style={{ marginBottom: 18 }}><Icon name="trash" size="sm" /><div>Το άρθρο είναι στον κάδο. Επαναφέρετέ το για να το επεξεργαστείτε.</div></div>}

          <div className="lang-tabs" role="tablist" aria-label="Γλώσσα">
            {langs.map((l) => {
              const t = trs[l];
              const dot = l === draft.lang ? "is-source" : t && t.saved ? `is-${t.saved.status}` : "";
              return (
                <button key={l} type="button" role="tab" aria-selected={tab === l} onClick={() => setTab(l)}>
                  <span className={`dot ${dot}`} aria-hidden="true" />{LANG_NAME[l]}
                  {l === draft.lang && <span className="muted" style={{ fontWeight: 500 }}>πρωτότυπο</span>}
                  {translating === l && <Icon name="loader" size="sm" className="spin" />}
                </button>
              );
            })}
          </div>

          {!isSource && (
            <>
              <div className="translate-bar">
                <span className="grow">
                  {!tr.saved ? <>Δεν υπάρχει ακόμη μετάφραση στα <strong>{LANG_NAME[tab]}</strong>. Γράψτε τη ή ζητήστε αυτόματη.</>
                    : tr.saved.status === "ready" ? <><span className="state state--ready">Εγκεκριμένη</span> Εμφανίζεται {state === "published" ? "τώρα" : "μόλις δημοσιευτεί το άρθρο"} σε όσους διαλέγουν {LANG_NAME[tab]}.</>
                    : <><span className={`state ${tr.saved.origin === "machine" ? "state--machine" : "state--draft"}`}>{tr.saved.origin === "machine" ? "Αυτόματη, χωρίς έλεγχο" : "Πρόχειρη"}</span> Δεν εμφανίζεται μέχρι να εγκριθεί.</>}
                </span>
                <button type="button" className="btn btn--sm" onClick={() => machine(tab)} disabled={!!translating || readOnly || !draft.body.trim()}>
                  <Icon name={translating === tab ? "loader" : "translate"} size="sm" className={translating === tab ? "spin" : ""} />{translating === tab ? "Μετάφραση…" : "Αυτόματη μετάφραση"}
                </button>
                {tr.saved && tr.saved.status !== "ready" && <button type="button" className="btn btn--primary btn--sm" onClick={() => approve(tab)} disabled={readOnly}><Icon name="check" size="sm" />Έγκριση</button>}
                {tr.saved && tr.saved.status === "ready" && <button type="button" className="btn btn--sm" onClick={() => unapprove(tab)} disabled={readOnly}>Επιστροφή σε πρόχειρη</button>}
                {(tr.saved || TR_KEYS.some((k) => String(tr.draft[k] || "").trim())) && <button type="button" className="btn btn--quiet btn--sm btn--icon" onClick={() => removeTr(tab)} aria-label="Διαγραφή μετάφρασης" title="Διαγραφή μετάφρασης" disabled={readOnly}><Icon name="trash" size="sm" /></button>}
              </div>
              {stale && <div className="notice notice--warn" style={{ marginBottom: 14 }}><Icon name="warning" size="sm" /><div>Το πρωτότυπο άλλαξε μετά από αυτή τη μετάφραση. Ελέγξτε τη και εγκρίνετέ τη ξανά, ή ζητήστε νέα αυτόματη.</div></div>}
              {tr.saved && tr.saved.origin === "machine" && <div className="notice notice--info" style={{ marginBottom: 14 }}><Icon name="info" size="sm" /><div>Αυτόματη μετάφραση: διαβάστε την ολόκληρη και διορθώστε πριν την εγκρίνετε. Οι όροι θεραπειών και παθήσεων θέλουν ιδιαίτερη προσοχή.</div></div>}
              {(trWarnings[tab] || []).length > 0 && (
                <div className="notice notice--warn" style={{ marginBottom: 14 }}>
                  <Icon name="warning" size="sm" />
                  <div>Ελέγξτε ιδιαίτερα:
                    <ul>{trWarnings[tab].map((w, i) => <li key={i}>{
                      w.code === "field_failed" ? `το πεδίο «${{ title: "τίτλος", excerpt: "περίληψη", coverAlt: "περιγραφή εξωφύλλου", seoTitle: "τίτλος κοινοποίησης", seoDescription: "περιγραφή κοινοποίησης" }[w.field] || w.field}» δεν μεταφράστηκε αξιόπιστα και έμεινε κενό`
                      : w.code === "chunk_untranslated" ? `ένα κομμάτι του κειμένου έμεινε αμετάφραστο («${w.start}…»)`
                      : w.code === "structure_changed" ? "η μορφοποίηση διαφέρει από το πρωτότυπο (επικεφαλίδες, λίστες ή έντονα)"
                      : w.code === "link_changed" ? `ένας σύνδεσμος άλλαξε (${w.url})`
                      : w.code === "image_moved" ? "μια εικόνα μετακινήθηκε στο τέλος" : w.code
                    }</li>)}</ul>
                  </div>
                </div>
              )}
            </>
          )}

          {problems.length > 0 && (
            <div className="notice notice--danger" role="alert" style={{ marginBottom: 18 }}>
              <Icon name="warning" size="sm" />
              <div style={{ flex: 1 }}>
                <strong>Πριν από τη δημοσίευση:</strong>
                <ul>{problems.map((p, i) => <li key={i}><button type="button" className="btn btn--quiet btn--sm" style={{ height: "auto", padding: "2px 4px", color: "inherit", textDecoration: "underline" }} onClick={() => focusProblem(p)}>{PSA.problemText(p)}</button></li>)}</ul>
              </div>
              <button type="button" className="btn btn--quiet btn--icon btn--sm" aria-label="Κλείσιμο" onClick={() => setProblems([])}><Icon name="close" size="sm" /></button>
            </div>
          )}

          {isSource ? (
            <>
              <div className="head-fields">
                <TitleInput id="ed-title" value={draft.title} readOnly={readOnly} onChange={(v) => setField("title", v)} lang={draft.lang} />
                <ExcerptInput value={draft.excerpt} readOnly={readOnly} onChange={(v) => setField("excerpt", v)} lang={draft.lang} />
              </div>
              <Compose id="ed-body" key={draft.lang} lang={draft.lang} handleRef={rich} value={draft.body} readOnly={readOnly} onChange={(v) => setField("body", v)}
                       placeholder="Ξεκινήστε να γράφετε…"
                       onPickImage={(insert) => setPicker({ mode: "insert", onPick: ({ media, alt, caption }) => insert({ src: media.url, alt, caption: caption || "" }) })}
                       onFiles={onFiles} />
            </>
          ) : (
            <>
              <div className="head-fields">
                <TitleInput id="ed-title" value={tr.draft.title} readOnly={readOnly} onChange={(v) => setTrField(tab, "title", v)} lang={tab} placeholder={draft.title || "Τίτλος"} />
                <ExcerptInput value={tr.draft.excerpt} readOnly={readOnly} onChange={(v) => setTrField(tab, "excerpt", v)} lang={tab} />
              </div>
              <Compose id="ed-body" key={tab} lang={tab} handleRef={rich} value={tr.draft.body} readOnly={readOnly} onChange={(v) => setTrField(tab, "body", v)}
                       placeholder={`Η μετάφραση στα ${LANG_NAME[tab]}…`}
                       onPickImage={(insert) => setPicker({ mode: "insert", onPick: ({ media, alt, caption }) => insert({ src: media.url, alt, caption: caption || "" }) })}
                       onFiles={onFiles} />
              <div className="sheet sheet__pad" style={{ marginTop: 18, display: "grid", gap: 16 }}>
                <Field label="Περιγραφή εξωφύλλου" htmlFor="ed-cover-alt" count={tr.draft.coverAlt.length} max={250} hint={draft.coverAlt ? `Πρωτότυπο: ${draft.coverAlt}` : "Το άρθρο δεν έχει εξώφυλλο."}>
                  <input id="ed-cover-alt" className="input" lang={tab} value={tr.draft.coverAlt} readOnly={readOnly} onChange={(e) => setTrField(tab, "coverAlt", e.target.value)} />
                </Field>
              </div>
            </>
          )}
        </div>

        <aside className="editor__side" aria-label="Ρυθμίσεις άρθρου">
          <Panel icon="send" title="Δημοσίευση">
            <div className="publish-state">
              <dl>
                <dt>Κατάσταση</dt><dd><StateBadge state={state} /></dd>
                {state === "published" && <><dt>Από</dt><dd>{fmtDate(server.publishedAt)}</dd></>}
                {state === "scheduled" && <><dt>Στις</dt><dd>{fmtDate(server.publishedAt)}</dd></>}
                <dt>Αλλαγή</dt><dd>{fmtRelative(server.updatedAt)}</dd>
              </dl>
            </div>
            {state === "published" && !session.settings.blogEnabled && <div className="notice notice--info"><Icon name="lock" size="sm" /><div>Το blog είναι κρυφό: το άρθρο δεν φαίνεται στον ιστότοπο μέχρι να γίνει δημόσιο από την Αρχική.</div></div>}
            {state === "draft" && (
              <div className="field">
                <span className="label">Πότε δημοσιεύεται</span>
                <PSA.Segmented label="Πότε" value={when} onChange={(v) => { setWhen(v); setScheduleError(null); }} options={[{ value: "now", label: "Τώρα" }, { value: "later", label: "Αργότερα" }]} />
                {when === "later" && <PSA.DateTimePicker id="ed-schedule" ariaLabel="Ημερομηνία και ώρα δημοσίευσης" value={scheduleAt} min="now"
                  invalid={!!scheduleError} describedBy={scheduleError ? "ed-when-error" : undefined}
                  onChange={(v) => { setScheduleAt(v); setScheduleError(null); }} />}
                {when === "later" && scheduleError && <p id="ed-when-error" className="field__error" role="alert">{scheduleError}</p>}
              </div>
            )}
            {state === "scheduled" && (
              <div className="field">
                <label className="label" htmlFor="ed-resched">Νέα ημερομηνία</label>
                <PSA.DateTimePicker id="ed-resched" ariaLabel="Νέα ημερομηνία δημοσίευσης" value={scheduleAt} min="now"
                  invalid={!!scheduleError} describedBy={scheduleError ? "ed-when-error" : undefined}
                  onChange={(v) => { setScheduleAt(v); setScheduleError(null); }} />
                {scheduleError && <p id="ed-when-error" className="field__error" role="alert">{scheduleError}</p>}
              </div>
            )}
            <div className="publish-actions">
              {state === "draft" && <button type="button" className="btn" onClick={() => saveAll()} disabled={!anyDirty || saving}>Αποθήκευση πρόχειρου</button>}
              {state === "scheduled" && <div className="row">
                <button type="button" className="btn" disabled={!!busy} onClick={() => publish(scheduleAt, "schedule")}>Αλλαγή ημερομηνίας</button>
                <button type="button" className="btn btn--live" disabled={!!busy} onClick={() => publish(null, "now")}>Δημοσίευση τώρα</button>
              </div>}
              {state === "published" && session.settings.blogEnabled && <a className="btn" href={`${session.publicOrigin}/blog/${server.slug}`} target="_blank" rel="noopener"><Icon name="external" size="sm" />Προβολή στον ιστότοπο</a>}
              {(state === "published" || state === "scheduled") && <button type="button" className="btn btn--danger" onClick={unpublish} disabled={!!busy}>{state === "scheduled" ? "Ακύρωση προγραμματισμού" : "Απόσυρση"}</button>}
              {state !== "trash" && <div className="row">
                <button type="button" className="btn btn--quiet btn--sm" onClick={duplicate}><Icon name="copy" size="sm" />Αντίγραφο</button>
                <button type="button" className="btn btn--quiet btn--sm" onClick={trashPost} disabled={!!busy}><Icon name="trash" size="sm" />Στον κάδο</button>
              </div>}
            </div>
          </Panel>

          <Panel icon="translate" title="Γλώσσες">
            <Field label="Γλώσσα πρωτοτύπου" htmlFor="ed-lang" hint="Η γλώσσα στην οποία γράφεται το άρθρο.">
              <PSA.Select id="ed-lang" ariaLabel="Γλώσσα πρωτοτύπου" value={draft.lang} disabled={readOnly}
                onChange={(lang) => { setField("lang", lang); setTab(lang); }}
                options={LANGS.filter((l) => l === draft.lang || !(trs[l] && trs[l].saved)).map((l) => ({ value: l, label: LANG_NAME[l] }))} />
            </Field>
            <ul className="revisions">
              {LANGS.filter((l) => l !== draft.lang).map((l) => {
                const t = trs[l];
                return (
                  <li key={l}>
                    <span className="grow">
                      <strong>{LANG_NAME[l]}</strong>
                      <span className="muted" style={{ display: "block" }}>{!t || !t.saved ? "Χωρίς μετάφραση" : t.saved.status === "ready" ? "Εγκεκριμένη" : t.saved.origin === "machine" ? "Αυτόματη, για έλεγχο" : "Πρόχειρη"}</span>
                    </span>
                    <button type="button" className="btn btn--sm" onClick={() => setTab(l)}>Άνοιγμα</button>
                  </li>
                );
              })}
            </ul>
            <button type="button" className="btn btn--sm" disabled={!!translating || readOnly || !draft.body.trim() || LANGS.every((l) => l === draft.lang || (trs[l] && trs[l].saved))} onClick={translateMissing}>
              <Icon name="translate" size="sm" />Αυτόματη μετάφραση όσων λείπουν
            </button>
            <p className="hint">Με το μοντέλο GLM-5.3 Flash της Z.ai. Οι μεταφράσεις μένουν πρόχειρες μέχρι να τις εγκρίνετε.</p>
          </Panel>

          <Panel icon="cover" title="Εξώφυλλο">
            <p className="hint">Η εικόνα εξωφύλλου είναι απαραίτητη για τη δημοσίευση και τον προγραμματισμό.</p>
            <div className="cover">
              {draft.coverUrl ? <img src={smallCover(draft.coverUrl)} alt="" draggable="false" /> : <span className="muted">Χωρίς εικόνα</span>}
            </div>
            <div className="cover-actions">
              <button id="ed-cover" type="button" className="btn btn--sm" disabled={readOnly} onClick={() => setPicker({ mode: "cover", initialAlt: draft.coverAlt, onPick: ({ media, alt }) => setDraft((d) => ({ ...d, coverMediaId: media.id, coverUrl: media.url, coverAlt: alt })) })}>
                <Icon name="imagePlus" size="sm" />{draft.coverUrl ? "Αλλαγή" : "Επιλογή εικόνας"}
              </button>
              {draft.coverUrl && <button type="button" className="btn btn--quiet btn--sm" disabled={readOnly} onClick={() => setDraft((d) => ({ ...d, coverMediaId: null, coverUrl: null, coverAlt: "" }))}>Αφαίρεση</button>}
            </div>
            {draft.coverUrl && (
              <Field label="Περιγραφή εξωφύλλου" htmlFor={isSource ? "ed-cover-alt" : "ed-cover-alt-src"} count={draft.coverAlt.length} max={250} hint="Απαραίτητη για τη δημοσίευση.">
                <input id={isSource ? "ed-cover-alt" : "ed-cover-alt-src"} className="input" value={draft.coverAlt} readOnly={readOnly} onChange={(e) => setField("coverAlt", e.target.value)} />
              </Field>
            )}
          </Panel>

          <Panel icon="link" title="Διεύθυνση και συντάκτης">
            <Field label={state === "published" ? "Διεύθυνση (URL)" : "Διεύθυνση μετά τη δημοσίευση"} htmlFor="ed-slug"
                   error={slugState && !slugState.available ? (slugState.reason === "slug_taken" ? "Χρησιμοποιείται ήδη από άλλο άρθρο." : "Μόνο λατινικά πεζά, αριθμοί και παύλες.") : null}
                   hint={server.firstPublishedAt ? "Αν την αλλάξετε, η παλιά διεύθυνση θα οδηγεί αυτόματα στη νέα." : slugTouched ? null : "Συμπληρώνεται αυτόματα από τον τίτλο."}>
              <div className="input-group">
                <span className="input-group__prefix mono">/blog/</span>
                <input id="ed-slug" className="input mono" value={draft.slug} readOnly={readOnly} spellCheck="false"
                       onChange={(e) => { setSlugTouched(true); setField("slug", e.target.value.toLowerCase().replace(/\s+/g, "-")); }}
                       onBlur={() => { if (text) { const s = text.slugify(draft.slug); if (s && s !== draft.slug) setField("slug", s); } }} />
              </div>
            </Field>
            <Field label="Συντάκτης" htmlFor="ed-author"
                   error={problems.some((p) => p.code === "writer_required") && !draft.writerId ? "Διαλέξτε συντάκτη." : null}
                   hint={<>Το όνομα και η φωτογραφία που εμφανίζονται στο άρθρο. <PSA.Link to="/writers">Διαχείριση συντακτών</PSA.Link></>}>
              <PSA.Select id="ed-author" ariaLabel="Συντάκτης" value={writerList.data ? draft.writerId : null} disabled={readOnly || !writerList.data}
                onChange={(writerId) => setField("writerId", writerId)} placeholder={writerList.data ? "Επιλέξτε συντάκτη" : "Φόρτωση…"}
                invalid={problems.some((p) => p.code === "writer_required") && !draft.writerId}
                emptyText="Δεν υπάρχει ακόμη συντάκτης."
                options={writers.map((w) => ({ value: w.id, label: w.name, avatarUrl: w.photoSmallUrl || w.photoUrl }))}
                action={{ label: "Νέος συντάκτης", onAction: () => setAddingWriter(true) }} />
              {writerList.error && <PSA.ErrorNotice error={writerList.error} onRetry={writerList.reload} />}
            </Field>
          </Panel>

          <Panel icon="globe" title="Κοινοποίηση" open={false}>
            <p className="hint">Έτσι εμφανίζεται ο σύνδεσμος όταν μοιράζεται (Facebook, Viber, WhatsApp). Τα μεμονωμένα άρθρα δεν μπαίνουν στην αναζήτηση Google· μόνο η σελίδα /blog.</p>
            <Snippet title={sharing.title} slug={draft.slug} desc={sharing.description} image={sharing.image} live={state === "published" && session.settings.blogEnabled} />
            <p className="hint">Ο τίτλος, η περιγραφή και η εικόνα δημιουργούνται αυτόματα από το άρθρο. Αφήστε τα πεδία κενά για αυτόματη ενημέρωση.</p>
            <details className="subpanel">
              <summary>Προσαρμογή τίτλου και περιγραφής<Icon name="chevronDown" size="sm" className="chev" /></summary>
              <div className="subpanel__body">
              <Field label="Τίτλος" htmlFor="ed-seo-title" optional count={sharingDraft.seoTitle.length} max={120}>
                <input id="ed-seo-title" className="input" maxLength={120} lang={tab} value={sharingDraft.seoTitle} readOnly={readOnly} onChange={(e) => setSharingField("seoTitle", e.target.value)} placeholder={sharingDraft.title} />
              </Field>
              <Field label="Περιγραφή" htmlFor="ed-seo-desc" optional count={sharingDraft.seoDescription.length} max={160}>
                <textarea id="ed-seo-desc" className="textarea" rows={3} maxLength={320} lang={tab} value={sharingDraft.seoDescription} readOnly={readOnly} onChange={(e) => setSharingField("seoDescription", e.target.value)} placeholder={metaFor({ ...sharingDraft, seoDescription: "" }).description} />
              </Field>
              <button type="button" className="btn btn--quiet btn--sm" disabled={readOnly || (!sharingDraft.seoTitle && !sharingDraft.seoDescription)} onClick={() => { setSharingField("seoTitle", ""); setSharingField("seoDescription", ""); }}>Επαναφορά στα αυτόματα</button>
              </div>
            </details>
          </Panel>

          <HistoryPanel postId={id} version={server.version} onRestore={(snap) => {
            setTab(draft.lang);
            setDraft((d) => ({ ...d, title: snap.title, excerpt: snap.excerpt, body: snap.body, seoTitle: snap.seoTitle || "", seoDescription: snap.seoDescription || "", coverAlt: snap.coverAlt || d.coverAlt }));
            toast("Η έκδοση επανήλθε στον επεξεργαστή. Αποθηκεύστε για να κρατηθεί.");
          }} />
        </aside>

        {addingWriter && <PSA.WriterDialog onClose={() => setAddingWriter(false)} onSaved={(writer) => { writerList.setData((data) => ({ ...data, writers: [...((data && data.writers) || []).filter((w) => w.id !== writer.id), writer] })); setField("writerId", writer.id); setAddingWriter(false); writerList.reload(); }} />}
        {picker && <PSA.MediaPicker mode={picker.mode} initialAlt={picker.initialAlt} initialMedia={picker.initialMedia} onClose={() => setPicker(null)} onPick={(choice) => { setPicker(null); picker.onPick(choice); }} />}

        {conflict && (
          <PSA.Dialog open title="Το άρθρο άλλαξε στο μεταξύ" onClose={() => setConflict(null)} footer={<>
            <button type="button" className="btn" onClick={async () => { setConflict(null); await fetchPost(); toast("Φορτώθηκε η πιο πρόσφατη έκδοση. Οι αλλαγές σας κρατήθηκαν σε αυτή τη συσκευή και επαναφέρονται από την ειδοποίηση πάνω από το κείμενο."); }}>Φόρτωση της νέας έκδοσης</button>
            <button type="button" className="btn btn--primary" onClick={async () => {
              if (conflict.post) { live.current.server = conflict.post; setServer(conflict.post); }
              if (conflict.translation) { const t = conflict.translation; live.current.trs = { ...live.current.trs, [t.lang]: { ...live.current.trs[t.lang], saved: t } }; setTrs((x) => ({ ...x, [t.lang]: { ...x[t.lang], saved: t } })); }
              setConflict(null);
              await saveAll();
            }}>Κράτηση των δικών μου</button>
          </>}>
            <p>
              Το άρθρο αποθηκεύτηκε από άλλη καρτέλα ή συσκευή{conflict.post ? ` ${fmtRelative(conflict.post.updatedAt)}` : ""}, ενώ γράφατε εδώ.
            </p>
            <p className="muted">Η «Φόρτωση» ανοίγει εκείνη την έκδοση και κρατά τις αλλαγές σας σε αυτή τη συσκευή για επαναφορά. Η «Κράτηση» την αντικαθιστά με τις δικές σας.</p>
          </PSA.Dialog>
        )}
      </div>
    );
  };

  const TitleInput = ({ id, value, onChange, readOnly, lang, placeholder = "Τίτλος άρθρου" }) => {
    const ref = useRef(null);
    PSA.useAutosize(ref, value);
    return <textarea ref={ref} id={id} className="title-input" rows={1} lang={lang} value={value} readOnly={readOnly} placeholder={placeholder} aria-label="Τίτλος" maxLength={200}
                     onChange={(e) => onChange(e.target.value.replace(/\n/g, " "))} onKeyDown={(e) => { if (e.key === "Enter") e.preventDefault(); }} />;
  };
  const ExcerptInput = ({ value, onChange, readOnly, lang, placeholder = "Περίληψη (προαιρετική): μία ή δύο προτάσεις για τη λίστα των άρθρων" }) => {
    const ref = useRef(null);
    PSA.useAutosize(ref, value || placeholder);
    return <textarea ref={ref} className="excerpt-input" rows={1} lang={lang} value={value} readOnly={readOnly} placeholder={placeholder} aria-label="Περίληψη" maxLength={320} onChange={(e) => onChange(e.target.value)} />;
  };
})();
