// The "project" route — /project/new (a freshly opened, unsaved template or
// generated variant, carried via history state) or /project/:id (a saved
// project, fetched by id). Either way, once we have a document, this page
// renders the full canvas editor (top bar, tool rail, canvas, inspector).
function ProjectPage({ id, initialTemplate, mode, authReady, onBack, onSaved }) {
  const [template, setTemplate] = useState(initialTemplate || null);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (initialTemplate) return; // fresh open, or restored via history state on back/forward
    if (!id || id === "new") {
      // No state to recover — e.g. a hard refresh of /project/new. Nothing
      // to render; send them back rather than showing a dead end.
      onBack();
      return;
    }
    // getProject() requires a signed-in session, and on a hard refresh
    // Auth.init() resolves asynchronously alongside this mount — fetching
    // before that finishes would reject with "sign in required" even
    // though the user is actually signed in. Wait for auth to settle.
    if (!authReady) return;
    let alive = true;
    API.getProject(id)
      .then(({ project }) => { if (alive) setTemplate({ ...project.doc, _projectId: project.id }); })
      .catch((e) => { if (alive) setError(e.message || "Couldn't load that project."); });
    return () => { alive = false; };
  }, [id, initialTemplate, authReady]);

  if (error) {
    return (
      <div style={{ minHeight: "100vh", display: "grid", placeItems: "center", background: "var(--bg)", padding: 24 }}>
        <div className="card" style={{ padding: 32, textAlign: "center", maxWidth: 360 }}>
          <div style={{ fontFamily: "var(--font-display)", fontSize: 18, fontWeight: 600, marginBottom: 8 }}>
            Couldn't open this project
          </div>
          <p style={{ color: "var(--ink-3)", fontSize: 13, marginBottom: 18 }}>{error}</p>
          <button className="btn btn-primary" onClick={onBack}>Back</button>
        </div>
      </div>
    );
  }

  if (!template) {
    return <ProjectLoading onBack={onBack} />;
  }

  return <ProjectEditor template={template} mode={mode} onBack={onBack} onSaved={(newId) => onSaved?.(newId)} />;
}

// =============================================================
// ProjectLoading — the editor shell (topbar, tool rail, inspector) with
// a spinner in the canvas area, shown while the project document is
// still being fetched. Mirrors ProjectEditor's layout so there's no
// jump once the real thing mounts.
// =============================================================
function ProjectLoading({ onBack }) {
  return (
    <div className="editor">
      <div className="editor-topbar">
        <div className="editor-topbar-left">
          <button className="editor-back" onClick={onBack}>
            <Icons.ArrowLeft size={14} />
            All templates
          </button>
          <div style={{ width: 1, height: 24, background: "var(--line)" }}></div>
          <div className="skeleton-bar" style={{ width: 160, height: 14 }}></div>
        </div>
      </div>
      <div className="editor-body">
        <div className="editor-tools">
          {[0, 1, 2, 3, 4].map((i) => (
            <div key={i} className="skeleton-bar" style={{ width: 56, height: 44, margin: "2px 0" }}></div>
          ))}
        </div>
        <div className="editor-canvas-area">
          <div className="editor-canvas-wrap">
            <div className="skeleton-bar" style={{ width: 420, height: 525 }}></div>
          </div>
        </div>
        <div className="editor-inspector">
          <div className="inspector-section">
            <div className="skeleton-bar" style={{ width: 80, height: 11, marginBottom: 10 }}></div>
            <div className="skeleton-bar" style={{ width: "100%", height: 34 }}></div>
          </div>
          <div className="inspector-section">
            <div className="skeleton-bar" style={{ width: 60, height: 11, marginBottom: 10 }}></div>
            <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              <div className="skeleton-bar" style={{ width: "100%", height: 44 }}></div>
              <div className="skeleton-bar" style={{ width: "100%", height: 44 }}></div>
            </div>
          </div>
          <div className="inspector-section">
            <div className="skeleton-bar" style={{ width: 50, height: 11, marginBottom: 10 }}></div>
            <div className="skeleton-bar" style={{ width: "100%", height: 34 }}></div>
          </div>
        </div>
      </div>
    </div>
  );
}

// Adds undo/redo on top of useState. Rapid-fire updates (a keystroke, a
// drag frame) within COALESCE_MS of the last one merge into the same
// history entry, so undo steps feel like "one edit" rather than "one
// pixel of movement" or "one typed character".
const HISTORY_COALESCE_MS = 500;
const HISTORY_LIMIT = 100;
function useHistoryState(initial) {
  const [state, setStateRaw] = useState(initial);
  const [past, setPast] = useState([]);
  const [future, setFuture] = useState([]);
  const lastPushRef = useRef(0);

  const setState = useCallback((updater) => {
    setStateRaw((prev) => {
      const next = typeof updater === "function" ? updater(prev) : updater;
      const now = Date.now();
      if (now - lastPushRef.current > HISTORY_COALESCE_MS) {
        setPast((p) => {
          const np = [...p, prev];
          return np.length > HISTORY_LIMIT ? np.slice(np.length - HISTORY_LIMIT) : np;
        });
        setFuture([]);
      }
      lastPushRef.current = now;
      return next;
    });
  }, []);

  const undo = useCallback(() => {
    setPast((p) => {
      if (!p.length) return p;
      const previous = p[p.length - 1];
      setFuture((f) => [...f, state]);
      setStateRaw(previous);
      return p.slice(0, -1);
    });
    lastPushRef.current = 0;
  }, [state]);

  const redo = useCallback(() => {
    setFuture((f) => {
      if (!f.length) return f;
      const next = f[f.length - 1];
      setPast((p) => [...p, state]);
      setStateRaw(next);
      return f.slice(0, -1);
    });
    lastPushRef.current = 0;
  }, [state]);

  return [state, setState, { undo, redo, canUndo: past.length > 0, canRedo: future.length > 0 }];
}

// =============================================================
// ProjectEditor — the canvas-style editor. Loads a template, lets you
// mutate every element.
// =============================================================
function ProjectEditor({ template, mode = "project", onBack, onSaved }) {
  // Deep clone template so we can mutate
  const [doc, setDoc, { undo, redo, canUndo, canRedo }] = useHistoryState(() => JSON.parse(JSON.stringify(template)));
  const [slideIdx, setSlideIdx] = useState(0);
  const [selectedId, setSelectedId] = useState(null);
  const [editingId, setEditingId] = useState(null);
  const [showAssetPicker, setShowAssetPicker] = useState(false);
  const [activeTool, setActiveTool] = useState("select");
  const canvasWrapRef = useRef(null);
  const [scale, setScale] = useState(0.5);

  // Undo/redo can restore a doc with fewer slides than the current index
  // (e.g. undoing an addSlide) — clamp so slide-scoped updates keep
  // targeting a real slide instead of silently no-op'ing.
  useEffect(() => {
    if (slideIdx >= doc.slides.length) setSlideIdx(doc.slides.length - 1);
  }, [doc.slides.length, slideIdx]);

  // Backend wiring: persistence + export
  const [savedId, setSavedId] = useState((mode === "template" ? template.id : template._projectId) || null);
  // Tracks whether `doc` has changed since the last successful save, so
  // the topbar button can read "Save" (or "Save template") while there
  // are pending edits and "Saved" only once they're actually persisted —
  // previously it just checked `savedId`, which stayed "Saved" forever
  // after the first save even with unsaved edits piling up.
  const lastSavedRef = useRef(JSON.stringify(doc));
  const dirty = JSON.stringify(doc) !== lastSavedRef.current;
  const [saving, setSaving] = useState(false);
  const [exporting, setExporting] = useState(false);
  const [toast, setToast] = useState(null);
  const showToast = (msg) => {
    setToast(msg);
    window.clearTimeout(showToast._t);
    showToast._t = window.setTimeout(() => setToast(null), 2600);
  };

  const handleSave = async () => {
    setSaving(true);
    try {
      if (mode === "template") {
        const payload = {
          name: doc.name,
          platform: doc.platform,
          icon: doc.icon,
          width: doc.width,
          height: doc.height,
          preview: doc.preview,
          description: doc.description,
          slides: doc.slides,
        };
        if (savedId) {
          await API.updateTemplate(savedId, payload);
          showToast("Template updated");
        } else {
          const { template: saved } = await API.createTemplate(payload);
          setSavedId(saved.id);
          showToast("Template created");
        }
        lastSavedRef.current = JSON.stringify(doc);
        return;
      }
      const payload = {
        name: doc.name,
        category: doc.category,
        platform: doc.platform,
        doc,
        source: String(doc.id || "").startsWith("gen-") ? "generate" : "template",
      };
      if (savedId) {
        await API.updateProject(savedId, payload);
        showToast("Saved");
      } else {
        const { project } = await API.createProject(payload);
        setSavedId(project.id);
        onSaved?.(project.id);
        showToast("Saved to your projects");
      }
      lastSavedRef.current = JSON.stringify(doc);
    } catch (e) {
      showToast(`Save failed: ${e.message}`);
    } finally {
      setSaving(false);
    }
  };

  const [exportOpen, setExportOpen] = useState(false);
  const safeName = (n) => (n || "asset").replace(/[^a-z0-9-_]+/gi, "-").slice(0, 60) || "asset";
  const downloadBlob = (blob, filename) => {
    const fileUrl = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = fileUrl;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(fileUrl); }, 100);
  };

  // Export the current slide in a given format (png | jpeg).
  const handleExport = async (format = "png") => {
    setExportOpen(false);
    setExporting(true);
    try {
      const blob = await API.exportImage({ doc, slideIndex: slideIdx, format, filename: doc.name });
      const ext = format === "jpeg" ? "jpg" : "png";
      downloadBlob(blob, `${safeName(doc.name)}-${slideIdx + 1}.${ext}`);
      showToast(`Exported ${ext.toUpperCase()}`);
    } catch (e) {
      showToast(`Export failed: ${e.message}`);
    } finally {
      setExporting(false);
    }
  };

  // Export every slide bundled as a ZIP.
  const handleExportZip = async (format = "png") => {
    setExportOpen(false);
    setExporting(true);
    try {
      const blob = await API.exportZip({ doc, format, filename: doc.name });
      downloadBlob(blob, `${safeName(doc.name)}.zip`);
      showToast(`Exported ${doc.slides.length}-slide ZIP`);
    } catch (e) {
      showToast(`Export failed: ${e.message}`);
    } finally {
      setExporting(false);
    }
  };

  // Export all slides as a multi-page PDF.
  const handleExportPdf = async () => {
    setExportOpen(false);
    setExporting(true);
    try {
      const blob = await API.exportPdf({ doc, filename: doc.name });
      downloadBlob(blob, `${safeName(doc.name)}.pdf`);
      showToast("Exported PDF");
    } catch (e) {
      showToast(`Export failed: ${e.message}`);
    } finally {
      setExporting(false);
    }
  };

  // Undo/redo can restore a doc with fewer slides than the current index
  // (e.g. undoing an addSlide) — fall back to the last slide rather than
  // crashing on an out-of-range lookup.
  const slide = doc.slides[slideIdx] || doc.slides[doc.slides.length - 1];
  const selected = slide.elements.find((el) => el.id === selectedId);

  // Clear selection on a true background click — but ignore the synthetic
  // click that fires right after a drag/resize (which can land on the
  // background), so manipulating an element never deselects it.
  const clearSelection = useCallback(() => {
    if (Date.now() - (window.__canvasInteractedAt || 0) < 250) return;
    setSelectedId(null);
    setEditingId(null);
  }, []);

  // Replace an image element's source by uploading a new file.
  const handleReplaceImage = async (id, file) => {
    try {
      const { uploads } = await API.upload([file]);
      const url = uploads && uploads[0] && uploads[0].url;
      if (url) updateElement(id, { src: url, tint: null, mask: null, repeat: false, fit: "cover" });
      showToast("Image replaced");
    } catch (e) {
      showToast(`Upload failed: ${e.message}`);
    }
  };

  // Auto-fit scale
  useEffect(() => {
    const calc = () => {
      const wrap = canvasWrapRef.current;
      if (!wrap) return;
      const padding = 64;
      const availW = wrap.clientWidth - padding;
      const availH = wrap.clientHeight - padding;
      const s = Math.min(availW / doc.width, availH / doc.height, 1);
      setScale(Math.max(s, 0.1));
    };
    calc();
    const ro = new ResizeObserver(calc);
    if (canvasWrapRef.current) ro.observe(canvasWrapRef.current);
    window.addEventListener("resize", calc);
    return () => { ro.disconnect(); window.removeEventListener("resize", calc); };
  }, [doc.width, doc.height]);

  // Mutate the currently selected element
  const updateSelected = useCallback((patch) => {
    if (!selectedId) return;
    setDoc((d) => {
      const next = { ...d, slides: d.slides.map((s, i) => i === slideIdx ? {
        ...s,
        elements: s.elements.map((el) => el.id === selectedId ? { ...el, ...patch } : el),
      } : s) };
      return next;
    });
  }, [selectedId, slideIdx]);

  const updateElement = useCallback((id, patch) => {
    setDoc((d) => ({
      ...d,
      slides: d.slides.map((s, i) => i === slideIdx ? {
        ...s,
        elements: s.elements.map((el) => el.id === id ? { ...el, ...patch } : el),
      } : s),
    }));
  }, [slideIdx]);

  const updateSlide = useCallback((patch) => {
    setDoc((d) => ({
      ...d,
      slides: d.slides.map((s, i) => i === slideIdx ? { ...s, ...patch } : s),
    }));
  }, [slideIdx]);

  const deleteSelected = useCallback(() => {
    if (!selectedId) return;
    setDoc((d) => ({
      ...d,
      slides: d.slides.map((s, i) => i === slideIdx ? {
        ...s,
        elements: s.elements.filter((el) => el.id !== selectedId),
      } : s),
    }));
    setSelectedId(null);
  }, [selectedId, slideIdx]);

  // Reorder the selected element in the stack: "front" | "forward" |
  // "backward" | "back". Later in the array = painted on top.
  const reorderSelected = useCallback((dir) => {
    if (!selectedId) return;
    setDoc((d) => ({
      ...d,
      slides: d.slides.map((s, i) => {
        if (i !== slideIdx) return s;
        const els = [...s.elements];
        const idx = els.findIndex((e) => e.id === selectedId);
        if (idx < 0) return s;
        const [el] = els.splice(idx, 1);
        const ni =
          dir === "front" ? els.length :
          dir === "back" ? 0 :
          dir === "forward" ? Math.min(els.length, idx + 1) :
          Math.max(0, idx - 1);
        els.splice(ni, 0, el);
        return { ...s, elements: els };
      }),
    }));
  }, [selectedId, slideIdx]);

  const duplicateSelected = useCallback(() => {
    if (!selected) return;
    const copy = { ...selected, id: `${selected.id}-copy-${Date.now()}`, x: selected.x + 30, y: selected.y + 30 };
    setDoc((d) => ({
      ...d,
      slides: d.slides.map((s, i) => i === slideIdx ? {
        ...s, elements: [...s.elements, copy],
      } : s),
    }));
    setSelectedId(copy.id);
  }, [selected, slideIdx]);

  const addElement = useCallback((kind) => {
    const id = `new-${Date.now()}`;
    let el;
    if (kind === "text") {
      el = { id, type: "text", x: 100, y: 100, w: 600, h: 100,
        content: "Double-click to edit", fontFamily: "Space Grotesk",
        fontSize: 64, fontWeight: 700, color: "#070707", textAlign: "left",
        lineHeight: 1.0, letterSpacing: -0.02, italic: false, rotation: 0 };
    } else if (kind === "shape") {
      el = { id, type: "shape", x: 100, y: 100, w: 200, h: 200, shape: "rect", fill: "#070707", borderRadius: 0, rotation: 0 };
    } else if (kind === "image") {
      el = { id, type: "image", x: 100, y: 100, w: 400, h: 400, src: "assets/brand-laptop-mockup.png", fit: "cover", borderRadius: 0, rotation: 0 };
    } else if (kind === "sticker") {
      el = { id, type: "sticker", x: 100, y: 100, w: 200, h: 60, content: "NEW", fill: "#070707", color: "#FFFFFF", rotation: 0, fontSize: 24, fontFamily: "Space Grotesk", fontWeight: 700 };
    }
    setDoc((d) => ({
      ...d,
      slides: d.slides.map((s, i) => i === slideIdx ? { ...s, elements: [...s.elements, el] } : s),
    }));
    setSelectedId(id);
  }, [slideIdx]);

  // Insert a fully-specified element (used by the Elements panel), then
  // return to the select tool with the new element selected.
  const insertElement = useCallback((partial) => {
    const id = `new-${Date.now()}`;
    const el = { id, x: 140, y: 160, rotation: 0, ...partial };
    setDoc((d) => ({
      ...d,
      slides: d.slides.map((s, i) => i === slideIdx ? { ...s, elements: [...s.elements, el] } : s),
    }));
    setSelectedId(id);
    setActiveTool("select");
  }, [slideIdx]);

  // Keyboard: delete, escape
  useEffect(() => {
    const onKey = (e) => {
      if (editingId) return; // text-editing mode
      if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
      if ((e.key === "Delete" || e.key === "Backspace") && selectedId) {
        e.preventDefault();
        deleteSelected();
      } else if (e.key === "Escape") {
        setSelectedId(null);
      } else if ((e.metaKey || e.ctrlKey) && e.key === "d" && selectedId) {
        e.preventDefault();
        duplicateSelected();
      } else if ((e.metaKey || e.ctrlKey) && e.key === "]" && selectedId) {
        e.preventDefault();
        reorderSelected(e.shiftKey ? "front" : "forward");
      } else if ((e.metaKey || e.ctrlKey) && e.key === "[" && selectedId) {
        e.preventDefault();
        reorderSelected(e.shiftKey ? "back" : "backward");
      } else if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z") {
        e.preventDefault();
        undo();
      } else if ((e.metaKey || e.ctrlKey) && (e.key.toLowerCase() === "y" || (e.shiftKey && e.key.toLowerCase() === "z"))) {
        e.preventDefault();
        redo();
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [selectedId, editingId, deleteSelected, duplicateSelected, reorderSelected, undo, redo]);

  const addSlide = useCallback(() => {
    const newSlide = { background: "#FFFFFF", elements: [] };
    setDoc((d) => ({ ...d, slides: [...d.slides, newSlide] }));
    setSlideIdx(doc.slides.length);
  }, [doc.slides.length]);

  const duplicateSlide = useCallback(() => {
    const copy = JSON.parse(JSON.stringify(doc.slides[slideIdx]));
    copy.elements = copy.elements.map((el) => ({ ...el, id: `${el.id}-dup-${Date.now()}` }));
    setDoc((d) => {
      const next = [...d.slides];
      next.splice(slideIdx + 1, 0, copy);
      return { ...d, slides: next };
    });
    setSlideIdx(slideIdx + 1);
  }, [doc.slides, slideIdx]);

  const deleteSlide = useCallback(() => {
    if (doc.slides.length <= 1) return;
    setDoc((d) => ({ ...d, slides: d.slides.filter((_, i) => i !== slideIdx) }));
    setSlideIdx(Math.max(0, slideIdx - 1));
  }, [doc.slides.length, slideIdx]);

  return (
    <div className="editor">
      <ProjectTopbar
        doc={doc}
        setDoc={setDoc}
        onBack={onBack}
        scale={scale}
        toast={toast}
        saving={saving}
        isSaved={Boolean(savedId) && !dirty}
        saveLabel={mode === "template" ? "Save template" : "Save"}
        onSave={handleSave}
        onUndo={undo}
        onRedo={redo}
        canUndo={canUndo}
        canRedo={canRedo}
        exporting={exporting}
        exportOpen={exportOpen}
        setExportOpen={setExportOpen}
        onExport={handleExport}
        onExportPdf={handleExportPdf}
        onExportZip={handleExportZip}
      />

      {/* BODY */}
      <div className="editor-body">
        <ProjectSidebar
          activeTool={activeTool}
          setActiveTool={setActiveTool}
          onAddText={() => addElement("text")}
          onAddImage={() => addElement("image")}
          onOpenElements={() => { setActiveTool("elements"); setSelectedId(null); setEditingId(null); }}
        />

        {/* CANVAS AREA */}
        <div className="editor-canvas-area">
          <div className="editor-canvas-wrap" ref={canvasWrapRef} onClick={clearSelection}>
            <div className="editor-canvas" style={{ width: doc.width * scale, height: doc.height * scale }}>
              <SlideRenderer
                slide={slide}
                width={doc.width}
                height={doc.height}
                scale={scale}
                editable={true}
                selectedId={selectedId}
                editingId={editingId}
                onSelect={(id) => { setSelectedId(id); setEditingId(null); setActiveTool("select"); }}
                onStartEdit={(id) => { setSelectedId(id); setEditingId(id); setActiveTool("select"); }}
                onChange={updateElement}
                onClickBackground={clearSelection}
                onReplaceImage={handleReplaceImage}
              />
            </div>
          </div>

          <ProjectSlidesStrip
            doc={doc}
            slideIdx={slideIdx}
            onSelectSlide={(i) => { setSlideIdx(i); setSelectedId(null); setEditingId(null); }}
            onAddSlide={addSlide}
            onDuplicateSlide={duplicateSlide}
            onDeleteSlide={deleteSlide}
          />
        </div>

        <ProjectRightPanel
          activeTool={activeTool}
          setActiveTool={setActiveTool}
          selected={selected}
          slide={slide}
          insertElement={insertElement}
          updateSelected={updateSelected}
          updateSlide={updateSlide}
          deleteSelected={deleteSelected}
          duplicateSelected={duplicateSelected}
          reorderSelected={reorderSelected}
          onPickAsset={() => setShowAssetPicker(true)}
        />
      </div>

      {/* ASSET PICKER MODAL */}
      {showAssetPicker && selected?.type === "image" && (
        <AssetPickerModal
          onPick={(p) => {
            // Reset per-image treatments (mask/tint/tiling) from the old asset
            updateSelected({ src: p.id, tint: null, mask: null, repeat: false, fit: p.fit || "cover" });
            setShowAssetPicker(false);
          }}
          onUpload={async (file) => {
            await handleReplaceImage(selected.id, file);
            setShowAssetPicker(false);
          }}
          onClose={() => setShowAssetPicker(false)}
        />
      )}
    </div>
  );
}
