(() => {
  const { useState, useEffect, useRef } = React;
  const { Icon } = window.HiraIcons;
  const { TagPicker, ContentPicker, LibraryPicker } = window.HiraPickers;
  const HiraFormat = window.HiraFormat;
  const HiraApi = window.HiraApi;
  const HiraValidation = window.HiraValidation;
  const HiraRuntime = window.HiraRuntime;
  const { SHARE_EXPIRY_CHOICES } = window.HiraConstants;
  const handleAuthFail = (message) => HiraRuntime.handleAuthFail(message);
  const getImageContent = window.HiraFilters.getImageContent;

// ── Resize util ───────────────────────────────────────────────────────────────
async function resizeToTarget(file, targetLongSide = 2048) {
  return new Promise((resolve) => {
    const img = new Image();
    const url = URL.createObjectURL(file);
    img.onload = () => {
      const scale = targetLongSide / Math.max(img.width, img.height);
      if (scale <= 1) { URL.revokeObjectURL(url); resolve(file); return; }
      const canvas = document.createElement("canvas");
      canvas.width = Math.round(img.width * scale);
      canvas.height = Math.round(img.height * scale);
      const ctx = canvas.getContext("2d");
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = "high";
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      canvas.toBlob((blob) => { URL.revokeObjectURL(url); resolve(new File([blob], file.name, { type: "image/jpeg" })); }, "image/jpeg", 0.92);
    };
    img.onerror = () => resolve(file);
    img.src = url;
  });
}

// ── UploadModal ───────────────────────────────────────────────────────────────
// ── UploadModal (P1 improved) ───────────────────────────────────────────────
function UploadModal({ T, libraries, allTags, onClose, onUploaded }) {
  const [mode, setMode] = useState("file");
  const [files, setFiles] = useState([]);
  const [previewUrl, setPreviewUrl] = useState(null);
  const [urlText, setUrlText] = useState("");
  const [upscale, setUpscale] = useState(false);
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [selectedTags, setSelectedTags] = useState([]);
  const [contentTags, setContentTags] = useState([]);
  const [libraryId, setLibraryId] = useState("");
  const [newLibName, setNewLibName] = useState("");
  const [creatingNew, setCreatingNew] = useState(false);
  const [uploading, setUploading] = useState(false);
  const [progress, setProgress] = useState({ done: 0, total: 0, current: "" });
  const [queue, setQueue] = useState([]); // {name, status: waiting|uploading|done|duplicate|failed, error, existingTitle}
  const [error, setError] = useState("");
  const [duplicateInfo, setDuplicateInfo] = useState(null);
  const [finished, setFinished] = useState(false);

  const cleanName = (filename) => filename.replace(/\.[^/.]+$/, "").replace(/[-_]/g, " ");
  const nameFromUrl = (url) => {
    try { return cleanName(decodeURIComponent(new URL(url).pathname.split("/").pop() || "gambar")); }
    catch (e) { return "gambar"; }
  };

  const buildInitialQueue = (fileList, urlList) => {
    if (fileList && fileList.length) return fileList.map(f => ({ name: f.name, status: "waiting", error: "", existingTitle: "" }));
    if (urlList && urlList.length) return urlList.map(u => ({ name: u, status: "waiting", error: "", existingTitle: "" }));
    return [];
  };

  const handleFileChange = (e) => {
    const picked = Array.from(e.target.files);
    setFiles(picked);
    setQueue(buildInitialQueue(picked, null));
    setFinished(false);
    setDuplicateInfo(null);
    setError("");
    if (picked.length === 1) {
      if (previewUrl) URL.revokeObjectURL(previewUrl);
      setPreviewUrl(URL.createObjectURL(picked[0]));
    } else {
      if (previewUrl) URL.revokeObjectURL(previewUrl);
      setPreviewUrl(null);
    }
  };

  const removeFile = () => {
    setFiles([]);
    setQueue([]);
    if (previewUrl) URL.revokeObjectURL(previewUrl);
    setPreviewUrl(null);
    setDuplicateInfo(null);
    setFinished(false);
  };

  const handleUrlChange = (e) => {
    const txt = e.target.value;
    setUrlText(txt);
    const list = txt.split("\n").map(u=>u.trim()).filter(Boolean);
    setQueue(buildInitialQueue(null, list));
    setFinished(false);
    setDuplicateInfo(null);
    setError("");
  };

  const updateQueueItem = (idx, patch) => {
    setQueue(prev => {
      const n = [...prev];
      if (n[idx]) n[idx] = { ...n[idx], ...patch };
      return n;
    });
  };

  const submit = async (force = false, resumeFrom = null) => {
    const urlList = mode === "url" ? urlText.split("\n").map((u) => u.trim()).filter(Boolean) : [];
    if (mode === "file" && files.length === 0) { setError("Pilih minimal satu gambar dulu."); return; }
    if (mode === "url" && urlList.length === 0) { setError("Isi minimal satu link gambar dulu."); return; }

    const titleValidation = HiraValidation.validateImageTitle(title);
    if (!titleValidation.valid) { setError(titleValidation.message); return; }
    const tagValidation = HiraValidation.validateTagList(selectedTags);
    if (!tagValidation.valid) { setError(tagValidation.message); return; }
    const contentValidation = HiraValidation.validateContentList(contentTags);
    if (!contentValidation.valid) { setError(contentValidation.message); return; }
    if (creatingNew) {
      const libraryValidation = HiraValidation.validateLibraryName(newLibName || `Koleksi ${libraries.length + 1}`);
      if (!libraryValidation.valid) { setError(libraryValidation.message); return; }
    }

    const normalizedTags = tagValidation.value;
    const normalizedContent = contentValidation.value;
    const startIdx = resumeFrom != null ? resumeFrom : 0;
    const total = mode === "file" ? files.length : urlList.length;
    const initial = resumeFrom == null ? buildInitialQueue(mode==="file"?files:null, mode==="url"?urlList:null) : queue;
    if (resumeFrom == null) {
      setQueue(initial);
    }
    setUploading(true);
    setError("");
    setDuplicateInfo(null);
    setFinished(false);
    setProgress({ done: startIdx, total, current: initial[startIdx]?.name || "" });

    try {
      let targetLibId = libraryId;
      if (creatingNew) {
        const data = await HiraApi.createLibrary(newLibName || `Koleksi ${libraries.length + 1}`);
        targetLibId = data.library.id;
      }

      const failed = [];

      if (mode === "file") {
        for (let i = startIdx; i < files.length; i++) {
          let f = files[i];
          updateQueueItem(i, { status: "uploading", error: "" });
          setProgress({ done: i, total: files.length, current: f.name });
          try {
            if (upscale) f = await resizeToTarget(f);
            const fd = new FormData();
            const t = files.length > 1 ? (title ? `${title} ${i + 1}` : cleanName(f.name)) : (title || cleanName(f.name));
            fd.append("file", f);
            fd.append("title", t);
            fd.append("description", description);
            fd.append("tags", normalizedTags.join(","));
            fd.append("content", JSON.stringify(normalizedContent));
            fd.append("libraryId", targetLibId);
            if (force) fd.append("force", "1");
            await HiraApi.uploadFormData(fd);
            updateQueueItem(i, { status: "done" });
          } catch (e) {
            if (e.status === 401) { handleAuthFail(); return; }
            if (e.status === 409 && e.data && e.data.duplicate) {
              updateQueueItem(i, { status: "duplicate", existingTitle: e.data.existingTitle || "duplikat" });
              setProgress({ done: i, total: files.length, current: f.name });
              setUploading(false);
              setDuplicateInfo({ type: "file", index: i, title: f.name, existingTitle: e.data.existingTitle, existingId: e.data.existingId, resumeAt: i, total: files.length });
              return;
            }
            const msg = e.message || "Upload gagal";
            updateQueueItem(i, { status: "failed", error: msg });
            failed.push(`${f.name}: ${msg}`);
          }
          setProgress({ done: i + 1, total: files.length, current: i+1<files.length ? files[i+1]?.name || "" : "" });
        }
      } else {
        for (let i = startIdx; i < urlList.length; i++) {
          const u = urlList[i];
          updateQueueItem(i, { status: "uploading", error: "" });
          setProgress({ done: i, total: urlList.length, current: u });
          try {
            const t = urlList.length > 1 ? (title ? `${title} ${i + 1}` : nameFromUrl(u)) : (title || nameFromUrl(u));
            const body = { url: u, title: t, description, tags: normalizedTags, libraryId: targetLibId, content: normalizedContent };
            if (force) body.force = true;
            await HiraApi.uploadFromUrl(body);
            updateQueueItem(i, { status: "done" });
          } catch (e) {
            if (e.status === 401) { handleAuthFail(); return; }
            if (e.status === 409 && e.data && e.data.duplicate) {
              updateQueueItem(i, { status: "duplicate", existingTitle: e.data.existingTitle || "duplikat" });
              setProgress({ done: i, total: urlList.length, current: u });
              setUploading(false);
              setDuplicateInfo({ type: "url", index: i, title: u, existingTitle: e.data.existingTitle, existingId: e.data.existingId, resumeAt: i, url: u, total: urlList.length });
              return;
            }
            const msg = e.message || "Gagal ambil gambar";
            updateQueueItem(i, { status: "failed", error: msg });
            failed.push(`${u}: ${msg}`);
          }
          setProgress({ done: i + 1, total: urlList.length, current: i+1<urlList.length ? urlList[i+1] : "" });
        }
      }

      setFinished(true);
      onUploaded();
      if (failed.length > 0) {
        setError(`${failed.length} gagal diunggah:\n${failed.join("\n")}`);
      } else if (duplicateInfo) {
        // keep open
      } else {
        // auto close when all done and no failures
        const hasFailed = queue.filter(q=>q.status==="failed").length>0 || failed.length>0;
        if (!hasFailed) onClose();
      }
    } catch (err) {
      if (err.status === 401) handleAuthFail();
      else setError(err.message);
    } finally {
      setUploading(false);
    }
  };

  const handleSkipDuplicate = () => {
    if (!duplicateInfo) return;
    const idx = duplicateInfo.index;
    updateQueueItem(idx, { status: "failed", error: "Dilewati (duplikat)" });
    setDuplicateInfo(null);
    submit(false, idx + 1);
  };

  const handleForceDuplicate = async () => {
    if (!duplicateInfo) return;
    const idx = duplicateInfo.index;
    const normalizedTags = HiraValidation.validateTagList(selectedTags).value;
    const normalizedContent = HiraValidation.validateContentList(contentTags).value;
    setDuplicateInfo(null);
    setUploading(true);
    updateQueueItem(idx, { status: "uploading", error: "" });
    try {
      const targetLibId = libraryId;
      if (mode === "file") {
        let f = files[idx];
        if (upscale) f = await resizeToTarget(f);
        const fd = new FormData();
        const t = files.length > 1 ? (title ? `${title} ${idx + 1}` : cleanName(f.name)) : (title || cleanName(f.name));
        fd.append("file", f);
        fd.append("title", t);
        fd.append("description", description);
        fd.append("tags", normalizedTags.join(","));
        fd.append("content", JSON.stringify(normalizedContent));
        fd.append("libraryId", targetLibId);
        fd.append("force", "1");
        await HiraApi.uploadFormData(fd);
        updateQueueItem(idx, { status: "done" });
      } else {
        const urlList = urlText.split("\n").map((u) => u.trim()).filter(Boolean);
        const u = urlList[idx];
        const t = urlList.length > 1 ? (title ? `${title} ${idx + 1}` : nameFromUrl(u)) : (title || nameFromUrl(u));
        await HiraApi.uploadFromUrl({ url: u, title: t, description, tags: normalizedTags, libraryId: targetLibId, content: normalizedContent, force: true });
        updateQueueItem(idx, { status: "done" });
      }
    } catch (error) {
      if (error.status === 401) {
        handleAuthFail();
        return;
      }
      updateQueueItem(idx, { status: "failed", error: error.message });
    }
    submit(false, idx + 1);
  };

  const failedItems = queue.filter(q => q.status === "failed");
  const doneCount = queue.filter(q => q.status === "done").length;

  return (
    <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 flex items-center justify-center px-4 overlay-enter" onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} className={`border rounded-2xl w-full max-w-lg p-6 relative max-h-[92vh] overflow-y-auto modal-enter ${T.modalBg}`} role="dialog" aria-modal="true">
        <button onClick={onClose} aria-label="Tutup modal" className={`absolute top-4 right-4 ${T.sub}`}><Icon.X /></button>
        <h2 className="font-display text-xl mb-1">Unggah gambar</h2>
        <p className={`text-xs ${T.sub} mb-3`}>Bisa lebih dari satu sekaligus. Otomatis tersimpan ke akun Drive dengan ruang tersisa.</p>

        <div className={`flex rounded-full border p-0.5 mb-4 text-xs ${T.chipInactive}`}>
          <button onClick={() => { setMode("file"); setFinished(false); }} className={`flex-1 py-1.5 rounded-full ${mode === "file" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Dari HP</button>
          <button onClick={() => { setMode("url"); setFinished(false); }} className={`flex-1 py-1.5 rounded-full ${mode === "url" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Dari link</button>
        </div>

        {error && (
          <div className="mb-3">
            <p className="text-rose-400 text-xs mb-2 whitespace-pre-line">{error}</p>
            {failedItems.length>0 && (
              <button onClick={()=>HiraFormat.copyText(failedItems.map(f=>`${f.name}: ${f.error}`).join("\n")).then(()=>alert("Error disalin"))} className={`text-xs px-3 py-1 rounded-full border ${T.chipInactive}`}>Salin error</button>
            )}
          </div>
        )}

        {mode === "file" ? (
          files.length === 0 ? (
            <label className={`flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-xl mb-3 cursor-pointer ${T.chipInactive} hover:border-amber-400/50 transition-colors`} onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add("border-amber-400", "bg-amber-400/10"); }} onDragLeave={(e) => { e.currentTarget.classList.remove("border-amber-400", "bg-amber-400/10"); }} onDrop={(e) => { e.preventDefault(); e.currentTarget.classList.remove("border-amber-400", "bg-amber-400/10"); const dropped = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith("image/")); if (dropped.length > 0) { setFiles(dropped); setQueue(buildInitialQueue(dropped,null)); if (dropped.length === 1) { if (previewUrl) URL.revokeObjectURL(previewUrl); setPreviewUrl(URL.createObjectURL(dropped[0])); } else { if (previewUrl) URL.revokeObjectURL(previewUrl); setPreviewUrl(null); } } }}>
              <Icon.Image className={T.faint} />
              <span className={`text-xs mt-2 ${T.sub}`}>Klik untuk pilih gambar</span>
              <input type="file" accept="image/*" multiple onChange={handleFileChange} className="hidden" />
            </label>
          ) : (
            <div className="mb-3">
              {previewUrl ? (
                <div className="relative rounded-xl overflow-hidden mb-2">
                  <img src={previewUrl} className="w-full max-h-48 object-cover rounded-xl" />
                  <button onClick={removeFile} className="absolute top-2 right-2 bg-black/60 text-white rounded-full p-1.5 backdrop-blur"><Icon.X /></button>
                </div>
              ) : (
                <div className={`flex items-center justify-between px-3 py-2.5 rounded-xl border mb-2 ${T.panel}`}>
                  <span className={`text-xs ${T.sub}`}>{files.length} gambar dipilih</span>
                  <button onClick={removeFile} className="text-rose-400 text-xs flex items-center gap-1"><Icon.Trash />Hapus</button>
                </div>
              )}
              <label className={`text-xs text-amber-400 cursor-pointer underline underline-offset-2`}>
                Ganti gambar
                <input type="file" accept="image/*" multiple onChange={handleFileChange} className="hidden" />
              </label>
            </div>
          )
        ) : (
          <>
            <textarea value={urlText} onChange={handleUrlChange}
              placeholder={"Tempel link gambar, satu per baris\nhttps://contoh.com/gambar1.jpg\nhttps://contoh.com/gambar2.jpg"}
              rows={4} className={`w-full border rounded-lg px-3 py-2 text-xs mb-1 ${T.inputBg}`} />
            <p className={`text-xs ${T.sub} mb-3`}>{urlText.split("\n").filter((u) => u.trim()).length} link gambar</p>
          </>
        )}

        <input value={title} onChange={(e) => setTitle(e.target.value)}
          placeholder="Judul — nomor otomatis ditambahkan (#1, #2, dst.)"
          className={`w-full border rounded-lg px-3 py-2 text-sm mb-2 ${T.inputBg}`} />
        <textarea value={description} onChange={(e) => setDescription(e.target.value)}
          placeholder="Deskripsi (opsional)" rows={2}
          className={`w-full border rounded-lg px-3 py-2 text-sm mb-2 ${T.inputBg}`} />

        <p className={`text-xs ${T.sub} mb-1.5 flex items-center gap-1`}><Icon.Tag />Tag</p>
        <TagPicker selectedTags={selectedTags} onChange={setSelectedTags} allTags={allTags} T={T} />
        <p className={`text-xs ${T.sub} mb-1.5 mt-2 flex items-center gap-1`}><Icon.Filter />Content</p>
        <ContentPicker selectedContent={contentTags} onChange={setContentTags} T={T} />

        {mode === "file" && (
          <label className="flex items-start gap-2 mb-3 cursor-pointer">
            <input type="checkbox" checked={upscale} onChange={(e) => setUpscale(e.target.checked)} className="mt-0.5" />
            <span className={`text-xs ${T.sub}`}>
              Resize sisi panjang ke ~2048px jika lebih kecil (bukan AI upscale — kualitas tidak ditambah)
            </span>
          </label>
        )}

        <p className={`text-xs ${T.sub} mb-1.5`}>Koleksi (opsional)</p>
        <LibraryPicker
          value={creatingNew ? "" : libraryId}
          newLibName={creatingNew ? newLibName : ""}
          libraries={libraries}
          onSelect={(id) => { setLibraryId(id); setCreatingNew(false); setNewLibName(""); }}
          onCreateNew={(name) => { setCreatingNew(true); setNewLibName(name); setLibraryId(""); }}
          onClearNew={() => { setCreatingNew(false); setNewLibName(""); }}
          T={T}
        />

        {(uploading || queue.length>0) && (
          <div className="mb-3 mt-2">
            <div className={`h-1.5 rounded-full ${T.panel} overflow-hidden`}>
              <div className="h-full bg-amber-400 transition-all" style={{ width: `${progress.total? (progress.done / progress.total) * 100 : 0}%` }} />
            </div>
            <p className={`text-xs ${T.sub} mt-1`}>
              {uploading ? `Mengunggah ${progress.done+1}/${progress.total}: ${progress.current}` : finished ? `Selesai: ${doneCount}/${progress.total} berhasil, ${failedItems.length} gagal` : `${progress.done}/${progress.total} — ${queue.length} file siap`}
            </p>
          </div>
        )}

        {queue.length>0 && (
          <div className={`mb-3 rounded-xl border max-h-44 overflow-y-auto ${T.panel}`}>
            {queue.map((item, idx) => (
              <div key={idx} className={`flex items-center gap-2 px-3 py-2 text-xs border-b last:border-b-0 ${T.railBg} ${item.status==="failed"? "bg-rose-500/5" : item.status==="done"? "bg-emerald-500/5" : ""}`}>
                <span className={`w-5 h-5 rounded-full flex items-center justify-center text-[10px] shrink-0 ${item.status==="done"?"bg-emerald-500 text-white":item.status==="failed"?"bg-rose-500 text-white":item.status==="uploading"?"bg-amber-400 text-[#0a0c16] animate-pulse":item.status==="duplicate"?"bg-amber-400/30 text-amber-400 border border-amber-400/40":"bg-white/10 text-stone-400"}`}>
                  {item.status==="done"?"✓":item.status==="failed"?"!":item.status==="uploading"?"…":item.status==="duplicate"?"=":"•"}
                </span>
                <span className="flex-1 truncate" title={item.name}>{item.name}</span>
                <span className={`shrink-0 text-[10px] px-1.5 py-0.5 rounded-full ${item.status==="done"?"bg-emerald-500/20 text-emerald-400":item.status==="failed"?"bg-rose-500/20 text-rose-400":item.status==="uploading"?"bg-amber-400/20 text-amber-400":item.status==="duplicate"?"bg-amber-400/20 text-amber-400":"bg-white/5 text-stone-500"}`}>{item.status}</span>
              </div>
            ))}
          </div>
        )}

        {failedItems.length>0 && finished && (
          <div className="mb-3 rounded-xl border border-rose-400/20 bg-rose-500/5 p-3">
            <p className="text-xs text-rose-400 font-medium mb-1">{failedItems.length} gagal</p>
            <ul className="text-[11px] text-stone-400 space-y-1 max-h-24 overflow-y-auto">
              {failedItems.map((f,i)=><li key={i} className="truncate">{f.name}: {f.error}</li>)}
            </ul>
          </div>
        )}

        {duplicateInfo && (
          <div className="mb-3 rounded-xl border border-amber-400/30 bg-amber-400/10 p-3">
            <p className="text-xs text-amber-400 font-medium mb-1">⚠️ Konten duplikat terdeteksi</p>
            <p className={`text-xs ${T.sub} mb-2`}>
              "{duplicateInfo.title}" mirip dengan "<span className="text-amber-400">{duplicateInfo.existingTitle}</span>" yang sudah ada.
            </p>
            <div className="flex gap-2">
              <button onClick={handleSkipDuplicate} className={`text-xs px-3 py-1.5 rounded-full border ${T.chipInactive}`}>Lewati</button>
              <button onClick={handleForceDuplicate} className="text-xs bg-amber-400 text-[#0a0c16] px-3 py-1.5 rounded-full font-medium">Upload anyway</button>
            </div>
          </div>
        )}

        <button onClick={() => submit(false)} disabled={uploading || !!duplicateInfo} className="w-full mt-1 bg-amber-400 hover:bg-amber-300 text-[#0a0c16] font-medium text-sm py-2.5 rounded-full disabled:opacity-50">
          {uploading ? `Mengunggah... ${progress.done+1}/${progress.total}` : duplicateInfo ? "Menunggu aksi duplikat..." : mode === "file"
            ? (files.length > 1 ? `Unggah ${files.length} gambar` : "Simpan gambar")
            : (urlText.split("\n").filter((u) => u.trim()).length > 1 ? `Ambil ${urlText.split("\n").filter((u) => u.trim()).length} gambar` : "Ambil gambar dari link")}
        </button>
      </div>
    </div>
  );
}

// ── DevAuthModal ──────────────────────────────────────────────────────────────
function DevAuthModal({ T, onSubmit, onClose, turnstileSiteKey }) {
  const [pwd, setPwd] = useState("");
  const [error, setError] = useState("");
  const [checking, setChecking] = useState(false);
  const [turnstileToken, setTurnstileToken] = useState("");
  const turnstileRef = React.useRef(null);

  // Render Turnstile widget after mount (only if site key is configured)
  React.useEffect(() => {
    if (!turnstileSiteKey || !turnstileRef.current) return;
    if (typeof window === "undefined" || !window.turnstile) return;
    try {
      window.turnstile.render(turnstileRef.current, {
        sitekey: turnstileSiteKey,
        callback: (token) => setTurnstileToken(token),
        "expired-callback": () => setTurnstileToken(""),
        "error-callback": () => setTurnstileToken(""),
        theme: "dark",
        size: "compact",
      });
    } catch (_) {}
  }, [turnstileSiteKey]);

  const submit = async () => {
    if (turnstileSiteKey && !turnstileToken) {
      setError("Verifikasi keamanan belum selesai. Tunggu sebentar.");
      return;
    }
    setChecking(true);
    setError("");
    const ok = await onSubmit(pwd, turnstileToken);
    setChecking(false);
    if (!ok) {
      setError("Kata sandi salah.");
      setTurnstileToken("");
      // Reset Turnstile widget
      if (turnstileRef.current && window.turnstile) {
        try { window.turnstile.reset(turnstileRef.current); } catch (_) {}
      }
    }
  };
  return (
    <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center px-4 overlay-enter" onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} className={`border rounded-2xl w-full max-w-xs p-6 modal-enter ${T.modalBg}`}>
        <h3 className="font-display text-lg mb-1">Buka Mode Edit</h3>
        <p className={`text-xs ${T.sub} mb-3`}>Wajib untuk unggah, edit, dan hapus gambar.</p>
        {error && <p className="text-rose-400 text-xs mb-2">{error}</p>}
        <input type="password" value={pwd} onChange={(e) => setPwd(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && submit()}
          placeholder="Kata sandi" className={`w-full border rounded-lg px-3 py-2 text-sm mb-3 ${T.inputBg}`} />
        {turnstileSiteKey && <div ref={turnstileRef} className="mb-3 flex justify-center"></div>}
        <div className="flex gap-2">
          <button onClick={onClose} className={`flex-1 px-3 py-2 rounded-full text-sm border ${T.chipInactive}`}>Batal</button>
          <button onClick={submit} disabled={checking} className="flex-1 bg-amber-400 text-[#0a0c16] text-sm py-2 rounded-full font-medium disabled:opacity-50">
            {checking ? "Memeriksa..." : "Masuk"}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── EditModal ─────────────────────────────────────────────────────────────────
function EditModal({ T, item, allTags, libraries, onClose, onSaved, onToggleSave, onToast }) {
  const baseName = (item.title || "").replace(/ #\d+$/, "");
  const [title, setTitle] = useState(baseName);
  const [description, setDescription] = useState(item.description || "");
  const [selectedTags, setSelectedTags] = useState(item.tags || []);
  const [contentTags, setContentTags] = useState(() => getImageContent(item));
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState("");
  // Koleksi state
  const [editLibId, setEditLibId] = useState(item.libraryId || "");
  const [editSavedTo, setEditSavedTo] = useState(item.savedTo || []);
  const [libCreating, setLibCreating] = useState(false);
  const [libNewName, setLibNewName] = useState("");

  const allCurrentLibIds = [editLibId, ...editSavedTo.filter(id => id !== editLibId)].filter(Boolean);

  const removeFromCollection = (id) => {
    if (id === editLibId) {
      const next = editSavedTo.find(x => x !== id);
      setEditLibId(next || "");
      setEditSavedTo(prev => prev.filter(x => x !== id && x !== next));
    } else {
      setEditSavedTo(prev => prev.filter(x => x !== id));
    }
  };

  const addToCollection = (id) => {
    if (!id || allCurrentLibIds.includes(id)) return;
    if (!editLibId) setEditLibId(id);
    else setEditSavedTo(prev => [...prev, id]);
  };

  const submit = async () => {
    setSaving(true);
    setError("");
    try {
      const titleValidation = HiraValidation.validateImageTitle(title);
      if (!titleValidation.valid) throw new Error(titleValidation.message);
      const tagValidation = HiraValidation.validateTagList(selectedTags);
      if (!tagValidation.valid) throw new Error(tagValidation.message);
      const contentValidation = HiraValidation.validateContentList(contentTags);
      if (!contentValidation.valid) throw new Error(contentValidation.message);

      let finalLibId = editLibId;
      if (libCreating && libNewName.trim()) {
        const libraryValidation = HiraValidation.validateLibraryName(libNewName);
        if (!libraryValidation.valid) throw new Error(libraryValidation.message);
        const ldata = await HiraApi.createLibrary(libraryValidation.value);
        finalLibId = ldata.library?.id || editLibId;
      }

      const data = await HiraApi.updateImage(item.id, {
        title: titleValidation.value || baseName,
        description,
        tags: tagValidation.value,
        libraryId: finalLibId,
        content: contentValidation.value,
      });

      // 3. Handle savedTo diff via toggleSave
      if (onToggleSave) {
        const origSavedTo = item.savedTo || [];
        const finalSavedTo = libCreating && libNewName.trim()
          ? editSavedTo
          : editSavedTo.filter(id => id !== finalLibId);
        for (const id of finalSavedTo.filter(id => !origSavedTo.includes(id))) await onToggleSave(item.id, id);
        for (const id of origSavedTo.filter(id => !finalSavedTo.includes(id))) await onToggleSave(item.id, id);
      }

      onSaved({ ...(data.image || item), title: data.image?.title || (title.trim() || baseName), description, tags: tagValidation.value, content: contentValidation.value, libraryId: finalLibId, savedTo: editSavedTo });
      onToast?.({ visible: true, message: "Perubahan disimpan", type: "success" });
      onClose();
    } catch (err) {
      if (err.status === 401) handleAuthFail();
      else setError(err.message);
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center px-4 overlay-enter" onClick={onClose}>
      <div onClick={(e) => e.stopPropagation()} className={`border rounded-2xl w-full max-w-md p-6 relative max-h-[90vh] overflow-y-auto modal-enter ${T.modalBg}`} role="dialog" aria-modal="true">
        <button onClick={onClose} aria-label="Tutup modal" className={`absolute top-4 right-4 ${T.sub}`}><Icon.X /></button>
        <h2 className="font-display text-xl mb-1">Edit info gambar</h2>
        <p className={`text-xs ${T.sub} mb-4`}>Judul saat ini: <span className="text-amber-400">{item.title}</span></p>
        {error && <p className="text-rose-400 text-xs mb-3">{error}</p>}

        <label className={`text-xs ${T.sub} mb-1 block`}>Nama (tanpa nomor — nomor otomatis disesuaikan)</label>
        <input value={title} onChange={(e) => setTitle(e.target.value)}
          placeholder="Nama gambar" className={`w-full border rounded-lg px-3 py-2 text-sm mb-2 ${T.inputBg}`} />
        <textarea value={description} onChange={(e) => setDescription(e.target.value)}
          placeholder="Deskripsi" rows={2} className={`w-full border rounded-lg px-3 py-2 text-sm mb-2 ${T.inputBg}`} />

        <p className={`text-xs ${T.sub} mb-1.5 flex items-center gap-1`}><Icon.Tag />Tag</p>
        <TagPicker selectedTags={selectedTags} onChange={setSelectedTags} allTags={allTags} T={T} />
        <p className={`text-xs ${T.sub} mb-1.5 mt-2 flex items-center gap-1`}><Icon.Filter />Content</p>
        <ContentPicker selectedContent={contentTags} onChange={setContentTags} T={T} />

        {/* Koleksi management */}
        <div className={`mt-1 mb-4 rounded-xl border p-3 ${T.panel}`}>
          <p className={`text-xs ${T.sub} mb-2 font-medium flex items-center gap-1.5`}>
            📁 Koleksi
          </p>

          {/* Current collections as chips */}
          {allCurrentLibIds.length > 0 || (libCreating && libNewName) ? (
            <div className="flex flex-wrap gap-2 mb-3">
              {allCurrentLibIds.map((id, idx) => {
                const lib = (libraries || []).find(l => l.id === id);
                const isPrimary = id === editLibId;
                return lib ? (
                  <span key={id} className={`flex items-center gap-1 text-xs rounded-full px-2.5 py-1 ${isPrimary ? "bg-amber-400/20 text-amber-400" : T.panel}`}>
                    {lib.name}
                    {isPrimary && <span className="opacity-60 text-[10px]">(utama)</span>}
                    <button onClick={() => removeFromCollection(id)}
                      className="hover:text-rose-400 ml-0.5 font-bold leading-none text-sm">×</button>
                  </span>
                ) : null;
              })}
              {libCreating && libNewName && (
                <span className="flex items-center gap-1 text-xs bg-amber-400/20 text-amber-400 rounded-full px-2.5 py-1">
                  {libNewName} <span className="opacity-60">(baru)</span>
                  <button onClick={() => { setLibCreating(false); setLibNewName(""); }}
                    className="hover:text-rose-400 ml-0.5 font-bold leading-none text-sm">×</button>
                </span>
              )}
            </div>
          ) : (
            <p className={`text-xs ${T.sub} italic mb-2`}>Tidak ada koleksi</p>
          )}

          {/* Add more via LibraryPicker */}
          <p className={`text-xs ${T.sub} mb-1`}>Tambah ke koleksi</p>
          <LibraryPicker
            value=""
            newLibName=""
            libraries={(libraries || []).filter(l => !allCurrentLibIds.includes(l.id))}
            onSelect={addToCollection}
            onCreateNew={(name) => { setLibCreating(true); setLibNewName(name); }}
            onClearNew={() => { setLibCreating(false); setLibNewName(""); }}
            placeholder="Pilih koleksi..."
            hideNone={true}
            T={T}
          />
        </div>

        <p className={`text-xs ${T.sub} mb-4`}>Catatan: nama file di Google Drive tidak ikut berubah secara langsung.</p>
        <button onClick={submit} disabled={saving} className="w-full bg-amber-400 hover:bg-amber-300 text-[#0a0c16] font-medium text-sm py-2.5 rounded-full disabled:opacity-50">
          {saving ? "Menyimpan..." : "Simpan perubahan"}
        </button>
      </div>
    </div>
  );
}

// ── BatchEditModal ──────────────────────────────────────────────────────────
function BatchEditModal({ T, libraries, allTags, selectedCount, onClose, onSubmit }) {
  const [mode, setMode] = useState("add"); // add | remove | replace
  const [selectedTags, setSelectedTags] = useState([]);
  const [contentTags, setContentTags] = useState([]);
  const [applyContent, setApplyContent] = useState(false);
  const [libAction, setLibAction] = useState("none"); // none | move | add | remove
  const [libId, setLibId] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState("");

  const submit = () => {
    setError("");
    const patch = {};
    const tagValidation = HiraValidation.validateTagList(selectedTags);
    if (!tagValidation.valid) { setError(tagValidation.message); return; }
    const contentValidation = HiraValidation.validateContentList(contentTags);
    if (applyContent && !contentValidation.valid) { setError(contentValidation.message); return; }
    if (tagValidation.value.length > 0) {
      if (mode === "add") patch.addTags = tagValidation.value;
      else if (mode === "remove") patch.removeTags = tagValidation.value;
      else patch.tags = tagValidation.value;
    }
    if (applyContent) {
      patch.setContent = contentValidation.value;
    }
    if (libAction === "move" && libId) patch.libraryId = libId;
    else if (libAction === "add" && libId) patch.addToLibraryId = libId;
    else if (libAction === "remove" && libId) patch.removeFromLibraryId = libId;
    if (!Object.keys(patch).length) { setError("Pilih minimal satu aksi."); return; }
    setSubmitting(true);
    onSubmit(patch).then(() => { setSubmitting(false); }).catch(e => { setError(e.message); setSubmitting(false); });
  };

  return (
    <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[55] flex items-center justify-center px-4 overlay-enter" onClick={onClose}>
      <div onClick={e => e.stopPropagation()} className={`border rounded-2xl w-full max-w-md p-6 relative max-h-[90vh] overflow-y-auto modal-enter ${T.modalBg}`} role="dialog" aria-modal="true">
        <button onClick={onClose} aria-label="Tutup modal" className={`absolute top-4 right-4 ${T.sub}`}><Icon.X /></button>
        <h2 className="font-display text-xl mb-1">Edit massal</h2>
        <p className={`text-xs ${T.sub} mb-4`}>{selectedCount} gambar dipilih</p>
        {error && <p className="text-rose-400 text-xs mb-3">{error}</p>}

        {/* Tags section */}
        <div className={`rounded-xl border p-3 mb-3 ${T.panel}`}>
          <p className={`text-xs ${T.sub} mb-2 font-medium flex items-center gap-1`}><Icon.Tag />Tag</p>
          <div className={`flex rounded-full border p-0.5 mb-2 text-xs ${T.chipInactive}`}>
            <button onClick={() => setMode("add")} className={`flex-1 py-1 rounded-full ${mode === "add" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Tambah</button>
            <button onClick={() => setMode("remove")} className={`flex-1 py-1 rounded-full ${mode === "remove" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Hapus</button>
            <button onClick={() => setMode("replace")} className={`flex-1 py-1 rounded-full ${mode === "replace" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Ganti</button>
          </div>
          <TagPicker selectedTags={selectedTags} onChange={setSelectedTags} allTags={allTags} T={T} />
          <p className={`text-[10px] ${T.faint}`}>
            {mode === "add" ? "Tag akan ditambahkan ke item yang belum punya." : mode === "remove" ? "Tag akan dihapus dari item yang punya." : "Tag akan diganti sepenuhnya (hati-hati)."}
          </p>
        </div>

        {/* Content section */}
        <div className={`rounded-xl border p-3 mb-3 ${T.panel}`}>
          <label className="flex items-center gap-2 mb-2 cursor-pointer">
            <input type="checkbox" checked={applyContent} onChange={e => setApplyContent(e.target.checked)} />
            <span className={`text-xs ${T.sub} font-medium flex items-center gap-1`}><Icon.Filter />Ubah Content</span>
          </label>
          {applyContent && <ContentPicker selectedContent={contentTags} onChange={setContentTags} T={T} />}
        </div>

        {/* Library section */}
        <div className={`rounded-xl border p-3 mb-4 ${T.panel}`}>
          <p className={`text-xs ${T.sub} mb-2 font-medium`}>📁 Koleksi</p>
          <div className={`flex rounded-full border p-0.5 mb-2 text-xs ${T.chipInactive}`}>
            <button onClick={() => setLibAction("none")} className={`flex-1 py-1 rounded-full ${libAction === "none" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Lewati</button>
            <button onClick={() => setLibAction("add")} className={`flex-1 py-1 rounded-full ${libAction === "add" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Tambah</button>
            <button onClick={() => setLibAction("move")} className={`flex-1 py-1 rounded-full ${libAction === "move" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Pindah</button>
            <button onClick={() => setLibAction("remove")} className={`flex-1 py-1 rounded-full ${libAction === "remove" ? "bg-amber-400 text-[#0a0c16] font-medium" : ""}`}>Hapus</button>
          </div>
          {libAction !== "none" && (
            <select value={libId} onChange={e => setLibId(e.target.value)} className={`w-full border rounded-lg px-3 py-2 text-sm ${T.inputBg}`}>
              <option value="">Pilih koleksi...</option>
              {libraries.map(l => <option key={l.id} value={l.id}>{l.name}</option>)}
            </select>
          )}
          <p className={`text-[10px] ${T.faint} mt-1`}>
            {libAction === "add" ? "Simpan ke koleksi tanpa mengubah utama." : libAction === "move" ? "Ubah koleksi utama." : libAction === "remove" ? "Hapus dari koleksi." : ""}
          </p>
        </div>

        <button onClick={submit} disabled={submitting} className="w-full bg-amber-400 hover:bg-amber-300 text-[#0a0c16] font-medium text-sm py-2.5 rounded-full disabled:opacity-50">
          {submitting ? "Menyimpan..." : `Terapkan ke ${selectedCount} gambar`}
        </button>
      </div>
    </div>
  );
}

// ── ImportModal ──────────────────────────────────────────────────────────────
function ImportModal({ T, onClose, onResult }) {
  const [importing, setImporting] = useState(false);
  const [error, setError] = useState("");
  const fileRef = useRef(null);

  const handleImport = async () => {
    const file = fileRef.current?.files?.[0];
    if (!file) { setError("Pilih file JSON."); return; }
    setError("");
    setImporting(true);
    try {
      const text = await file.text();
      const preview = HiraValidation.validateImportPayloadPreview(text);
      if (!preview.valid) throw new Error(preview.message || "Preview import tidak valid.");
      const result = await HiraApi.importImages(preview.value);
      onResult(result);
    } catch (error) {
      if (error.status === 401) handleAuthFail();
      else setError(error.message);
    } finally {
      setImporting(false);
    }
  };

  return (
    <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[55] flex items-center justify-center px-4 overlay-enter" onClick={onClose}>
      <div onClick={e => e.stopPropagation()} className={`border rounded-2xl w-full max-w-md p-6 relative modal-enter ${T.modalBg}`}>
        <button onClick={onClose} aria-label="Tutup modal" className={`absolute top-4 right-4 ${T.sub}`}><Icon.X /></button>
        <h2 className="font-display text-xl mb-1">Import Metadata</h2>
        <p className={`text-xs ${T.sub} mb-4`}>Pilih file JSON hasil export. Metadata akan dipulihkan untuk file yang masih ada di Drive.</p>
        {error && <p className="text-rose-400 text-xs mb-3">{error}</p>}
        <input ref={fileRef} type="file" accept=".json" className={`w-full border rounded-lg px-3 py-2 text-sm mb-4 ${T.inputBg}`} />
        <button onClick={handleImport} disabled={importing} className="w-full bg-amber-400 hover:bg-amber-300 text-[#0a0c16] font-medium text-sm py-2.5 rounded-full disabled:opacity-50">
          {importing ? "Mengimpor..." : "Mulai Import"}
        </button>
      </div>
    </div>
  );
}

// ── DeleteConfirmModal ───────────────────────────────────────────────────────
function DeleteConfirmModal({ T, item, visible, onConfirm, onCancel }) {
  if (!visible) return null;
  return (
    <div className="fixed inset-0 z-[60] bg-black/60 backdrop-blur-sm flex items-center justify-center px-4 overlay-enter" onClick={onCancel}>
      <div onClick={(e) => e.stopPropagation()} className={`border rounded-2xl w-full max-w-xs p-6 modal-enter ${T.modalBg}`}>
        <h3 className="font-display text-lg mb-2 text-rose-400">Hapus Gambar</h3>
        <p className={`text-sm mb-5 ${T.sub}`}>
          Hapus <strong>"{item?.title || "gambar ini"}"</strong>? File asli di Drive akan dihapus dan tidak bisa dikembalikan.
        </p>
        <div className="flex gap-2">
          <button onClick={onCancel} className={`flex-1 px-3 py-2.5 rounded-full text-sm border ${T.chipInactive}`}>Batal</button>
          <button onClick={onConfirm} className="flex-1 bg-rose-500 text-white text-sm py-2.5 rounded-full font-medium">Hapus</button>
        </div>
      </div>
    </div>
  );
}

// ── ExpiryPickerModal (P1 share expiry) ──────────────────────────────────────
function ExpiryPickerModal({ T, request, onClose, onCreated, onToast }) {
  const [selected, setSelected] = React.useState("7");
  const [creating, setCreating] = React.useState(false);
  React.useEffect(() => { setSelected("7"); }, [request]);
  if (!request) return null;
  const options = SHARE_EXPIRY_CHOICES;
  const computeExpiry = () => {
    const validation = HiraValidation.validateShareExpiryChoice(selected);
    const opt = options.find((option) => option.id === validation.value);
    if (!opt || opt.days===null) return null;
    const d = new Date();
    d.setDate(d.getDate()+opt.days);
    return d.toISOString();
  };
  const handleCreate = async () => {
    setCreating(true);
    try {
      const expiryValidation = HiraValidation.validateShareExpiryChoice(selected);
      if (!expiryValidation.valid) throw new Error(expiryValidation.message);
      const expiresAt = computeExpiry();
      const data = await HiraApi.createShareLink(request.type, request.targetId, expiresAt);
      const url = data.link?.publicUrl;
      if (!url) throw new Error("URL share tidak tersedia");
      await HiraFormat.copyText(url);
      const daysLabel = selected==="none" ? "tanpa kedaluwarsa" : `${selected} hari`;
      onToast?.(`Link publik disalin, aktif ${daysLabel}`, "success");
      onCreated?.(data.link);
      onClose();
    } catch (e) {
      if (e.status===401) handleAuthFail();
      else onToast?.(e.message||"Gagal membuat link", "error");
    } finally { setCreating(false); }
  };
  return (
    <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[60] flex items-center justify-center px-4 overlay-enter" onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} className={`border rounded-2xl w-full max-w-sm p-6 modal-enter ${T.modalBg}`}>
        <h3 className="font-display text-lg mb-1">Masa aktif link publik</h3>
        <p className={`text-xs ${T.sub} mb-4`}>Pilih berapa lama link dapat diakses. Default direkomendasikan 7 hari.</p>
        <div className="space-y-2 mb-5">
          {options.map(opt => (
            <button key={opt.id} onClick={()=>setSelected(opt.id)} className={`w-full text-left px-4 py-3 rounded-xl border flex items-center justify-between ${selected===opt.id ? "border-amber-400 bg-amber-400/10 text-amber-400" : T.chipInactive}`}>
              <span className="text-sm">{opt.label}</span>
              <span className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${selected===opt.id?"border-amber-400 bg-amber-400":"border-white/20"}`}>
                {selected===opt.id && <span className="w-1.5 h-1.5 rounded-full bg-[#0a0c16]" />}
              </span>
            </button>
          ))}
        </div>
        <div className="flex gap-2">
          <button onClick={onClose} className={`flex-1 px-3 py-2.5 rounded-full text-sm border ${T.chipInactive}`}>Batal</button>
          <button onClick={handleCreate} disabled={creating} className="flex-1 bg-amber-400 text-[#0a0c16] text-sm py-2.5 rounded-full font-medium disabled:opacity-50">{creating?"Membuat...":"Buat & Salin link"}</button>
        </div>
      </div>
    </div>
  );
}


// ── ImportFullBackupModal (v2) ──────────────────────────────────────────────
function ImportFullBackupModal({ T, onClose, onResult }) {
  const [fileData, setFileData] = useState(null);
  const [error, setError] = useState("");
  const [dryRunLoading, setDryRunLoading] = useState(false);
  const [importing, setImporting] = useState(false);
  const [summary, setSummary] = useState(null);
  const [conflictMode, setConflictMode] = useState("upsert");
  const [restoreShareLinks, setRestoreShareLinks] = useState(false);
  const [includeDeleted, setIncludeDeleted] = useState(true);
  const fileRef = useRef(null);

  const handleFileChange = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    setError("");
    setSummary(null);
    try {
      const text = await file.text();
      const parsed = JSON.parse(text);
      if (parsed.version !== 2 || parsed.app !== "hira-library") {
        setError("File backup tidak valid. Harus versi 2 dan app 'hira-library'.");
        setFileData(null);
        return;
      }
      setFileData(parsed);
      const hasRealTokens = (parsed.shareLinks || []).some(link => link.token && !link.token.includes("..."));
      setRestoreShareLinks(hasRealTokens);
    } catch (err) {
      setError("Gagal membaca file JSON: " + err.message);
      setFileData(null);
    }
  };

  const runImport = async (dryRun) => {
    if (!fileData) return;
    setError("");
    setSummary(null);
    if (dryRun) setDryRunLoading(true);
    else setImporting(true);

    try {
      const params = {
        dryRun: dryRun ? "true" : "false",
        conflictMode,
        restoreShareLinks: restoreShareLinks ? "true" : "false",
        includeDeleted: includeDeleted ? "true" : "false"
      };
      const result = await HiraApi.importFullBackup(fileData, params);
      setSummary(result);
      if (!dryRun) {
        onResult?.(result);
      }
    } catch (err) {
      if (err.status === 401) handleAuthFail();
      else setError(err.message || "Gagal mengimpor backup.");
    } finally {
      setDryRunLoading(false);
      setImporting(false);
    }
  };

  const hasMaskedTokens = fileData && (fileData.shareLinks || []).some(link => link.token && link.token.includes("..."));

  return (
    <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-[55] flex items-center justify-center px-4 overlay-enter" onClick={onClose}>
      <div onClick={e => e.stopPropagation()} className={`border rounded-2xl w-full max-w-md p-6 relative max-h-[90vh] overflow-y-auto modal-enter ${T.modalBg}`} role="dialog" aria-modal="true">
        <button onClick={onClose} aria-label="Tutup modal" className={`absolute top-4 right-4 ${T.sub}`}><Icon.X /></button>
        <h2 className="font-display text-xl mb-1">Import Full Backup (v2)</h2>
        <p className={`text-xs ${T.sub} mb-4`}>Unggah file backup lengkap untuk memulihkan library, images, dan share links.</p>
        
        {error && <p className="text-rose-400 text-xs mb-3 whitespace-pre-line">{error}</p>}
        
        <input ref={fileRef} type="file" accept=".json" onChange={handleFileChange} className={`w-full border rounded-lg px-3 py-2 text-sm mb-4 ${T.inputBg}`} />
        
        {fileData && (
          <div className={`p-4 rounded-xl border mb-4 text-xs ${T.panel} space-y-2`}>
            <p className="font-semibold text-amber-400">Preview Backup:</p>
            <p><span className={T.sub}>Versi:</span> {fileData.version}</p>
            <p><span className={T.sub}>Tanggal Ekspor:</span> {new Date(fileData.exportedAt).toLocaleString("id-ID")}</p>
            <p><span className={T.sub}>Jumlah Item:</span></p>
            <ul className="list-disc pl-5 space-y-0.5 text-stone-300">
              <li>Images: {fileData.counts?.images || 0}</li>
              <li>Libraries: {fileData.counts?.libraries || 0}</li>
              <li>Share Links: {fileData.counts?.shareLinks || 0}</li>
              <li>Accounts: {fileData.counts?.accounts || 0} (hanya metadata)</li>
            </ul>

            {hasMaskedTokens && (
              <div className="text-amber-400 font-medium mt-2 flex items-start gap-1">
                <span>⚠</span>
                <span>Share link dalam backup ini ter-masking (sensor). Link tidak dapat dipulihkan jika di-import.</span>
              </div>
            )}
            {!hasMaskedTokens && fileData.shareLinks?.length > 0 && (
              <div className="text-emerald-400 font-medium mt-2 flex items-start gap-1">
                <span>✓</span>
                <span>Share link menyertakan token penuh dan dapat dipulihkan dengan aman.</span>
              </div>
            )}
          </div>
        )}

        {fileData && (
          <div className="space-y-3 mb-4">
            <div>
              <label className={`text-xs ${T.sub} mb-1 block`}>Mode Konflik</label>
              <select value={conflictMode} onChange={e => setConflictMode(e.target.value)} className={`w-full border rounded-lg px-3 py-1.5 text-xs ${T.inputBg}`}>
                <option value="upsert">Upsert (Timpa data lama)</option>
                <option value="skip-existing">Skip Existing (Hanya tambah yang baru)</option>
              </select>
            </div>

            <div className="flex items-center gap-3">
              <label className="flex items-center gap-1.5 text-xs cursor-pointer">
                <input type="checkbox" checked={includeDeleted} onChange={e => setIncludeDeleted(e.target.checked)} />
                Sertakan item Trash
              </label>

              <label className="flex items-center gap-1.5 text-xs cursor-pointer">
                <input type="checkbox" checked={restoreShareLinks} disabled={hasMaskedTokens} onChange={e => setRestoreShareLinks(e.target.checked)} />
                Pulihkan share links
              </label>
            </div>
          </div>
        )}

        {summary && (
          <div className={`p-4 rounded-xl border mb-4 text-xs ${T.panel} space-y-1.5`}>
            <p className="font-semibold text-emerald-400">Ringkasan Hasil:</p>
            <p><span className={T.sub}>Libraries:</span> {summary.libraries?.imported || 0} baru, {summary.libraries?.updated || 0} update, {summary.libraries?.skipped || 0} dilewati</p>
            <p><span className={T.sub}>Images:</span> {summary.images?.imported || 0} baru, {summary.images?.updated || 0} update, {summary.images?.skipped || 0} dilewati</p>
            <p><span className={T.sub}>Share Links:</span> {summary.shareLinks?.imported || 0} baru, {summary.shareLinks?.updated || 0} update, {summary.shareLinks?.skipped || 0} dilewati</p>
            <p><span className={T.sub}>Accounts:</span> {summary.accounts?.skipped || 0} dilewati</p>
            {summary.errors?.length > 0 && (
              <details className="mt-2 text-rose-400">
                <summary className="cursor-pointer font-semibold">{summary.errors.length} error terjadi</summary>
                <ul className="list-disc pl-5 mt-1 max-h-24 overflow-y-auto space-y-0.5 text-stone-300">
                  {summary.errors.map((err, i) => <li key={i}>{err}</li>)}
                </ul>
              </details>
            )}
          </div>
        )}

        {fileData && (
          <div className="flex gap-2">
            <button onClick={() => runImport(true)} disabled={dryRunLoading || importing} className={`flex-1 text-xs py-2.5 rounded-full border ${T.chipInactive}`}>
              {dryRunLoading ? "Memeriksa..." : "Dry Run"}
            </button>
            <button onClick={() => runImport(false)} disabled={dryRunLoading || importing} className="flex-1 bg-amber-400 text-[#0a0c16] text-xs py-2.5 rounded-full font-medium">
              {importing ? "Mengimpor..." : "Import"}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

  window.HiraModals = {
    UploadModal,
    DevAuthModal,
    EditModal,
    BatchEditModal,
    ImportModal,
    ImportFullBackupModal,
    DeleteConfirmModal,
    ExpiryPickerModal,
  };
})();
