// Editor: real thumbnail data + N independent text overlays. Each text has
// its own pre/hl/post, position (x/y%), alignment, color, and font scale.
// Image is generated by Luma without text — texts are CSS overlays composited
// into the final PNG at download time via canvas.

const EMOTION_OPTIONS = [
  { id: "shock",     label: "Choque",    glyph: "😱" },
  { id: "happy",     label: "Sorrindo",  glyph: "😊" },
  { id: "laughing",  label: "Rindo",     glyph: "😂" },
  { id: "excited",   label: "Empolgado", glyph: "🤩" },
  { id: "serious",   label: "Sério",     glyph: "😐" },
  { id: "thinking",  label: "Pensativo", glyph: "🤔" },
  { id: "angry",     label: "Bravo",     glyph: "😠" },
  { id: "sad",       label: "Triste",    glyph: "😢" },
];

const HL_COLORS = ["#FBBF24", "#FF3B30", "#34D399", "#60A5FA", "#FFFFFF"];
const ALIGNS = [
  { id: "left",   label: "Esquerda" },
  { id: "center", label: "Centro" },
  { id: "right",  label: "Direita" },
];
const DEFAULT_OVERLAY = window.DEFAULT_OVERLAY || { hl_color: "#FBBF24", behind_subject: false, font_scale: 1 };
const DEFAULT_AVATAR_OVERLAY = { x: 70, y: 50, scale: 1 };

const newTextEntry = (overrides = {}) => ({
  id: (typeof crypto !== "undefined" && crypto.randomUUID) ? crypto.randomUUID() : `t${Date.now()}-${Math.random()}`,
  pre: "",
  hl: "",
  post: "",
  x: 50,
  y: 50,
  align: "center",
  hl_color: "#FBBF24",
  font_scale: 1,
  behind_subject: false,
  ...overrides,
});

const Editor = ({ thumbnailId, onBack, onDownload, userAvatarUrl }) => {
  const [thumb, setThumb] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [texts, setTexts] = React.useState([]);
  const [selectedId, setSelectedId] = React.useState(null);
  const [overlay, setOverlay] = React.useState(DEFAULT_OVERLAY);
  const [avatarOverlay, setAvatarOverlay] = React.useState(DEFAULT_AVATAR_OVERLAY);
  const [variationIdx, setVariationIdx] = React.useState(0);

  // When the thumb hydrates, jump to whichever variation the user previously
  // picked as the "winner" so the editor opens on the same one as the gallery.
  React.useEffect(() => {
    if (!thumb?.variations || !thumb?.selected_variation_id) return;
    const completedNow = thumb.variations.filter((v) => v.state === "completed" && v.image_url);
    const idx = completedNow.findIndex((v) => v.id === thumb.selected_variation_id);
    if (idx >= 0) setVariationIdx(idx);
  }, [thumb?.selected_variation_id, thumb?.variations]);

  const pickVariationAndPersist = (i, variationId) => {
    setVariationIdx(i);
    if (!variationId || !thumbnailId) return;
    window.gtAPI.fetch(`/thumbnails/${thumbnailId}`, {
      method: "PATCH",
      body: JSON.stringify({ selected_variation_id: variationId }),
    }).then((updated) => {
      setThumb((t) => t && ({ ...t, selected_variation_id: updated.selected_variation_id }));
    }).catch((e) => console.warn("save selected variation failed:", e));
  };
  const [saving, setSaving] = React.useState(false);
  const [saveStatus, setSaveStatus] = React.useState(null);
  const [subjectUrl, setSubjectUrl] = React.useState(null);
  const [bgRemovalState, setBgRemovalState] = React.useState("idle");
  const [bgRemovalErr, setBgRemovalErr] = React.useState(null);
  const [downloading, setDownloading] = React.useState(false);
  const [showYTPreview, setShowYTPreview] = React.useState(false);
  const [editingEmotion, setEditingEmotion] = React.useState(null); // id of the NEW pending variation
  const [emotionPick, setEmotionPick] = React.useState(null); // emotion option staged for confirm
  const [submittingEmotion, setSubmittingEmotion] = React.useState(false);
  const [emotionError, setEmotionError] = React.useState(null);
  const stageRef = React.useRef(null);
  const saveTimerRef = React.useRef(null);
  const pollerActiveRef = React.useRef(false);
  const pollerTimerRef = React.useRef(null);
  const editingEmotionRef = React.useRef(null);
  React.useEffect(() => { editingEmotionRef.current = editingEmotion; }, [editingEmotion]);

  // Re-fetch and refresh server-driven fields (status, variations, avatar
  // subject). Preserves local edits to texts / overlay / avatarOverlay.
  // Keeps polling at 3s intervals while status is pending. Memoized on
  // thumbnailId only so emotion-edit state changes don't re-trigger the
  // initial-load effect (which would clobber the user's local text edits).
  //
  // URL stability: GET /thumbnails/:id re-signs URLs every call (TTL 1h), so
  // naïvely swapping in the new payload makes every <img> see a brand-new src
  // and re-decode → visible flicker on each 3s tick. For variations whose
  // state didn't change (still "completed"), we keep the cached row so URLs
  // stay stable. New transitions (pending → completed/failed) take the fresh
  // payload. Same idea for avatar_subject_url.
  const pollOnce = React.useCallback(async () => {
    if (!thumbnailId) return;
    try {
      const t = await window.gtAPI.fetch(`/thumbnails/${thumbnailId}`);
      if (!pollerActiveRef.current) return;
      setThumb((prev) => {
        if (!prev) return t;
        const prevVars = prev.variations || [];
        const mergedVars = (t.variations || []).map((v) => {
          const cached = prevVars.find((p) => p.id === v.id);
          if (cached && cached.state === v.state && cached.image_url && v.state === "completed") {
            return cached;
          }
          return v;
        });
        return {
          ...prev,
          status: t.status,
          variations: mergedVars,
          avatar_subject_url: prev.avatar_subject_url ?? t.avatar_subject_url,
          avatar_subject_url_full: prev.avatar_subject_url_full ?? t.avatar_subject_url_full,
        };
      });
      if (editingEmotionRef.current) {
        const editedVar = (t.variations || []).find((v) => v.id === editingEmotionRef.current);
        if (editedVar && editedVar.state !== "pending") setEditingEmotion(null);
      }
      if (t.status === "pending" && pollerActiveRef.current) {
        pollerTimerRef.current = setTimeout(pollOnce, 3000);
      }
    } catch (e) {
      console.warn("poll failed:", e);
    }
  }, [thumbnailId]);

  // Fetch thumbnail. Initial load hydrates the local edit state; subsequent
  // polls (while status=pending) only refresh server-driven fields so the
  // user's text edits aren't clobbered by the poll.
  React.useEffect(() => {
    if (!thumbnailId) { setError("nenhuma thumbnail selecionada"); return; }
    let active = true;
    pollerActiveRef.current = true;

    (async () => {
      try {
        const t = await window.gtAPI.fetch(`/thumbnails/${thumbnailId}`);
        if (!active) return;
        setThumb(t);
        const initial = Array.isArray(t.texts) ? t.texts : [];
        setTexts(initial);
        setSelectedId(initial[0]?.id || null);
        setOverlay({ ...DEFAULT_OVERLAY, ...(t.text_overlay || {}) });
        setAvatarOverlay({ ...DEFAULT_AVATAR_OVERLAY, ...(t.avatar_overlay || {}) });
        if (t.status === "pending") {
          pollerTimerRef.current = setTimeout(pollOnce, 3000);
        }
      } catch (e) {
        if (active) setError(e.message || "falha ao carregar");
      }
    })();
    return () => {
      active = false;
      pollerActiveRef.current = false;
      if (pollerTimerRef.current) { clearTimeout(pollerTimerRef.current); pollerTimerRef.current = null; }
    };
  }, [thumbnailId, pollOnce]);

  // Hydrate persisted subject layer when the active variation changes.
  const heroIdRef = React.useRef(null);
  React.useEffect(() => {
    const completedNow = (thumb?.variations || []).filter((v) => v.state === "completed" && v.image_url);
    const heroNow = completedNow[Math.min(variationIdx, completedNow.length - 1)] || null;
    if (!heroNow) return;
    if (heroIdRef.current === heroNow.id) return;
    heroIdRef.current = heroNow.id;
    setSubjectUrl((curr) => {
      if (curr && curr.startsWith("blob:")) URL.revokeObjectURL(curr);
      return heroNow.subject_image_url || null;
    });
    setBgRemovalState(heroNow.subject_image_url ? "ready" : "idle");
    setBgRemovalErr(null);
  }, [variationIdx, thumb]);

  // Auto-save on text or overlay change.
  React.useEffect(() => {
    if (!thumb) return;
    const initialTexts = Array.isArray(thumb.texts) ? thumb.texts : [];
    const initialOverlay = { ...DEFAULT_OVERLAY, ...(thumb.text_overlay || {}) };
    const initialAvatar = { ...DEFAULT_AVATAR_OVERLAY, ...(thumb.avatar_overlay || {}) };
    const dirty =
      JSON.stringify(texts) !== JSON.stringify(initialTexts) ||
      JSON.stringify(overlay) !== JSON.stringify(initialOverlay) ||
      JSON.stringify(avatarOverlay) !== JSON.stringify(initialAvatar);
    if (!dirty) return;

    if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    saveTimerRef.current = setTimeout(async () => {
      setSaving(true);
      try {
        const updated = await window.gtAPI.fetch(`/thumbnails/${thumbnailId}`, {
          method: "PATCH",
          body: JSON.stringify({ texts, text_overlay: overlay, avatar_overlay: avatarOverlay }),
        });
        setThumb((t) => t && ({
          ...t,
          texts: updated.texts,
          text_overlay: updated.text_overlay,
          avatar_overlay: updated.avatar_overlay,
        }));
        setSaveStatus("saved");
        setTimeout(() => setSaveStatus(null), 1500);
      } catch (e) {
        setSaveStatus("error");
      } finally {
        setSaving(false);
      }
    }, 700);

    return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); };
  }, [texts, overlay, avatarOverlay, thumb, thumbnailId]);

  // Drag handler for the avatar layer (scene_only mode).
  const onAvatarDragStart = (e) => {
    if (!stageRef.current) return;
    e.preventDefault();
    e.stopPropagation();
    setSelectedId(null);
    const rect = stageRef.current.getBoundingClientRect();
    const startMx = e.clientX;
    const startMy = e.clientY;
    const startX = avatarOverlay.x;
    const startY = avatarOverlay.y;
    const onMove = (ev) => {
      const dx = ((ev.clientX - startMx) / rect.width) * 100;
      const dy = ((ev.clientY - startMy) / rect.height) * 100;
      setAvatarOverlay((o) => ({
        ...o,
        x: Math.max(-30, Math.min(130, startX + dx)),
        y: Math.max(-30, Math.min(130, startY + dy)),
      }));
    };
    const onUp = () => {
      document.removeEventListener("pointermove", onMove);
      document.removeEventListener("pointerup", onUp);
    };
    document.addEventListener("pointermove", onMove);
    document.addEventListener("pointerup", onUp);
  };

  // Drag handler — moves the text whose id is passed in.
  const onTextDragStart = (e, textId) => {
    if (!stageRef.current) return;
    e.preventDefault();
    e.stopPropagation();
    setSelectedId(textId);
    const rect = stageRef.current.getBoundingClientRect();
    const startMx = e.clientX;
    const startMy = e.clientY;
    const target = texts.find((t) => t.id === textId);
    if (!target) return;
    const startX = target.x;
    const startY = target.y;
    const onMove = (ev) => {
      const dx = ((ev.clientX - startMx) / rect.width) * 100;
      const dy = ((ev.clientY - startMy) / rect.height) * 100;
      const x = Math.max(0, Math.min(100, startX + dx));
      const y = Math.max(0, Math.min(100, startY + dy));
      setTexts((curr) => curr.map((t) => t.id === textId ? { ...t, x, y } : t));
    };
    const onUp = () => {
      document.removeEventListener("pointermove", onMove);
      document.removeEventListener("pointerup", onUp);
    };
    document.addEventListener("pointermove", onMove);
    document.addEventListener("pointerup", onUp);
  };

  const completed = (thumb?.variations || []).filter((v) => v.state === "completed" && v.image_url);
  const pendingVars = (thumb?.variations || []).filter((v) => v.state === "pending");
  const hero = completed[Math.min(variationIdx, completed.length - 1)] || null;

  // Background removal lazy.
  const runBgRemoval = React.useCallback(async () => {
    if (!hero || bgRemovalState === "loading" || bgRemovalState === "ready") return;
    setBgRemovalState("loading");
    setBgRemovalErr(null);
    try {
      const removeBackground = await window.__loadBgRemoval();
      if (!removeBackground) throw new Error("biblioteca não carregou");
      const blob = await removeBackground(hero.image_url, { progress: () => {} });
      const url = URL.createObjectURL(blob);
      setSubjectUrl(url);
      setBgRemovalState("ready");
      try {
        const fd = new FormData();
        fd.append("subject", new File([blob], "subject.png", { type: "image/png" }));
        const res = await window.gtAPI.fetch(
          `/thumbnails/${thumbnailId}/variations/${hero.id}/subject`,
          { method: "POST", body: fd },
        );
        if (res?.subject_image_url) {
          setSubjectUrl((curr) => {
            if (curr && curr.startsWith("blob:")) URL.revokeObjectURL(curr);
            return res.subject_image_url;
          });
          setThumb((t) => t && ({
            ...t,
            variations: (t.variations || []).map((v) =>
              v.id === hero.id ? { ...v, subject_image_url: res.subject_image_url } : v,
            ),
          }));
        }
      } catch (uploadErr) {
        console.warn("subject upload failed (non-fatal):", uploadErr);
      }
    } catch (e) {
      console.error("bg removal failed:", e);
      setBgRemovalErr(e.message || String(e));
      setBgRemovalState("error");
    }
  }, [hero, bgRemovalState, thumbnailId]);

  const anyBehind = texts.some((t) => t.behind_subject);
  React.useEffect(() => {
    if (!anyBehind) return;
    if (bgRemovalState !== "idle") return;
    if (hero?.subject_image_url) return;
    if (subjectUrl) return;
    runBgRemoval();
  }, [anyBehind, bgRemovalState, hero, subjectUrl, runBgRemoval]);

  const downloadComposed = async () => {
    if (!hero || downloading) return;
    setDownloading(true);
    try {
      const baseImg = await loadImage(hero.image_url);
      const canvas = document.createElement("canvas");
      canvas.width = baseImg.naturalWidth;
      canvas.height = baseImg.naturalHeight;
      const ctx = canvas.getContext("2d");
      ctx.drawImage(baseImg, 0, 0);

      // Layer order: behind texts → subject cutout → front texts.
      // Subject = real avatar photo (scene_only mode) OR legacy bg-removed
      // generated subject. Real avatar wins when present.
      const behindTexts = texts.filter((t) => t.behind_subject);
      const frontTexts = texts.filter((t) => !t.behind_subject);
      const realSubjectUrl = thumb.scene_only ? thumb.avatar_subject_url : null;
      behindTexts.forEach((t) => drawText(ctx, canvas.width, canvas.height, t));
      if (realSubjectUrl) {
        const subImg = await loadImage(realSubjectUrl);
        // Match on-screen positioning exactly: x/y is the CENTER of the avatar
        // in % of canvas; scale is height multiplier (1 = canvas height).
        const targetH = canvas.height * (avatarOverlay.scale ?? 1);
        const ratio = subImg.naturalWidth / subImg.naturalHeight;
        const targetW = targetH * ratio;
        const cx = (canvas.width * (avatarOverlay.x ?? 70)) / 100;
        const cy = (canvas.height * (avatarOverlay.y ?? 50)) / 100;
        ctx.drawImage(subImg, cx - targetW / 2, cy - targetH / 2, targetW, targetH);
      } else if (behindTexts.length > 0 && subjectUrl) {
        const subImg = await loadImage(subjectUrl);
        ctx.drawImage(subImg, 0, 0, canvas.width, canvas.height);
      }
      frontTexts.forEach((t) => drawText(ctx, canvas.width, canvas.height, t));

      const blob = await new Promise((r) => canvas.toBlob(r, "image/png"));
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = `thumbnail-${thumbnailId.slice(0, 8)}.png`;
      document.body.appendChild(a);
      a.click();
      a.remove();
      URL.revokeObjectURL(url);
      onDownload && onDownload("Thumbnail baixada com texto");
    } catch (e) {
      console.error("download composed failed:", e);
      onDownload && onDownload(`Falha no download: ${e.message}`);
    } finally {
      setDownloading(false);
    }
  };

  const confirmEditEmotion = async () => {
    if (!hero || editingEmotion || !emotionPick) return;
    setEmotionError(null);
    const sourceId = hero.id;
    const emotion = emotionPick.id;
    setSubmittingEmotion(true);
    try {
      const res = await window.gtAPI.fetch(
        `/thumbnails/${thumbnailId}/variations/${sourceId}/edit-emotion`,
        { method: "POST", body: JSON.stringify({ emotion }) },
      );
      // Append the new pending variation locally so the switcher shows it
      // immediately; the source variation stays untouched.
      setThumb((prev) => prev ? {
        ...prev,
        status: "pending",
        variations: [...(prev.variations || []), res.variation],
      } : prev);
      setEditingEmotion(res.variation.id);
      setEmotionPick(null);
      pollerActiveRef.current = true;
      if (pollerTimerRef.current) clearTimeout(pollerTimerRef.current);
      pollerTimerRef.current = setTimeout(pollOnce, 3000);
    } catch (e) {
      setEmotionError(e.message || "falha ao editar emoção");
      setEmotionPick(null);
    } finally {
      setSubmittingEmotion(false);
    }
  };

  // Helpers for editing the selected text.
  const selected = texts.find((t) => t.id === selectedId) || null;
  const updateSelected = (patch) => {
    if (!selected) return;
    setTexts((curr) => curr.map((t) => t.id === selected.id ? { ...t, ...patch } : t));
  };
  const addText = () => {
    const t = newTextEntry({ x: 50, y: 50, align: "center", hl: "TEXTO" });
    setTexts((curr) => [...curr, t]);
    setSelectedId(t.id);
  };
  const removeText = (id) => {
    setTexts((curr) => curr.filter((t) => t.id !== id));
    setSelectedId((curr) => curr === id ? (texts.find((t) => t.id !== id)?.id || null) : curr);
  };

  if (!thumbnailId) {
    return <div className="fade-in">
      <button className="btn btn-sm btn-ghost" onClick={onBack}>← Voltar</button>
      <div className="muted" style={{padding: 40, textAlign: "center"}}>Nenhuma thumbnail selecionada.</div>
    </div>;
  }
  if (error) {
    return <div className="fade-in">
      <button className="btn btn-sm btn-ghost" onClick={onBack}>← Voltar</button>
      <div className="card" style={{padding: 20, marginTop: 16, borderColor: "var(--brand)", color: "var(--brand)"}}>
        {error}
      </div>
    </div>;
  }
  if (!thumb) {
    return <div className="fade-in" style={{padding: 40, textAlign: "center"}}>
      <span className="ring" style={{width: 28, height: 28, borderWidth: 2, color: "var(--brand)"}} />
      <div className="muted" style={{marginTop: 12}}>Carregando…</div>
    </div>;
  }

  return (
    <div className="fade-in">
      <div className="row-gap" style={{marginBottom: 18}}>
        <button className="btn btn-sm btn-ghost" onClick={onBack}>← Voltar</button>
        <div>
          <div className="eyebrow">Editor</div>
          <div style={{fontFamily: "var(--font-display)", fontWeight: 700, fontSize: 22}} title={thumb.youtube_title || ""}>
            {thumb.youtube_title || "Sua thumbnail"}
          </div>
        </div>
        <div className="spacer" />
        <span className="muted mono" style={{fontSize: 11}}>
          {saving ? "salvando…" : saveStatus === "saved" ? "✓ salvo" : saveStatus === "error" ? "erro ao salvar" : ""}
        </span>
        <button className="btn btn-sm" onClick={() => setShowYTPreview(true)} disabled={!hero}>
          <Icon name="play" size={13} /> Preview YouTube
        </button>
        <button className="btn btn-sm btn-primary" onClick={downloadComposed} disabled={!hero || downloading}>
          <Icon name="download" size={13} /> {downloading ? "Compondo…" : "Baixar PNG"}
        </button>
      </div>

      <div className="editor-shell" style={{display: "grid", gridTemplateColumns: "1fr 340px", gap: 22}}>
          <div>
            <div ref={stageRef}
                 onClick={() => setSelectedId(null)}
                 style={{
                   position: "relative",
                   width: "100%",
                   aspectRatio: "16 / 9",
                   borderRadius: 14,
                   overflow: "hidden",
                   border: "1px solid var(--border)",
                   background: "var(--bg-2)",
                   userSelect: "none",
                 }}>
              <window.ComposedThumb
                imageUrl={hero?.image_url}
                subjectUrl={subjectUrl}
                avatarSubjectUrl={thumb.scene_only ? thumb.avatar_subject_url : null}
                avatarSubjectUrlFull={thumb.scene_only ? thumb.avatar_subject_url_full : null}
                avatarOverlay={avatarOverlay}
                texts={texts}
                overlay={overlay}
                draggable
                selectedTextId={selectedId}
                onTextPointerDown={onTextDragStart}
                onTextClick={(id) => setSelectedId(id)}
                onAvatarPointerDown={onAvatarDragStart}
              />
              {/* Pending placeholder while the image is still being generated.
                  Overlays still render so the user can compose text in advance. */}
              {!hero && (
                <div style={{
                  position: "absolute", inset: 0,
                  display: "grid", placeItems: "center",
                  color: "var(--text-3)", fontSize: 13, textAlign: "center",
                  pointerEvents: "none", zIndex: 4,
                }}>
                  <div>
                    <span className="ring" style={{width: 32, height: 32, borderWidth: 3, color: "var(--brand)", display: "inline-block"}} />
                    <div style={{marginTop: 10, fontWeight: 600, color: "var(--text-2)"}}>
                      {thumb.status === "failed" ? "Geração falhou" : "Gerando imagem…"}
                    </div>
                    <div className="muted" style={{marginTop: 4, fontSize: 11}}>
                      {thumb.status === "failed"
                        ? "Você ainda pode editar os textos."
                        : "Você já pode editar os textos enquanto isso."}
                    </div>
                  </div>
                </div>
              )}
              {texts.length === 0 && (
                <div style={{position: "absolute", left: "50%", top: "50%", transform: "translate(-50%, -50%)", color: "rgba(255,255,255,0.5)", fontSize: 14, pointerEvents: "none", textAlign: "center"}}>
                  Sem texto. Clique em "+ Texto" pra adicionar.
                </div>
              )}

              {bgRemovalState === "loading" && (
                <div style={{position: "absolute", inset: 0, background: "rgba(0,0,0,0.45)", display: "grid", placeItems: "center", zIndex: 5, color: "#fff", fontSize: 13, textAlign: "center", padding: 16}}>
                  <div>
                    <span className="ring" style={{width: 32, height: 32, borderWidth: 3, color: "#fff", display: "inline-block"}} />
                    <div style={{marginTop: 12, fontWeight: 600}}>Recortando o sujeito…</div>
                    <div className="muted-2" style={{marginTop: 4, fontSize: 11, color: "rgba(255,255,255,0.7)"}}>
                      Pra deixar o texto atrás da pessoa. Primeira vez baixa o modelo (~80 MB).
                    </div>
                  </div>
                </div>
              )}
              {bgRemovalState === "error" && (
                <div style={{position: "absolute", top: 8, left: 8, right: 8, background: "rgba(225,29,72,0.9)", color: "#fff", padding: 10, borderRadius: 8, fontSize: 12, zIndex: 5}}>
                  Falha ao recortar: {bgRemovalErr}
                </div>
              )}
            </div>

            {(completed.length > 1 || pendingVars.length > 0) && (
              <div className="row-gap" style={{gap: 8, marginTop: 12, flexWrap: "wrap"}}>
                {completed.map((v, i) => (
                  <button key={v.id}
                          className={"btn btn-sm " + (variationIdx === i ? "btn-primary" : "")}
                          onClick={() => pickVariationAndPersist(i, v.id)}>
                    Variação {String(i + 1).padStart(2, "0")}
                  </button>
                ))}
                {pendingVars.map((v) => (
                  <button key={v.id} className="btn btn-sm" disabled
                          style={{gap: 6, opacity: 0.7}}>
                    <span className="ring" style={{width: 11, height: 11, borderWidth: 2, color: "var(--brand)"}} />
                    Gerando…
                  </button>
                ))}
              </div>
            )}
            <div className="muted-2" style={{fontSize: 11, marginTop: 8}}>
              ⤤ Clique num texto pra selecionar · arraste pra reposicionar.
            </div>
          </div>

          {/* Side panel */}
          <aside className="editor-panel" style={{display: "flex", flexDirection: "column", gap: 18}}>
            {thumb.scene_only && thumb.avatar_subject_url && (
              <div className="section">
                <h4>Avatar</h4>
                <div className="muted" style={{fontSize: 11, marginBottom: 8}}>
                  Arraste no canvas pra posicionar.
                </div>
                <label style={{fontSize: 12, color: "var(--text-2)"}}>Tamanho</label>
                <input type="range" className="slider" min="0.4" max="2" step="0.05"
                       value={avatarOverlay.scale}
                       onChange={(e) => setAvatarOverlay((o) => ({ ...o, scale: parseFloat(e.target.value) }))} />
                <div className="muted mono" style={{fontSize: 10, textAlign: "right"}}>{Math.round(avatarOverlay.scale * 100)}%</div>
                <button className="btn btn-sm" style={{marginTop: 8, width: "100%", justifyContent: "center"}}
                        onClick={() => setAvatarOverlay(DEFAULT_AVATAR_OVERLAY)}
                        disabled={JSON.stringify(avatarOverlay) === JSON.stringify(DEFAULT_AVATAR_OVERLAY)}>
                  <Icon name="refresh" size={12} /> Posição padrão
                </button>
              </div>
            )}

            {/* Emotion editor — kicks off an image_edit on the active variation
                that produces a NEW pending variation. Source variation stays
                intact. Hidden in scene_only mode since the face is the real
                avatar photo composited on top, not part of the Luma image. */}
            {!thumb.scene_only && (
              <div className="section">
                <div className="row-gap" style={{alignItems: "baseline", marginBottom: 6}}>
                  <h4 style={{margin: 0}}>Trocar emoção</h4>
                  <span className="spacer" />
                  <span className="mono muted" style={{fontSize: 10}}>1 crédito</span>
                </div>
                <div className="muted" style={{fontSize: 11, marginBottom: 12}}>
                  Gera uma nova variação só com a expressão trocada — rosto, roupa e fundo ficam iguais.
                </div>
                <div style={{display: "grid", gridTemplateColumns: "repeat(2, 1fr)", gap: 8}}>
                  {EMOTION_OPTIONS.map((e) => (
                    <button key={e.id}
                            className="btn"
                            disabled={!hero || !!editingEmotion}
                            onClick={() => { setEmotionError(null); setEmotionPick(e); }}
                            title={e.label}
                            style={{
                              padding: "12px 8px",
                              flexDirection: "column",
                              gap: 6,
                              lineHeight: 1,
                              height: "auto",
                            }}>
                      <span style={{fontSize: 24}}>{e.glyph}</span>
                      <span style={{fontSize: 12, fontWeight: 500}}>{e.label}</span>
                    </button>
                  ))}
                </div>
                {editingEmotion && (
                  <div className="row-gap" style={{gap: 8, marginTop: 12, padding: "10px 12px", background: "var(--bg-2)", borderRadius: 10, fontSize: 12, alignItems: "center"}}>
                    <span className="ring" style={{width: 14, height: 14, borderWidth: 2, color: "var(--brand)"}} />
                    <span>Gerando nova variação… leva ~30s.</span>
                  </div>
                )}
                {emotionError && !editingEmotion && (
                  <div style={{marginTop: 12, padding: 10, borderRadius: 8, background: "var(--brand-soft, rgba(225,29,72,0.12))", color: "var(--brand)", fontSize: 12}}>
                    {emotionError}
                  </div>
                )}
              </div>
            )}

            {/* Text list */}
            <div className="section">
              <div className="row-gap" style={{alignItems: "baseline", marginBottom: 8}}>
                <h4 style={{margin: 0}}>Textos</h4>
                <span className="spacer" />
                <button className="btn btn-sm" onClick={addText} disabled={texts.length >= 8}>
                  <Icon name="plus" size={12} /> Texto
                </button>
              </div>
              <div className="col-gap" style={{gap: 6}}>
                {texts.map((t, i) => {
                  const label = (t.pre + " " + t.hl + " " + t.post).trim() || "(vazio)";
                  return (
                    <div key={t.id}
                         className={"text-list-item " + (selectedId === t.id ? "active" : "")}
                         onClick={() => setSelectedId(t.id)}
                         style={{
                           display: "flex", alignItems: "center", gap: 8,
                           padding: "8px 10px",
                           borderRadius: 8,
                           border: `1px solid ${selectedId === t.id ? "var(--brand)" : "var(--border)"}`,
                           background: selectedId === t.id ? "var(--brand-soft)" : "var(--bg-2)",
                           cursor: "pointer",
                           fontSize: 13,
                         }}>
                      <span className="mono" style={{fontSize: 10, color: "var(--text-3)"}}>
                        {String(i + 1).padStart(2, "0")}
                      </span>
                      <span style={{flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap"}}>
                        {label}
                      </span>
                      <button className="btn btn-sm btn-ghost"
                              onClick={(e) => { e.stopPropagation(); removeText(t.id); }}
                              title="Remover">
                        <Icon name="trash" size={11} />
                      </button>
                    </div>
                  );
                })}
                {texts.length === 0 && (
                  <div className="muted" style={{fontSize: 12, padding: "8px 0"}}>
                    Nenhum texto ainda.
                  </div>
                )}
              </div>
            </div>

            {/* Selected text fields */}
            {selected && (
              <>
                <div className="section">
                  <h4>Conteúdo</h4>
                  <div className="muted" style={{fontSize: 11, marginBottom: 8}}>
                    Aperte Enter pra quebrar linha.
                  </div>
                  <div className="col-gap" style={{gap: 10}}>
                    <div className="field">
                      <label>Antes do destaque</label>
                      <TextLineArea value={selected.pre} onChange={(v) => updateSelected({ pre: v })} />
                    </div>
                    <div className="field">
                      <label>Destaque</label>
                      <TextLineArea value={selected.hl} onChange={(v) => updateSelected({ hl: v })} />
                    </div>
                    <div className="field">
                      <label>Depois</label>
                      <TextLineArea value={selected.post} onChange={(v) => updateSelected({ post: v })} />
                    </div>
                  </div>
                </div>

                <div className="section">
                  <h4>Alinhamento</h4>
                  <div className="row-gap" style={{gap: 6}}>
                    {ALIGNS.map((a) => (
                      <button key={a.id}
                              className={"btn btn-sm " + (selected.align === a.id ? "btn-primary" : "")}
                              onClick={() => updateSelected({ align: a.id })}>{a.label}</button>
                    ))}
                  </div>
                </div>

                <div className="section">
                  <h4>Cor do destaque</h4>
                  <div className="row-gap" style={{gap: 8, flexWrap: "wrap"}}>
                    {HL_COLORS.map((c) => (
                      <button key={c}
                              onClick={() => updateSelected({ hl_color: c })}
                              style={{
                                width: 36, height: 36, borderRadius: 10,
                                border: `2px solid ${selected.hl_color === c ? "var(--brand)" : "var(--border)"}`,
                                background: c, cursor: "pointer", padding: 0,
                              }}
                              aria-label={c} />
                    ))}
                  </div>
                </div>

                <div className="section">
                  <h4>Tamanho</h4>
                  <input type="range" className="slider" min="0.5" max="2" step="0.05"
                         value={selected.font_scale}
                         onChange={(e) => updateSelected({ font_scale: parseFloat(e.target.value) })} />
                  <div className="muted mono" style={{fontSize: 10, textAlign: "right"}}>{Math.round(selected.font_scale * 100)}%</div>
                </div>

                <div className="section">
                  <label className="row-gap" style={{justifyContent: "space-between", padding: "10px 12px", background: "var(--bg-2)", borderRadius: 10, cursor: "pointer"}}>
                    <div>
                      <div style={{fontWeight: 600, fontSize: 14}}>Atrás do avatar</div>
                      <div className="muted" style={{fontSize: 11, marginTop: 2}}>Esse texto fica entre o fundo e o sujeito recortado.</div>
                    </div>
                    <input type="checkbox"
                           checked={!!selected.behind_subject}
                           onChange={(e) => updateSelected({ behind_subject: e.target.checked })} />
                  </label>
                  {selected.behind_subject && bgRemovalState === "loading" && (
                    <div className="row-gap" style={{gap: 8, marginTop: 8, padding: "8px 10px", background: "var(--bg-2)", borderRadius: 8, fontSize: 12, alignItems: "center"}}>
                      <span className="ring" style={{width: 12, height: 12, borderWidth: 2, color: "var(--brand)"}} />
                      <span>Recortando a pessoa…</span>
                    </div>
                  )}
                </div>
              </>
            )}
          </aside>
        </div>

      {showYTPreview && hero && (
        <window.YouTubePreviewModal
          thumbnail={{
            imageUrl: hero.image_url,
            subjectUrl,
            avatarSubjectUrl: thumb.scene_only ? thumb.avatar_subject_url : null,
            avatarSubjectUrlFull: thumb.scene_only ? thumb.avatar_subject_url_full : null,
            avatarOverlay,
            texts,
            overlay,
            title: thumb.youtube_title || "Sua thumbnail",
            channel: thumb.youtube_author || "Seu canal",
          }}
          userAvatarUrl={userAvatarUrl}
          onClose={() => setShowYTPreview(false)}
        />
      )}

      {emotionPick && hero && (
        <EmotionConfirmModal
          emotion={emotionPick}
          sourceImageUrl={hero.image_url}
          submitting={submittingEmotion}
          onCancel={() => !submittingEmotion && setEmotionPick(null)}
          onConfirm={confirmEditEmotion}
        />
      )}
    </div>
  );
};

const EmotionConfirmModal = ({ emotion, sourceImageUrl, submitting, onCancel, onConfirm }) => {
  React.useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !submitting) onCancel(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onCancel, submitting]);

  // Portal to <body> so the backdrop covers the full viewport. The Editor's
  // .fade-in wrapper animates `transform`, which creates a containing block
  // for `position: fixed` — without the portal the backdrop only covers the
  // editor pane.
  return ReactDOM.createPortal(
    <div onClick={onCancel}
         style={{
           position: "fixed", inset: 0, zIndex: 200,
           background: "rgba(0,0,0,0.55)",
           display: "grid", placeItems: "center", padding: 20,
         }}>
      <div onClick={(e) => e.stopPropagation()}
           className="card"
           style={{padding: 24, maxWidth: 460, width: "100%", display: "flex", flexDirection: "column", gap: 18}}>
        <div>
          <div className="eyebrow">Trocar emoção</div>
          <h3 style={{margin: "4px 0 0", fontSize: 20}}>Gerar variação com {emotion.label.toLowerCase()}</h3>
        </div>

        <div style={{
          position: "relative",
          aspectRatio: "16 / 9",
          borderRadius: 12,
          overflow: "hidden",
          border: "1px solid var(--border)",
          background: "var(--bg-2)",
        }}>
          <img src={sourceImageUrl} alt=""
               style={{width: "100%", height: "100%", objectFit: "cover"}} />
          <div style={{
            position: "absolute", top: 10, left: 10,
            background: "rgba(0,0,0,0.7)", color: "#fff",
            fontSize: 11, padding: "4px 8px", borderRadius: 6,
            fontFamily: "var(--font-mono)",
          }}>BASE</div>
          <div style={{
            position: "absolute", bottom: 10, right: 10,
            background: "var(--brand)", color: "#fff",
            display: "flex", alignItems: "center", gap: 6,
            fontSize: 13, padding: "6px 10px", borderRadius: 8,
            fontWeight: 600,
          }}>
            <span style={{fontSize: 18, lineHeight: 1}}>{emotion.glyph}</span>
            {emotion.label}
          </div>
        </div>

        <p className="muted-2" style={{margin: 0, fontSize: 13, lineHeight: 1.5}}>
          Vamos criar uma <strong>nova variação</strong> a partir dessa imagem trocando só a expressão facial.
          A variação atual fica intacta.
        </p>

        <div className="row-gap" style={{
          justifyContent: "space-between", alignItems: "center",
          padding: "10px 12px", background: "var(--bg-2)", borderRadius: 10,
        }}>
          <span style={{fontSize: 13, color: "var(--text-2)"}}>Custo</span>
          <span className="mono" style={{fontSize: 13, fontWeight: 600}}>1 crédito</span>
        </div>

        <div className="row-gap" style={{gap: 10, justifyContent: "flex-end"}}>
          <button className="btn" onClick={onCancel} disabled={submitting}>Cancelar</button>
          <button className="btn btn-primary" onClick={onConfirm} disabled={submitting}>
            {submitting
              ? <><span className="ring" style={{width: 14, height: 14, borderWidth: 2}} /> Enviando…</>
              : <><Icon name="sparkles" size={14} /> Gerar variação</>}
          </button>
        </div>
      </div>
    </div>,
    document.body,
  );
};

// Multi-line text input that auto-grows by line count. Used for pre/hl/post
// fields in the editor — Enter inserts a literal \n which the live preview
// (and canvas download) honor as a line break.
const TextLineArea = ({ value, onChange }) => {
  const rows = Math.max(1, (value || "").split("\n").length);
  return (
    <textarea
      value={value || ""}
      maxLength={200}
      rows={rows}
      spellCheck={false}
      onChange={(e) => onChange(e.target.value)}
      style={{
        width: "100%",
        padding: "8px 10px",
        borderRadius: 8,
        border: "1px solid var(--border)",
        background: "var(--bg-2)",
        color: "var(--text)",
        fontFamily: "inherit",
        fontSize: 14,
        lineHeight: 1.35,
        resize: "none",
        outline: "none",
      }}
    />
  );
};

// Helpers ---------------------------------------------------------------

function loadImage(src) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.crossOrigin = "anonymous";
    img.onload = () => resolve(img);
    img.onerror = () => reject(new Error("failed to load image"));
    img.src = src;
  });
}

// Renders a single text overlay onto the canvas. Anchors the LAST line's
// baseline at (text.x%, text.y%); alignment shifts each line's start X so it
// matches the CSS preview (translate -50%/-100% for center/right).
function drawText(ctx, w, h, text) {
  const fontPx = Math.round(w * 0.09 * (text.font_scale || 1));
  ctx.font = `900 ${fontPx}px "Montserrat", system-ui, sans-serif`;
  ctx.textBaseline = "alphabetic";
  ctx.textAlign = "left";

  // Build lines from pre/hl/post, honoring any \n the user typed (Enter in
  // the textarea creates literal line breaks). Spaces never auto-wrap —
  // matches the live preview's white-space: pre.
  const lines = [[]];
  const push = (str, color) => {
    if (!str) return;
    const segs = str.toUpperCase().split("\n");
    segs.forEach((seg, i) => {
      if (i > 0) lines.push([]);
      seg.split(/\s+/).filter(Boolean).forEach((word) => {
        lines[lines.length - 1].push({ word, color });
      });
    });
  };
  push(text.pre, "#fff");
  push(text.hl, text.hl_color || "#FBBF24");
  push(text.post, "#fff");
  while (lines.length > 1 && lines[lines.length - 1].length === 0) lines.pop();
  if (lines.every((l) => l.length === 0)) return;

  const align = text.align || "left";
  const xPct = text.x ?? 6;
  const anchorX = (xPct / 100) * w;
  const anchorY = ((text.y ?? 80) / 100) * h;
  const lineHeight = fontPx * 1.05;
  const lineWidths = lines.map((toks) =>
    toks.reduce((acc, tok, j) => acc + ctx.measureText((j === 0 ? "" : " ") + tok.word).width, 0),
  );
  const lineStartX = (i) => {
    if (align === "center") return anchorX - lineWidths[i] / 2;
    if (align === "right") return anchorX - lineWidths[i];
    return anchorX;
  };
  const firstLineY = anchorY - lineHeight * (lines.length - 1);

  // Light drop shadow only — no stroke. Single fill pass per line.
  ctx.shadowColor = "rgba(0,0,0,0.35)";
  ctx.shadowOffsetY = fontPx * 0.04;
  ctx.shadowBlur = fontPx * 0.08;
  lines.forEach((toks, i) => {
    let x = lineStartX(i);
    const y = firstLineY + i * lineHeight;
    toks.forEach((tok, j) => {
      const piece = (j === 0 ? "" : " ") + tok.word;
      ctx.fillStyle = tok.color;
      ctx.fillText(piece, x, y);
      x += ctx.measureText(piece).width;
    });
  });
}

window.Editor = Editor;
